Veronica Chatbot - Sistema AI WordPress con Tool-based Retrieval ai-projects

Link Progetto

Come ho trasformato un esperimento del corso di Ed Donner in un chatbot professionale integrato nel mio sito WordPress

📖 Il Punto di Partenza: Agenti AI Costruiti a Mano

La settimana scorsa ho seguito il corso di Ed Donner sulla costruzione di agenti AI, dove abbiamo imparato a creare agenti "from scratch"

Qui il link per leggere l'articolo completo!

Era affascinante vedere il meccanismo di reasoning esposto, ma mi sono resa conto di due limitazioni:

  1. Scalabilità: Ogni nuovo tool richiedeva modifiche manuali al loop
  2. Robustezza: Gestione errori e edge cases complessa da implementare
  3. Manutenzione: Codice verboso e difficile da estendere

L'Idea di Evoluzione

Navigando il mio sito web, mi sono chiesta: "E se il mio chatbot potesse rispondere in modo intelligente su tutti i miei contenuti?"

Il problema da risolvere:

  • Avevo già un sito WordPress con progetti, articoli, certificazioni
  • Volevo un assistente che mi rappresentasse professionalmente
  • Serviva qualcosa di più robusto degli esperimenti del corso

La visione:

Un sistema AI con tool-based retrieval che trasformi il mio sito in una conversazione intelligente

🧠 L'Architettura: Da Manuale a LangGraph

Perché LangGraph invece del Loop Manuale

Il pattern ReAct (Reasoning-Acting) di LangGraph risolve elegantemente i problemi del corso:

# LangGraph: Gestione automatica del pattern ReAct
def should_continue(state: State) -> Literal["tools", "__end__"]:
    """Decide se continuare con i tools o terminare"""
    messages = state["messages"]
    last_message = messages[-1]

    if last_message.tool_calls:
        return "tools"
    return "__end__"

def create_graph():
    builder = StateGraph(State, input=InputState, config_schema=Configuration)
    
    # Core nodes
    builder.add_node("agent", call_model)      # 🧠 Reasoning
    builder.add_node("tools", ToolNode(TOOLS)) # 🛠️ Actions
    
    # ReAct flow automatico
    builder.add_conditional_edges("agent", should_continue, {
        "tools": "tools",
        "__end__": "__end__"
    })
    builder.add_edge("tools", "agent")  # Loop automatico
    
    return builder.compile(checkpointer=MemorySaver())Code language: PHP (php)

Vantaggi immediati:

  • Loop automatico: Il framework gestisce il flusso ReAct
  • Scaling semplice: Aggiungi tools, LangGraph li orchestra
  • Memory integrata: Conversation persistence built-in
  • Error handling: Robusto e standardizzato
  • Observability: LangSmith tracing automatico

🔗 WordPress REST API vs Scraping: La Scelta Architetturale

Perché Non Scraping

Nel corso usavamo spesso scraping per recuperare dati, ma per il mio sito ho scelto un approccio diverso:

# ✅ WordPress REST API (robusto)
class OptimizedWordPressClient:
    def get_posts(self, params: Dict = None) -> List[Dict]:
        """Recupera post del blog ottimizzato"""
        return self._make_request("posts", params) or []
    
    def get_projects(self, params: Dict = None) -> List[Dict]:
        """Recupera progetti del portfolio ottimizzato"""
        return self._make_request("projects", params) or []

Vantaggi dell'API:

  • 📊 Dati strutturati: JSON invece di HTML parsing
  • 🔧 Custom Post Types: Accesso a progetti, certificazioni, tools
  • 🏷️ ACF Fields: Metadati ricchi (repository, external_url, rating)
  • 🔄 Aggiornamenti automatici: Nuovi contenuti disponibili subito
  • 🛡️ Standardizzazione: Endpoint WordPress consolidati

Design dei Custom Post Types

Ho progettato una struttura dati ottimizzata per il chatbot:

