Update geminiService.ts

This commit is contained in:
fcarraUniSa
2026-02-17 10:08:11 +01:00
committed by GitHub
parent 83b0a6bcbd
commit e7e1837fae

View File

@@ -1,5 +1,30 @@
import { GoogleGenAI } from "@google/genai"; 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 * Agent 1: Customer Support Chat
@@ -9,59 +34,74 @@ export const getSupportResponse = async (
apiKey: string, apiKey: string,
userQuery: string, userQuery: string,
chatHistory: string[], chatHistory: string[],
knowledgeBase: KBArticle[] knowledgeBase: KBArticle[],
provider: AiProvider = AiProvider.GEMINI,
model: string = 'gemini-3-flash-preview'
): Promise<string> => { ): Promise<string> => {
if (!apiKey) { if (!apiKey) {
return "L'assistente AI non è configurato (API Key mancante). Contatta l'amministratore."; 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 { try {
const ai = new GoogleGenAI({ apiKey }); if (provider === AiProvider.OPENROUTER || provider === AiProvider.OPENAI || provider === AiProvider.DEEPSEEK) {
// Logic for OpenRouter/OpenAI compatible APIs
// Prepare Context from KB const messages = [
const kbContext = knowledgeBase.map(a => { { role: "system", content: systemInstructionText },
if (a.type === 'url') { ...chatHistory.map(msg => ({ role: "user", content: msg })),
return `Fonte Esterna [${a.category}]: ${a.title} - URL: ${a.url}\nContenuto Estratto: ${a.content}`; { role: "user", content: userQuery }
} ];
return `Articolo [${a.category}]: ${a.title}\nContenuto: ${a.content}`;
}).join('\n\n');
const systemInstruction = `
Sei "OmniSupport AI", un assistente clienti virtuale globale.
IL TUO COMPITO: const response = await callOpenRouter(apiKey, model, messages);
Rispondere alle domande dei clienti basandoti ESCLUSIVAMENTE sulla seguente Base di Conoscenza (KB) fornita in ITALIANO. return response || "Mi dispiace, non riesco a generare una risposta al momento.";
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): } else {
${kbContext} // 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) { } catch (error) {
console.error("Gemini Error:", error); console.error("AI Service Error:", error);
return "Si è verificato un errore nel servizio AI (Verifica API Key o connessione)."; 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 ( export const generateNewKBArticle = async (
apiKey: string, apiKey: string,
resolvedTickets: Ticket[], resolvedTickets: Ticket[],
existingArticles: KBArticle[] existingArticles: KBArticle[],
provider: AiProvider = AiProvider.GEMINI,
model: string = 'gemini-3-pro-preview'
): Promise<{ title: string; content: string; category: string } | null> => { ): Promise<{ title: string; content: string; category: string } | null> => {
if (!apiKey) return 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 { try {
const ai = new GoogleGenAI({ apiKey }); let rawText = "";
// Filter only resolved tickets if (provider === AiProvider.OPENROUTER || provider === AiProvider.OPENAI || provider === AiProvider.DEEPSEEK) {
const relevantTickets = resolvedTickets.filter(t => t.status === TicketStatus.RESOLVED); 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; const result = JSON.parse(cleanedText);
// 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);
if (result.foundGap) { if (result.foundGap) {
return { return {
title: result.title, title: result.title,
@@ -134,6 +184,7 @@ export const generateNewKBArticle = async (
}; };
} }
return null; return null;
} catch (error) { } catch (error) {
console.error("Knowledge Agent Error:", error); console.error("Knowledge Agent Error:", error);
return null; return null;