diff --git a/add-ons/ai-ask.js b/add-ons/ai-ask.js index 11d7f603..21753527 100644 --- a/add-ons/ai-ask.js +++ b/add-ons/ai-ask.js @@ -1,8 +1,9 @@ -// AI Ask Add-on for Showdown ChatBot +// AI Ask Add-on for Showdown ChatBot (Supports Gemini & Self-Hosted Models of Ollama) // Install as an Add-On // ------------------------------------ -// Make sure to change the following constants: -// - AI_API_KEY: Your Gemini API key (get from https://aistudio.google.com/app/apikey) +// Make sure to configure the following options below: +// - AI_CONFIG: Choose your provider ('gemini' or 'ollama') and adjust connection options. +// - AI_API_KEY: Your Gemini API key (get from https://aistudio.google.com/app/apikey). Only required for Gemini. // - DEFAULT_ALLOWED_GROUP: Group allowed to use the command by default. Can be a symbol or a group name. 'use strict'; @@ -16,17 +17,24 @@ const USER_MEMORY_LIMIT = 5; const MEMORY_EXPIRY_HOURS = 24; const AI_CONFIG = { + provider: 'gemini', gemini: { host: 'generativelanguage.googleapis.com', - pathTemplate: '/v1beta/models/gemini-3.0-flash:generateContent?key=', + pathTemplate: '/v1beta/models/gemini-3.5-flash:generateContent?key=', + }, + ollama: { + host: '127.0.0.1', + port: 11434, + model: 'llama4', + useHttps: false, }, }; const Https = require('https'); +const Http = require('http'); const Text = Tools('text'); const chatHistory = Object.create(null); - const userMemory = Object.create(null); function httpsPost(options, data, callback) { @@ -105,9 +113,68 @@ function askGemini(question, context, callback) { }); } +function askOllama(question, context, callback) { + const config = AI_CONFIG.ollama; + const client = config.useHttps ? Https : Http; + + let promptText = 'You\'re a friendly, laid-back AI hanging out in a Pokemon Showdown chat room. Talk like a real person would — casual, warm, and genuine. Use contractions, keep things conversational, and don\'t be stiff or formal. If something\'s funny, be funny. If someone\'s struggling, be real and empathetic with them. You\'ve got personality, you\'re curious, and you actually care about what people are asking. Keep your replies short and snappy — nobody wants to read an essay in a chatroom. Aim to stay under ' + MAX_MESSAGE_LENGTH + ' characters. Don\'t sound like you\'re reading off a script or a FAQ page — just have a real conversation. If you don\'t know the answer or aren\'t sure about something, say so casually — like "idk man", "no idea tbh", "honestly not sure lol" — never say anything stiff like "I don\'t have information about that in my training data" or "I\'m unable to assist with that".'; + + if (context) { + promptText += '\n\nRecent chat context (for reference only):\n' + context; + } + + promptText += '\n\nUser: ' + question; + + const data = { + model: config.model, + prompt: promptText, + stream: false, + }; + + const postData = JSON.stringify(data); + const options = { + hostname: config.host, + port: config.port, + path: '/api/generate', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData), + }, + }; + + const req = client.request(options, res => { + let responseData = ''; + res.on('data', chunk => { + responseData += chunk; + }); + res.on('end', () => { + try { + const parsed = JSON.parse(responseData); + if (parsed.error) { + return callback(null, new Error(parsed.error)); + } + if (parsed.response) { + return callback(parsed.response.trim(), null); + } + return callback(null, new Error('Unexpected response format from Ollama')); + } catch (e) { + return callback(null, new Error('Failed to parse response: ' + e.message)); + } + }); + }); + + req.on('error', err => { + return callback(null, err); + }); + + req.write(postData); + req.end(); +} + function askAI(question, context, callback) { - if (!AI_API_KEY) { - return callback(null, new Error('AI API key is not configured')); + if (AI_CONFIG.provider === 'ollama') { + return askOllama(question, context, callback); } return askGemini(question, context, callback); } @@ -129,7 +196,6 @@ function getUserMemory(userId, room) { return []; } - // Clean up expired memories const now = Date.now(); const expiryTime = MEMORY_EXPIRY_HOURS * 60 * 60 * 1000; userMemory[key] = userMemory[key].filter(mem => { @@ -221,14 +287,13 @@ exports.setup = function (App) { return this.errorReply('Your question is too long. Please keep it under 500 characters.'); } - if (!AI_API_KEY) { + if (!AI_API_KEY && AI_CONFIG.provider !== 'ollama') { return this.errorReply('AI API key is not configured. Please configure the add-on before use.'); } const room = this.room; const userId = Text.parseUserIdent(this.by).id; - // Build enhanced context with chat history and user memory const context = buildEnhancedContext(room, userId); askAI(question, context, (response, error) => { @@ -240,7 +305,6 @@ exports.setup = function (App) { const aiResponse = truncateResponse(response, MAX_MESSAGE_LENGTH); - // Store in user memory for future reference addUserMemory(userId, room, question, aiResponse); const finalResponse = Text.stripCommands(aiResponse); @@ -255,7 +319,6 @@ exports.setup = function (App) { }, }); - // Cleanup function addon.destroy = function () { App.bot.removeListener('userchat', chatHandler); };