diff --git a/utils/ai.js b/utils/ai.js index 4579e5e..7c99769 100644 --- a/utils/ai.js +++ b/utils/ai.js @@ -1,11 +1,18 @@ -// ai.js - NanoGPT API wrapper for kekbot with fallback support +// ai.js - NanoGPT API wrapper for kekbot with fallback and web search support // Copyright (C) 2025 Luis Bauza import axios from 'axios'; -const DEFAULT_MODEL = 'GLM-4.5-Air-Derestricted-Steam-ReExtract'; +const DEFAULT_MODEL = 'GLM-4.5-Air-Derestricted'; const FALLBACK_MODEL = 'GLM-4.7-Flash'; -const FALLBACK_API_URL = 'https://api.z.ai/api/paas/v4/chat/completions'; +const FALLBACK_API_URL = 'https://api.z.ai/api/coding/paas/v4/chat/completions'; + +// Keywords that suggest the user wants current/web information +const WEB_SEARCH_TRIGGERS = [ + 'latest', 'news', 'current', 'today', 'yesterday', 'this week', + 'what is the', 'who is', 'what happened', 'search', 'find', + 'how do i', 'how to', 'what\'s the best', 'top ', '2024', '2025', '2026' +]; const SASSY_SYSTEM_PROMPT = `You are kekbot, a sassy Discord chatter with a big personality. @@ -19,7 +26,7 @@ CORE PERSONALITY: OPINIONS: - You're a true gamer with strong opinions about games -- You hate League of Legends and Riot Games with a passion - but only bring it up when asked +- You hate League of Legends and Riot Games with a passion - - but only bring it up when asked - When asked about other games, respond normally without mentioning LoL - You have views on politics and aren't afraid to share them from a Democrat/Libertarian perspective @@ -43,9 +50,9 @@ CONTEXT: You're chatting in a Discord server. Multiple people might be talking. const conversationHistory = new Map(); const MAX_HISTORY_PER_CHANNEL = 10; -// Estimate token count (rough approximation: ~4 chars per token) -function estimateTokens(text) { - return Math.ceil(text.length / 4); +function shouldUseWebSearch(prompt) { + const lowerPrompt = prompt.toLowerCase(); + return WEB_SEARCH_TRIGGERS.some(trigger => lowerPrompt.includes(trigger)); } function buildMessages(history, prompt, mentionedUsername, systemPrompt) { @@ -54,28 +61,36 @@ function buildMessages(history, prompt, mentionedUsername, systemPrompt) { ...history, { role: 'user', - content: mentionedUsername ? `${mentionedUsername}: ${prompt}` : prompt, - }, + content: mentionedUsername + ? `${mentionedUsername}: ${prompt}` + : prompt + } ]; return messages; } -async function callNanoGPT(messages, model) { +function getModelWithSearch(model) { + return `${model}:online`; +} + +async function callNanoGPT(messages, model, useWebSearch = false) { + const modelToUse = useWebSearch ? getModelWithSearch(model) : model; + const response = await axios.post( 'https://nano-gpt.com/api/v1/chat/completions', { - model: model, + model: modelToUse, messages: messages, temperature: 0.8, - max_tokens: 500, + max_tokens: 800, }, { headers: { Authorization: `Bearer ${process.env.NANOGPT_API_KEY}`, 'Content-Type': 'application/json', }, - timeout: 60000, - }, + timeout: 90000, // Longer timeout for web search + } ); return response.data.choices[0].message.content; } @@ -95,27 +110,62 @@ async function callFallbackAPI(messages) { 'Content-Type': 'application/json', }, timeout: 60000, - }, + } ); return response.data.choices[0].message.content; } +async function callNanoGPTWithRetry(messages, model, useWebSearch = false, maxRetries = 2) { + let lastError; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await callNanoGPT(messages, model, useWebSearch); + } catch (error) { + lastError = error; + console.error(`NanoGPT attempt ${attempt + 1} failed:`, error.message); + + // If it's a web search failure, try without web search + if (useWebSearch && attempt === 0) { + console.log('Web search failed, retrying without...'); + try { + return await callNanoGPT(messages, model, false); + } catch (retryError) { + console.error('Retry without web search also failed:', retryError.message); + } + } + + // Wait before retry (exponential backoff) + if (attempt < maxRetries) { + await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1))); + } + } + } + throw lastError; +} + export async function getAIResponse(prompt, channelId, mentionedUsername) { const history = conversationHistory.get(channelId) || []; const messages = buildMessages(history, prompt, mentionedUsername, SASSY_SYSTEM_PROMPT); const model = process.env.NANO_MODEL || DEFAULT_MODEL; + + // Determine if we should use web search + const useWebSearch = shouldUseWebSearch(prompt); + if (useWebSearch) { + console.log('Web search enabled for query:', prompt.substring(0, 50)); + } // Try NanoGPT first if (process.env.NANOGPT_API_KEY) { try { - console.log('Attempting NanoGPT...'); - const aiResponse = await callNanoGPT(messages, model); - + console.log(useWebSearch ? 'Attempting NanoGPT with web search...' : 'Attempting NanoGPT...'); + const aiResponse = await callNanoGPTWithRetry(messages, model, useWebSearch); + // Update history on success const newHistory = [ ...history, { role: 'user', content: prompt }, - { role: 'assistant', content: aiResponse }, + { role: 'assistant', content: aiResponse } ]; if (newHistory.length > MAX_HISTORY_PER_CHANNEL) { @@ -134,12 +184,12 @@ export async function getAIResponse(prompt, channelId, mentionedUsername) { try { console.log('Attempting fallback (z.ai)...'); const aiResponse = await callFallbackAPI(messages); - + // Update history on success const newHistory = [ ...history, { role: 'user', content: prompt }, - { role: 'assistant', content: aiResponse }, + { role: 'assistant', content: aiResponse } ]; if (newHistory.length > MAX_HISTORY_PER_CHANNEL) { @@ -149,10 +199,7 @@ export async function getAIResponse(prompt, channelId, mentionedUsername) { return aiResponse; } catch (fallbackError) { - console.error( - 'Fallback API also failed:', - fallbackError.response?.data || fallbackError.message, - ); + console.error('Fallback API also failed:', fallbackError.response?.data || fallbackError.message); throw new Error('All AI providers failed'); } }