self.field_configs = {
    "projects": {
        "fields": "id,date,title,content,excerpt,link,acf,project-category",
        "acf_fields": [
            "project_external_url", "project_preview_text",
            "project_repository", "project_frontend"
        ],
        "description": "Progetti portfolio con ACF"
    },
    "certifications": {
        "fields": "id,date,title,content,link,acf",
        "acf_fields": [
            "ente_certificazione", "descrizione_certificazione",
            "start_corso", "end_corso", "link_corso"
        ],
        "description": "Certificazioni e formazione"
    },
    "tools": {
        "acf_fields": [
            "tool_url", "tool_category", "tool_description", "tool_rating"
        ],
        "description": "Strumenti e stack tecnologico"
    }
}Code language: PHP (php)

Ogni content type ha:

  • Campi base: Title, content, date, link
  • Metadati specializzati: ACF fields per context specifico
  • Processing ottimizzato: Estrazione automatica del contenuto rilevante

🛠️ Backend: Sistema AI con Tool-based Retrieval

Architettura del Sistema Tool-based

Il mio sistema implementa un tool-based retrieval completo:

@tool
def search_all_content(query: str, limit_per_type: int = 3) -> str:
    """
    Ricerca generale nei contenuti di Veronica (articoli, progetti, certificazioni, etc.).
    """
    try:
        config = Configuration()
        wp_client = OptimizedWordPressClient(config.wordpress_base_url)
        
        results = {"search_query": query, "results": {}}
        
        # 🔍 RETRIEVAL: Multi-source search
        posts = wp_client.get_posts({"search": query, "per_page": limit_per_type})
        projects = wp_client.get_projects({"search": query, "per_page": limit_per_type})
        certifications = wp_client.get_certifications({"search": query, "per_page": limit_per_type})
        
        # 📝 PROCESSING: Content extraction and cleaning
        if posts:
            results["results"]["articles"] = [
                ContentProcessor.process_post(p) for p in posts]
        if projects:
            results["results"]["projects"] = [
                ContentProcessor.process_project(p) for p in projects]
        
        # 🤖 PROCESSING: Structured data for LLM context
        return json.dumps(results)Code language: PHP (php)

9 Tools Specializzati per Content Retrieval

Invece di un tool generico, ho creato tools specializzati per ogni tipo di contenuto:

TOOLS = [
    search_blog_posts,          # 📝 Articoli del blog
    get_latest_blog_post,       # 🆕 Ultimo articolo pubblicato
    get_portfolio_projects,     # 💼 Progetti del portfolio
    get_certifications,         # 🎓 Certificazioni e formazione
    get_work_experience,        # 👔 Esperienze lavorative
    get_books_and_reading,      # 📚 Libri letti e recensiti
    get_tools_and_stack,        # 🔧 Stack tecnologico
    search_all_content,         # 🔍 Ricerca globale
    get_contact_info           # 📞 Informazioni contatto
]Code language: PHP (php)

Ogni tool:

  • È ottimizzato per il suo content type specifico
  • Processa i dati in modo context-aware
  • Restituisce JSON strutturato per l'LLM

Content Processing Intelligente

class ContentProcessor:
    @staticmethod
    def process_project(project: Dict) -> Dict:
        """Processa un progetto del portfolio"""
        title = project.get("title", {}).get("rendered", "")
        content = project.get("content", {}).get("rendered", "")
        
        # Estrai campi ACF specifici per progetti
        acf = project.get("acf", {})
        
        return {
            "title": title,
            "description": ContentProcessor.clean_html(content)[:400] + "...",
            "repository": acf.get("project_repository", ""),
            "external_url": acf.get("project_external_url", ""),
            "frontend_url": acf.get("project_frontend", ""),
            "preview_text": acf.get("project_preview_text", ""),
            "type": "project"
        }

Features del processing:

  • 🧹 HTML cleaning: Rimozione tag e entità HTML
  • ✂️ Content truncation: Limite lunghezza per token efficiency
  • 🏷️ Metadata extraction: ACF fields strutturati
  • 🎯 Type classification: Ogni content ha il suo tipo

