From 67c182c4d0fc68b57194e39d76435fab0bd2c705 Mon Sep 17 00:00:00 2001 From: TurboRx <187360786+TurboRx@users.noreply.github.com> Date: Sun, 21 Jun 2026 06:45:28 +0000 Subject: [PATCH 1/2] feat: add optional local Ollama support with strict provider isolation --- add-ons/ai-ask.js | 87 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/add-ons/ai-ask.js b/add-ons/ai-ask.js index 11d7f603..e24da1e8 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); + } + 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); }; From 3ae357920700a6c8b2f5856afb022b950d3f639d Mon Sep 17 00:00:00 2001 From: TurboRx <187360786+TurboRx@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:58:55 +0000 Subject: [PATCH 2/2] fix: add missing return before callback in askOllama (callback-return lint error) --- add-ons/ai-ask.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/add-ons/ai-ask.js b/add-ons/ai-ask.js index e24da1e8..21753527 100644 --- a/add-ons/ai-ask.js +++ b/add-ons/ai-ask.js @@ -157,7 +157,7 @@ function askOllama(question, context, callback) { if (parsed.response) { return callback(parsed.response.trim(), null); } - callback(null, new Error('Unexpected response format from Ollama')); + return callback(null, new Error('Unexpected response format from Ollama')); } catch (e) { return callback(null, new Error('Failed to parse response: ' + e.message)); }