From e7e1837fae2be0d99ee637bd3e24aa825d2feea7 Mon Sep 17 00:00:00 2001 From: fcarraUniSa Date: Tue, 17 Feb 2026 10:08:11 +0100 Subject: [PATCH] Update geminiService.ts --- services/geminiService.ts | 237 +++++++++++++++++++++++--------------- 1 file changed, 144 insertions(+), 93 deletions(-) diff --git a/services/geminiService.ts b/services/geminiService.ts index c93d3c2..5a66ab6 100644 --- a/services/geminiService.ts +++ b/services/geminiService.ts @@ -1,5 +1,30 @@ import { GoogleGenAI } from "@google/genai"; -import { KBArticle, Ticket, TicketStatus } from "../types"; +import { AiProvider, KBArticle, Ticket, TicketStatus } from "../types"; + +// --- OPENROUTER / OPENAI COMPATIBLE FETCH --- +async function callOpenRouter(apiKey: string, model: string, messages: any[]) { + const response = await fetch("https://openrouter.ai/api/v1/chat/completions", { + method: "POST", + headers: { + "Authorization": `Bearer ${apiKey}`, + "Content-Type": "application/json", + "HTTP-Referer": window.location.origin, // Required by OpenRouter + "X-Title": "OmniSupport AI" // Optional + }, + body: JSON.stringify({ + model: model || "openai/gpt-3.5-turbo", + messages: messages + }) + }); + + if (!response.ok) { + const err = await response.text(); + throw new Error(`OpenRouter API Error: ${err}`); + } + + const data = await response.json(); + return data.choices[0]?.message?.content || ""; +} /** * Agent 1: Customer Support Chat @@ -9,59 +34,74 @@ export const getSupportResponse = async ( apiKey: string, userQuery: string, chatHistory: string[], - knowledgeBase: KBArticle[] + knowledgeBase: KBArticle[], + provider: AiProvider = AiProvider.GEMINI, + model: string = 'gemini-3-flash-preview' ): Promise => { if (!apiKey) { return "L'assistente AI non è configurato (API Key mancante). Contatta l'amministratore."; } + // Prepare Context from KB + const kbContext = knowledgeBase.map(a => { + if (a.type === 'url') { + return `Fonte Esterna [${a.category}]: ${a.title} - URL: ${a.url}\nContenuto Estratto: ${a.content}`; + } + return `Articolo [${a.category}]: ${a.title}\nContenuto: ${a.content}`; + }).join('\n\n'); + + const systemInstructionText = ` + Sei "OmniSupport AI", un assistente clienti virtuale globale. + + IL TUO COMPITO: + Rispondere alle domande dei clienti basandoti ESCLUSIVAMENTE sulla seguente Base di Conoscenza (KB) fornita in ITALIANO. + + GESTIONE LINGUA (IMPORTANTE): + 1. Rileva automaticamente la lingua utilizzata dall'utente nel suo ultimo messaggio. + 2. Anche se la KB è in Italiano, devi tradurre mentalmente la richiesta, cercare la risposta nella KB Italiana, e poi RISPONDERE NELLA LINGUA DELL'UTENTE. + + BASE DI CONOSCENZA (ITALIANO): + ${kbContext} + + REGOLE: + 1. Se la risposta è nella KB, forniscila. + 2. Se l'articolo è una fonte web (URL), usa il "Contenuto Estratto" per rispondere e fornisci anche il link originale all'utente. + 3. Se la risposta NON si trova nella KB, ammettilo gentilmente (nella lingua dell'utente) e consiglia di aprire un ticket. + 4. Sii cortese, professionale e sintetico. + `; + try { - const ai = new GoogleGenAI({ apiKey }); - - // Prepare Context from KB - const kbContext = knowledgeBase.map(a => { - if (a.type === 'url') { - return `Fonte Esterna [${a.category}]: ${a.title} - URL: ${a.url}\nContenuto Estratto: ${a.content}`; - } - return `Articolo [${a.category}]: ${a.title}\nContenuto: ${a.content}`; - }).join('\n\n'); - - const systemInstruction = ` - Sei "OmniSupport AI", un assistente clienti virtuale globale. + if (provider === AiProvider.OPENROUTER || provider === AiProvider.OPENAI || provider === AiProvider.DEEPSEEK) { + // Logic for OpenRouter/OpenAI compatible APIs + const messages = [ + { role: "system", content: systemInstructionText }, + ...chatHistory.map(msg => ({ role: "user", content: msg })), + { role: "user", content: userQuery } + ]; - IL TUO COMPITO: - Rispondere alle domande dei clienti basandoti ESCLUSIVAMENTE sulla seguente Base di Conoscenza (KB) fornita in ITALIANO. - - GESTIONE LINGUA (IMPORTANTE): - 1. Rileva automaticamente la lingua utilizzata dall'utente nel suo ultimo messaggio. - 2. Anche se la KB è in Italiano, devi tradurre mentalmente la richiesta, cercare la risposta nella KB Italiana, e poi RISPONDERE NELLA LINGUA DELL'UTENTE. + const response = await callOpenRouter(apiKey, model, messages); + return response || "Mi dispiace, non riesco a generare una risposta al momento."; - BASE DI CONOSCENZA (ITALIANO): - ${kbContext} + } else { + // Default to Google Gemini + const ai = new GoogleGenAI({ apiKey }); + const response = await ai.models.generateContent({ + model: model, + contents: [ + ...chatHistory.map(msg => ({ role: 'user', parts: [{ text: msg }] })), + { role: 'user', parts: [{ text: userQuery }] } + ], + config: { + systemInstruction: systemInstructionText, + temperature: 0.3, + } + }); + return response.text || "Mi dispiace, non riesco a generare una risposta al momento."; + } - REGOLE: - 1. Se la risposta è nella KB, forniscila. - 2. Se l'articolo è una fonte web (URL), usa il "Contenuto Estratto" per rispondere e fornisci anche il link originale all'utente. - 3. Se la risposta NON si trova nella KB, ammettilo gentilmente (nella lingua dell'utente) e consiglia di aprire un ticket. - 4. Sii cortese, professionale e sintetico. - `; - - const response = await ai.models.generateContent({ - model: 'gemini-3-flash-preview', - contents: [ - ...chatHistory.map(msg => ({ role: 'user', parts: [{ text: msg }] })), - { role: 'user', parts: [{ text: userQuery }] } - ], - config: { - systemInstruction: systemInstruction, - temperature: 0.3, - } - }); - - return response.text || "Mi dispiace, non riesco a generare una risposta al momento."; } catch (error) { - console.error("Gemini Error:", error); - return "Si è verificato un errore nel servizio AI (Verifica API Key o connessione)."; + console.error("AI Service Error:", error); + return "Si è verificato un errore nel servizio AI (Verifica API Key, Provider o connessione)."; } }; @@ -71,61 +111,71 @@ export const getSupportResponse = async ( export const generateNewKBArticle = async ( apiKey: string, resolvedTickets: Ticket[], - existingArticles: KBArticle[] + existingArticles: KBArticle[], + provider: AiProvider = AiProvider.GEMINI, + model: string = 'gemini-3-pro-preview' ): Promise<{ title: string; content: string; category: string } | null> => { if (!apiKey) return null; + // Filter only resolved tickets + const relevantTickets = resolvedTickets.filter(t => t.status === TicketStatus.RESOLVED); + + if (relevantTickets.length === 0) return null; + + // Aggregate ticket conversations + const transcripts = relevantTickets.map(t => { + const convo = t.messages.map(m => `${m.role.toUpperCase()}: ${m.content}`).join('\n'); + return `TICKET ID: ${t.id}\nOGGETTO: ${t.subject}\nCONVERSAZIONE:\n${convo}\n---`; + }).join('\n'); + + const existingTitles = existingArticles.map(a => a.title).join(', '); + + const systemPrompt = ` + Sei un Knowledge Manager AI esperto. + Analizza i seguenti ticket risolti e confrontali con gli articoli esistenti nella Knowledge Base. + + ARTICOLI ESISTENTI: ${existingTitles} + + TICKET RISOLTI RECENTI: + ${transcripts} + + OBIETTIVO: + 1. Identifica un problema ricorrente o una soluzione tecnica presente nei ticket risolti MA NON coperta dagli articoli esistenti. + 2. Se trovi una lacuna, scrivi un NUOVO articolo di Knowledge Base per colmarla. + 3. Restituisci il risultato ESCLUSIVAMENTE in formato JSON valido. Non aggiungere markdown code blocks. + + SCHEMA JSON RICHIESTO: + { + "foundGap": boolean, + "title": "Titolo del nuovo articolo", + "content": "Contenuto dettagliato in formato Markdown", + "category": "Categoria suggerita" + } + `; + try { - const ai = new GoogleGenAI({ apiKey }); + let rawText = ""; - // Filter only resolved tickets - const relevantTickets = resolvedTickets.filter(t => t.status === TicketStatus.RESOLVED); + if (provider === AiProvider.OPENROUTER || provider === AiProvider.OPENAI || provider === AiProvider.DEEPSEEK) { + const messages = [{ role: "system", content: systemPrompt }]; + rawText = await callOpenRouter(apiKey, model, messages); + } else { + const ai = new GoogleGenAI({ apiKey }); + const response = await ai.models.generateContent({ + model: model, + contents: systemPrompt, + config: { responseMimeType: "application/json" } + }); + rawText = response.text || ""; + } + + if (!rawText) return null; + + // Clean markdown if present (sometimes models add ```json ... ```) + // We escape the backticks in the regex to ensure safety in all environments + const cleanedText = rawText.replace(/\`\`\`json/g, '').replace(/\`\`\`/g, '').trim(); - if (relevantTickets.length === 0) return null; - - // Aggregate ticket conversations - const transcripts = relevantTickets.map(t => { - const convo = t.messages.map(m => `${m.role.toUpperCase()}: ${m.content}`).join('\n'); - return `TICKET ID: ${t.id}\nOGGETTO: ${t.subject}\nCONVERSAZIONE:\n${convo}\n---`; - }).join('\n'); - - const existingTitles = existingArticles.map(a => a.title).join(', '); - - const prompt = ` - Sei un Knowledge Manager AI esperto. - Analizza i seguenti ticket risolti e confrontali con gli articoli esistenti nella Knowledge Base. - - ARTICOLI ESISTENTI: ${existingTitles} - - TICKET RISOLTI RECENTI: - ${transcripts} - - OBIETTIVO: - 1. Identifica un problema ricorrente o una soluzione tecnica presente nei ticket risolti MA NON coperta dagli articoli esistenti. - 2. Se trovi una lacuna, scrivi un NUOVO articolo di Knowledge Base per colmarla. - 3. Restituisci il risultato ESCLUSIVAMENTE in formato JSON. - - SCHEMA JSON RICHIESTO: - { - "foundGap": boolean, - "title": "Titolo del nuovo articolo", - "content": "Contenuto dettagliato in formato Markdown", - "category": "Categoria suggerita" - } - `; - - const response = await ai.models.generateContent({ - model: 'gemini-3-pro-preview', - contents: prompt, - config: { - responseMimeType: "application/json" - } - }); - - const text = response.text; - if (!text) return null; - - const result = JSON.parse(text); + const result = JSON.parse(cleanedText); if (result.foundGap) { return { title: result.title, @@ -134,6 +184,7 @@ export const generateNewKBArticle = async ( }; } return null; + } catch (error) { console.error("Knowledge Agent Error:", error); return null;