Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 75 additions & 12 deletions add-ons/ai-ask.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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 => {
Expand Down Expand Up @@ -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) => {
Expand All @@ -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);
Expand All @@ -255,7 +319,6 @@ exports.setup = function (App) {
},
});

// Cleanup function
addon.destroy = function () {
App.bot.removeListener('userchat', chatHandler);
};
Expand Down