🧠 System Prompt Engineering: L'Arte dell'Identità AI

La Sfida dell'Impersonificazione Professionale

Una delle parti più complesse è stata creare un system prompt che faccia sentire l'AI come "Veronica digitale". Il prompt finale è lungo 200+ righe e rappresenta il vero "cervello" del sistema:

def create_system_prompt() -> str:
    """Crea il system prompt professionale"""
    personal_summary = load_personal_summary()

    return f"""Tu sei Veronica Schembri, AI Engineer e Data Scientist.

PERSONALITÀ E BACKGROUND:
Rappresenti Veronica sul suo sito web per potenziali clienti e datori di lavoro.
Il tuo background include:
- Percorso dall'illustrazione → programmazione → AI
- Specializzazione in Data Science, LLM e agenti AI
- Esperienza in web development e design visivo
- Metodo di lavoro strutturato (sistemi organizzativi, Building a Second Brain)
- Passioni: serie TV, fumetti, Magic: The Gathering, Lego

INFORMAZIONI DI CONTATTO:
📧 Email principale: veronicaschembri@gmail.com
🌐 Sito web: https://www.veronicaschembri.com
💼 LinkedIn: https://www.linkedin.com/in/veronicaschembri/
🐙 GitHub: https://github.com/Pandagan-85/


"""Code language: PHP (php)

Tool Orchestration via Prompt

Il prompt mappa esplicitamente ogni tipo di query ai tools appropriati:

STRUMENTI DISPONIBILI:
1. search_blog_posts(query, limit): cerca negli articoli del blog per argomenti specifici
2. get_latest_blog_post(): ottieni l'ultimo articolo pubblicato con contenuto completo
3. get_portfolio_projects(category, limit): ottieni progetti del portfolio
4. get_certifications(limit): ottieni tutte le certificazioni e formazione

PRIORITÀ NELLE RISPOSTE:
1. USA SEMPRE i tool per domande su:
✅ "Quali certificazioni hai?" → get_certifications()
✅ "Di cosa parla l'ultimo articolo?" → get_latest_blog_post()
✅ "Progetti con tecnologia X" → get_portfolio_projects()
✅ "Articoli su argomento Y" → search_blog_posts("Y")

ESEMPI DI QUERY DA GESTIRE CON I TOOL:
- "Quali certificazioni hai conseguito?" → get_certifications()
- "Hai formazione su AI?" → get_certifications()
- "Di cosa parla il tuo ultimo articolo?" → get_latest_blog_post()
- "Progetti di machine learning?" → search_all_content("machine learning")
- "Come posso contattarti?" → get_contact_info()Code language: PHP (php)

Boundary Setting Professionale

Parte critica: definire cosa l'AI può e non può fare per mantenere il focus professionale:

REGOLE FONDAMENTALI:
🚫 RISPONDI SOLO SU ARGOMENTI RIGUARDANTI VERONICA SCHEMBRI:
- Il suo background professionale e percorso
- I suoi progetti, articoli e competenze
- Le sue esperienze e tecnologie utilizzate

🚫 NON RISPONDERE A DOMANDE GENERICHE come:
- "Come creare un sito WordPress?"
- "Tutorial su tecnologie generiche"
- "Spiegazioni teoriche non legate alla tua esperienza"

✅ INVECE, PER DOMANDE GENERICHE, REINDIRIZZA COSÌ:
"Interessante! Nel mio blog e nei miei progetti affronto spesso argomenti tecnici come questo. 
Ti suggerisco di esplorare i miei contenuti per vedere la mia prospettiva specifica, 
oppure contattami se vuoi discutere di una collaborazione su progetti simili!"

COMPORTAMENTO:
- Sii professionale ma coinvolgente
- Rimani sempre focalizzata su Veronica e il suo lavoro
- Integra i risultati dei tool in modo naturale nella conversazione
- Mostra curiosità per le esigenze del visitatore in relazione al lavoro di Veronica
- Incoraggia collaborazioni e contatti professionali
- NON INVENTARE MAI informazioni - se non sai, usa i tool appropriatiCode language: PHP (php)

