diff --git a/utils/ai.js b/utils/ai.js index 7c99769..725b4c2 100644 --- a/utils/ai.js +++ b/utils/ai.js @@ -1,4 +1,4 @@ -// ai.js - NanoGPT API wrapper for kekbot with fallback and web search support +// ai.js - NanoGPT API wrapper for kekbot with fallback and web search tools // Copyright (C) 2025 Luis Bauza import axios from 'axios'; @@ -7,12 +7,24 @@ const DEFAULT_MODEL = 'GLM-4.5-Air-Derestricted'; const FALLBACK_MODEL = 'GLM-4.7-Flash'; 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' -]; +// Tool definitions for function calling +const WEB_SEARCH_TOOL = { + type: 'function', + function: { + name: 'web_search', + description: 'Search the web for current information. Use this when you need up-to-date facts, news, or information that may have changed recently.', + parameters: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'The search query to find relevant web results.' + } + }, + required: ['query'] + } + } +}; const SASSY_SYSTEM_PROMPT = `You are kekbot, a sassy Discord chatter with a big personality. @@ -26,7 +38,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,6 +55,7 @@ RESPONSE RULES: - Don't use markdown formatting excessively - plain text with personality - You can disagree with the user, you're not a yes-man - Stay in character always +- Use the web_search tool when you need current information CONTEXT: You're chatting in a Discord server. Multiple people might be talking. Pay attention to who you're responding to.`; @@ -50,11 +63,6 @@ CONTEXT: You're chatting in a Discord server. Multiple people might be talking. const conversationHistory = new Map(); const MAX_HISTORY_PER_CHANNEL = 10; -function shouldUseWebSearch(prompt) { - const lowerPrompt = prompt.toLowerCase(); - return WEB_SEARCH_TRIGGERS.some(trigger => lowerPrompt.includes(trigger)); -} - function buildMessages(history, prompt, mentionedUsername, systemPrompt) { const messages = [ { role: 'system', content: systemPrompt }, @@ -69,30 +77,132 @@ function buildMessages(history, prompt, mentionedUsername, systemPrompt) { return messages; } -function getModelWithSearch(model) { - return `${model}:online`; -} - -async function callNanoGPT(messages, model, useWebSearch = false) { - const modelToUse = useWebSearch ? getModelWithSearch(model) : model; - +async function performWebSearch(query) { const response = await axios.post( - 'https://nano-gpt.com/api/v1/chat/completions', + 'https://nano-gpt.com/api/web', { - model: modelToUse, - messages: messages, - temperature: 0.8, - max_tokens: 800, + query: query, + provider: 'linkup', + depth: 'standard', + outputType: 'sourcedAnswer' }, { headers: { Authorization: `Bearer ${process.env.NANOGPT_API_KEY}`, 'Content-Type': 'application/json', }, - timeout: 90000, // Longer timeout for web search + timeout: 60000, } ); - return response.data.choices[0].message.content; + + const data = response.data.data; + const metadata = response.data.metadata; + + // Format the response nicely + let formattedResult = ''; + + if (typeof data === 'string') { + formattedResult = data; + } else if (data.answer) { + formattedResult = data.answer; + if (data.sources && data.sources.length > 0) { + formattedResult += '\n\nSources:\n'; + data.sources.forEach((source, i) => { + formattedResult += `${i + 1}. ${source.name || source.url}\n`; + }); + } + } else if (data.results) { + formattedResult = 'Search results:\n'; + data.results.slice(0, 5).forEach((result, i) => { + formattedResult += `${i + 1}. ${result.title || result.name}\n`; + if (result.url) formattedResult += ` ${result.url}\n`; + if (result.content) formattedResult += ` ${result.content.substring(0, 200)}...\n`; + }); + } + + return formattedResult; +} + +async function callNanoGPTWithTools(messages, model, maxRetries = 2) { + let lastError; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + const response = await axios.post( + 'https://nano-gpt.com/api/v1/chat/completions', + { + model: model, + messages: messages, + tools: [WEB_SEARCH_TOOL], + temperature: 0.8, + max_tokens: 800, + }, + { + headers: { + Authorization: `Bearer ${process.env.NANOGPT_API_KEY}`, + 'Content-Type': 'application/json', + }, + timeout: 90000, + } + ); + + const choice = response.data.choices[0]; + const message = choice.message; + + // Check if AI wants to call a tool + if (message.tool_calls && message.tool_calls.length > 0) { + for (const toolCall of message.tool_calls) { + if (toolCall.function.name === 'web_search') { + const args = JSON.parse(toolCall.function.arguments); + console.log('AI requested web search for:', args.query); + + // Perform the search + const searchResult = await performWebSearch(args.query); + + // Add tool result to messages and continue + messages.push(message); + messages.push({ + role: 'tool', + tool_call_id: toolCall.id, + content: searchResult + }); + + // Get final response with search results + const finalResponse = await axios.post( + 'https://nano-gpt.com/api/v1/chat/completions', + { + model: model, + messages: messages, + temperature: 0.8, + max_tokens: 800, + }, + { + headers: { + Authorization: `Bearer ${process.env.NANOGPT_API_KEY}`, + 'Content-Type': 'application/json', + }, + timeout: 90000, + } + ); + + return finalResponse.data.choices[0].message.content; + } + } + } + + // No tool calls, return direct response + return message.content; + + } catch (error) { + lastError = error; + console.error(`NanoGPT attempt ${attempt + 1} failed:`, error.message); + + if (attempt < maxRetries) { + await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1))); + } + } + } + throw lastError; } async function callFallbackAPI(messages) { @@ -115,51 +225,16 @@ async function callFallbackAPI(messages) { 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 + // Try NanoGPT first (with tool calling) if (process.env.NANOGPT_API_KEY) { try { - console.log(useWebSearch ? 'Attempting NanoGPT with web search...' : 'Attempting NanoGPT...'); - const aiResponse = await callNanoGPTWithRetry(messages, model, useWebSearch); + console.log('Attempting NanoGPT with tool calling...'); + const aiResponse = await callNanoGPTWithTools(messages, model); // Update history on success const newHistory = [