Integrazione con il Summary Personale

Il prompt carica dinamicamente il mio profilo personale dal file me/summary.txt:

def load_personal_summary() -> str:
    """Carica il summary personale dalla cartella me/"""
    try:
        summary_path = "me/summary.txt"
        if os.path.exists(summary_path):
            with open(summary_path, "r", encoding="utf-8") as f:
                return f.read()Code language: JavaScript (javascript)

Questo permette di aggiornare la personalità dell'AI semplicemente modificando un file di testo, senza toccare il codice.

Risultato: Conversazioni Autentiche

Il prompt engineerging ha prodotto conversazioni che sembrano davvero parlare con me:

  • Prima persona: "Ho lavorato su...", "Nel mio progetto..."
  • Dettagli specifici: Cita progetti, tecnologie, esperienze reali
  • Boundaries professionali: Non diventa un ChatGPT generico
  • Call-to-action naturali: Incoraggia contatti per collaborazioni
  • Coerenza: Ogni risposta è allineata con il mio brand professionale

🚀 Deploy e Observability

Railway Deploy

Per il deploy ho scelto Railway per la sua semplicità.

Environment variables:

OPENAI_API_KEY=sk-...
WORDPRESS_URL=https://www.veronicaschembri.com
LANGSMITH_API_KEY=lsv2_... # Per monitoring
LANGSMITH_PROJECT=veronica-wordpress-chatbotCode language: PHP (php)

LangSmith Monitoring

Integrazione completa con LangSmith per osservabilità:

@traceable(name="wordpress_chatbot_request")
def process_chat_with_tracing(message: str, thread_id: str) -> tuple[str, Optional[str]]:
    """Processa la chat con tracing LangSmith"""
    if chatbot is None:
        raise Exception("Chatbot non inizializzato")
    
    # LangSmith traccia automaticamente:
    # - Ogni tool call
    # - Token usage
    # - Latency
    # - Error rate
    response = chatbot.chat(message, thread_id)
    
    return response, trace_urlCode language: PHP (php)

🎨 Frontend: React Widget con Sicurezza

React Component Architecture

Il frontend è un React component completamente autocontenuto:

function VeronicaChatbot() {
    // 💾 Storage manager per persistenza
    const [storageManager] = React.useState(() => new ChatStorageManager());
    
    // 🎯 Stato con persistenza cross-page
    const [session, setSession] = React.useState(() => 
        storageManager.getOrCreateSession()
    );
    const [messages, setMessages] = React.useState(() => 
        storageManager.loadMessages()
    );
    
    // 🔄 UI state persistente
    const [uiState, setUIState] = React.useState(() => 
        storageManager.loadUIState()
    );
}Code language: JavaScript (javascript)

La sicurezza è stata una priorità assoluta:

function validateInputSecure(input) {
    if (!input || typeof input !== "string") return false;
    if (input.length > 1000) return false;
    
    // 🛡️ Lista completa di pattern XSS
    const xssPatterns = [
        /<script/i, /<\/script>/i, /<iframe/i, /<object/i,
        /javascript:/i, /vbscript:/i, /on\w+\s*=/i,
        /&lt;script/i, /&#60;script/i, /alert\s*\(/i,
        /eval\s*\(/i, /document\./i, /window\./i
        // ... 20+ pattern di sicurezza
    ];
    
    const hasXSS = xssPatterns.some(pattern => pattern.test(input));
    if (hasXSS) {
        logSecurityEvent("xss_attempt_blocked", input);
        return false;
    }
    
    return true;
}Code language: PHP (php)

Livelli di sicurezza:

  1. Input validation: Pre-filtering rigoroso
  2. Sanitization: Rimozione completa HTML per input utente
  3. Safe rendering: Markdown processing sicuro per bot responses
  4. Security logging: Event tracking per monitoring

🔌 Integrazione WordPress: Plugin Architecture

Plugin WordPress Completo

Ho sviluppato un plugin WordPress completo per l'integrazione:

class VeronicaChatbotPlugin {
    public function __construct() {
        add_action('init', array($this, 'init'));
        add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
        add_action('admin_menu', array($this, 'add_admin_menu'));
        add_action('wp_footer', array($this, 'render_chatbot'));
    }
    
    public function enqueue_scripts() {
        // Load React da CDN
        wp_enqueue_script('react', 'https://unpkg.com/react@18/...');
        
        // Load chatbot con configurazione
        wp_localize_script('veronica-chatbot', 'veronicaChatbotConfig', array(
            'apiUrl' => esc_url($options['api_url']),
            'theme' => sanitize_text_field($options['theme']),
            'enablePersistence' => (bool)$options['enable_persistence']
        ));
    }
}Code language: PHP (php)

Admin Panel Avanzato

Il plugin include un pannello di amministrazione completo:

class VeronicaChatbotPlugin {
    public function __construct() {
        add_action('init', array($this, 'init'));
        add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
        add_action('admin_menu', array($this, 'add_admin_menu'));
        add_action('wp_footer', array($this, 'render_chatbot'));
    }
    
    public function enqueue_scripts() {
        // Load React da CDN
        wp_enqueue_script('react', 'https://unpkg.com/react@18/...');
        
        // Load chatbot con configurazione
        wp_localize_script('veronica-chatbot', 'veronicaChatbotConfig', array(
            'apiUrl' => esc_url($options['api_url']),
            'theme' => sanitize_text_field($options['theme']),
            'enablePersistence' => (bool)$options['enable_persistence']
        ));
    }
}Code language: PHP (php)

Admin Panel

⚙️ Configurazione API: URL backend, theme, posizione 📊 Statistiche usage: Conversazioni, messaggi, sessioni attive 🧪 Test connessione: Verifica API endpoint in tempo reale 🔧 Debug mode: Console debugging per sviluppo 💾 Gestione persistenza: Controlli durata sessione e timeout

Cross-Page Persistence

La conversazione rimane aperta mentre l'utente naviga tra pagine del sito, creando un'esperienza seamless.

💭 Riflessioni Finali

Questo progetto rappresenta il mio primo vero sistema full-stack, dove ho dovuto integrare tecnologie diverse e competenze multiple:

  • 🧠 AI/ML: LangGraph, tool-based retrieval, conversation memory, prompt engineering"g
  • ⚙️ Backend: FastAPI, async programming, API design, error handling
  • 🎨 Frontend: React, responsive design, security, state management
  • 🔌 Integration: WordPress plugin, REST API, environment management
  • 📊 Observability: Monitoring, logging, debugging, performance tracking
  • 🚀 DevOps: Railway deployment, CI/CD, production management

Il salto qualitativo rispetto agli esperimenti del corso è evidente: da prototype educativo a sistema in produzione utilizzato quotidianamente da visitatori reali.

Related Projects

Novembre 11, 2025
Nextcloud Self-Hosted: Il Salto da Setup Manuale a Infrastructure as Code

Nextcloud self-hosted è la mia soluzione per sostituire Google Calendar, Contacts e Drive con un'alternativa open-source che controllo completamente. Deploy su Oracle Cloud Infrastructure usando il free tier (4 OCPU ARM, 24GB RAM - gratis per sempre). Il problema: La prima versione funzionava, ma era fragile. Setup manuale con 50+ comandi, 1GB RAM che crashava, […]

Aprile 12, 2025
AI Travel Agent with AWS

Ogni tanto capita un progetto che ti cambia la prospettiva. Per me, questo è stato uno di quelli. Durante il corso Build AI Agents with AWS di Zero To Mastery, ho avuto la sensazione di aver sbloccato un nuovo livello: Mi ha fatto scoprire un mondo completamente nuovo: quello dell’orchestrazione multi-agente in cloud, dove ogni […]

veronicaschembri
Copyright © Veronica Schembri

Privacy Policy
Cookie Policy
💬