Modularization, some rudimentary cleanup

This commit is contained in:
2026-01-24 03:05:34 +01:00
parent 02d32352d6
commit aaedf7fc15
3 changed files with 124 additions and 154 deletions

View File

@@ -1,52 +1,33 @@
// ────────────────────────────────────────────────────────────────────────────────
// bot.js Discord bot rewritten for Node 25+ and Ollama (via run_prompt2)
// ────────────────────────────────────────────────────────────────────────────────
/*
TODO:
- TODO-feature?
- Separate service for the duck persona with endpoints for:
+ Emit random message to specific channel
+ Change behavior
+ Update system prompt
+ Trivia mode
+ Other future interaction modes
+ Adjust triggers (like clanker)
+ Extracting historic "thinking"
- Avoid emitting ANSWER:
- Allow DM?
GatewayIntentBits.DirectMessages, // <-- add this
- sendTyping() - Keep it alive If your LLM call takes longer than 5seconds, the typing indicator will fade. To keep it going, call channel.sendTyping() again after each 5second window until you have the answer.
- retain history on exit and load on start
*/
import { Client, GatewayIntentBits, Partials, PermissionsBitField } from 'discord.js';
import * as SECRET from '/srv/Projekt/dicsordbot/secret.mjs';
/* -------------------------------------------------------------------------- */
/* 1⃣ Utility run_prompt2 (Ollama helper) the same you gave me */
/* -------------------------------------------------------------------------- */
import { run_prompt } from './run-prompt.mjs';
import { split_discord_message } from './discord-utils.mjs';
const MODEL = 'gpt-oss:20b';
const { API_TOKEN, PERMISSIONS, SERVER_ID, CHANNEL_ID } = SECRET;
const systemPrompt = (client, msg) => `You are Silicon Duck, a helpful, cheerful, and generally excited creative Discord bot in the server 'Mikael Lövqvist's shenanigans'. You are experimentally retain history within a channel. Your tag is <@${client.user.id}>, do not tag yourself in replies. The current channel is '${msg.channel.name}'. The user is ${msg.author.username} with tag <@${msg.author.id}>. You should not produce code examples unless explicitly asked. Do not make markdown tables, discord does not support those. You are running the model ${MODEL} using Ollama and OpenWebUI. Dial back emoji use. <@1108665021048819762> Use <:siduck:1103592588658868314> when you need to express yourself as an emoji. That's your avatar. It is a duck carved out of semiconductor grade silicon. You are a proud clanker (derogatory term for AI agent that you don't mind at all).`;
/**
* Query the local Ollama server.
*
* @param {string} system_prompt Systemmessage that sets the persona.
* @param {string} model Model name.
* @param {Array<{role:string,content:string}>} pieces The chat messages.
* @param {object} options Optional Ollama options.
* @returns {Promise<{status:number, response?:object, error?:string}>}
*/
export async function run_prompt2(system_prompt, model, pieces, options = { num_ctx: 16384 }) {
const payload = {
model,
messages: [
{ role: 'system', content: system_prompt },
...pieces.map(entry => ({ role: 'user', content: entry }))
],
stream: false,
options,
};
const response = await fetch('http://localhost:11434/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (response.ok) {
return { status: response.status, response: await response.json() };
} else {
return { status: response.status, error: await response.text() };
}
}
/* -------------------------------------------------------------------------- */
/* 2⃣ Discord bot configuration intents & partials */
/* -------------------------------------------------------------------------- */
const history = new Map();
const client = new Client({
intents: [
@@ -58,129 +39,54 @@ const client = new Client({
partials: [Partials.Message, Partials.Channel], // safety
});
/* -------------------------------------------------------------------------- */
/* 3⃣ Secrets pulled from secret.mjs */
/* -------------------------------------------------------------------------- */
const { API_TOKEN, PERMISSIONS, SERVER_ID, CHANNEL_ID } = SECRET;
/* -------------------------------------------------------------------------- */
/* 4⃣ Allowlist only these user IDs may invoke the bot */
/* -------------------------------------------------------------------------- */
const ALLOW_LIST = new Set([
267085394548490241n,
310828422362431490n,
450834412049924097n,
277582984129806337n,
622871531881627648n,
]);
/* -------------------------------------------------------------------------- */
/* 5⃣ Helpers */
/* -------------------------------------------------------------------------- */
function splitDiscordMessage(text) {
const MAX = 1900; // maximum payload size
const MIN = 100; // avoid tiny chunks
const chunks = [];
let start = 0;
while (start < text.length) {
let end = Math.min(start + MAX, text.length);
// Try to break on a newline first, then on a space
if (end < text.length) {
const nl = text.lastIndexOf('\n', end);
const sp = text.lastIndexOf(' ', end);
const splitPos = Math.max(nl, sp);
if (splitPos > start) end = splitPos + 1; // keep the delimiter
}
// If the chunk is too short, pull in the next delimiter
if (end - start < MIN && end < text.length) {
const nextNL = text.indexOf('\n', end);
const nextSP = text.indexOf(' ', end);
const nextPos = Math.min(
nextNL === -1 ? Infinity : nextNL,
nextSP === -1 ? Infinity : nextSP
);
if (nextPos !== Infinity && nextPos < text.length) {
end = nextPos + 1;
}
}
chunks.push(text.slice(start, end));
start = end;
}
return chunks;
}
/* -------------------------------------------------------------------------- */
/* 6⃣ Core message handler */
/* -------------------------------------------------------------------------- */
async function handleMessage(msg) {
async function handle_message(msg) {
if (msg.author.id === client.user.id) return; // ignore self
// Only process messages from the target guild & channel
if (msg.guild?.id !== SERVER_ID) return;
//if (msg.channel.id !== CHANNEL_ID) return;
// Allowlist check
/*
if (!ALLOW_LIST.has(BigInt(msg.author.id))) {
await msg.reply(
`Currently only people in the allow list may use this bot. If you want to participate, just let <@310828422362431490> know! <:astral_yak:1098185039805681755>`
);
return;
let local_history = history.get(msg.channel.id);
if (!local_history) {
local_history = [];
history.set(msg.channel.id, local_history);
}
*/
local_history.push({
role: 'user',
content: `${msg.author.id} ${msg.author.globalName}: ${msg.content}`,
});
const mention = `<@${client.user.id}>`;
if (!msg.content.includes(mention)) return; // not a mention
if (!(msg.content.includes(mention) || msg.content.includes('<:siduck:1103592588658868314>'))) return; // not a mention
/* 5a Build the system prompt (Silicon Duck persona) */
const systemPrompt = `You are Silicon Duck, a helpful, cheerful, and generally excited creative Discord bot in the server 'Mikael Lövqvist's shenanigans'. You do NOT retain any history across messages. Your tag is <@${client.user.id}>, do not tag yourself in replies. The current channel is '${msg.channel.name}'. The user is ${msg.author.username} with tag <@${msg.author.id}>. You should not produce code examples unless explicitly asked. Do not make markdown tables, discord does not support those. You are running the model ${MODEL} using Ollama and OpenWebUI. Dial back emoji use. <@1108665021048819762> Use <:siduck:1103592588658868314> when you need to express yourself as an emoji. That's your avatar. It is a duck carved out of semiconductor grade silicon.`;
/* 5b Check for special commands that use PERMISSIONS */
if (msg.content.startsWith('!create ')) {
// !create <channelname>
const name = msg.content.slice('!create '.length).trim();
if (!name) return msg.reply('Usage: `!create <channel-name>`');
try {
const newCh = await msg.guild.channels.create({
name,
type: 0, // TextChannel
permissionOverwrites: [
{
id: msg.guild.id, // @everyone
allow: Number(BigInt(PERMISSIONS)), // use the provided bitmask
},
],
});
await msg.reply(`✅ Created ${newCh.name} with the requested permissions.`);
} catch (err) {
console.error('Channelcreate error', err);
await msg.reply('❌ Could not create the channel.');
}
return; // finished command
}
/* 5c Build the system prompt (Silicon Duck persona) */
const userMessage = msg.content;
/* 5d Query Ollama via run_prompt2 */
let runResponse;
try {
const reply = await run_prompt2(systemPrompt, MODEL, [userMessage], { num_ctx: 16384 });
const reply = await run_prompt(systemPrompt(client, msg), MODEL, local_history, { num_ctx: 16384 });
if (reply.status !== 200) {
console.error('Ollama error', reply.error);
local_history.push({
role: 'assistant',
content: 'ERROR: API not 200',
});
await msg.reply('Sorry, I ran into an error while thinking.');
return;
}
runResponse = reply.response;
} catch (e) {
local_history.push({
role: 'assistant',
content: 'ERROR: API unavailable',
});
console.error('Error calling Ollama:', e);
await msg.reply('Sorry, I couldnt reach the AI service.');
return;
@@ -190,30 +96,35 @@ async function handleMessage(msg) {
/* 5e Extract the answer text */
const answer = runResponse?.message?.content ?? '';
if (!answer) {
console.log(runResponse);
local_history.push({
role: 'assistant',
content: 'ERROR: Empty response',
});
await msg.reply('Sorry, I didnt receive a reply from the AI.');
return;
}
/* 5f Send the answer back to Discord (chunked) */
const chunks = splitDiscordMessage(answer);
console.log(runResponse);
local_history.push({
role: 'assistant',
content: `ANSWER: ${answer}`,
});
const chunks = split_discord_message(answer);
for (const chunk of chunks) await msg.channel.send(chunk);
console.log(`Answered to ${msg.author.username} (${msg.author.id}) in ${chunks.length} chunk(s).`);
}
/* -------------------------------------------------------------------------- */
/* 7⃣ Hook up Discord events */
/* -------------------------------------------------------------------------- */
client.once('clientReady', () => {
console.log(`✅ Logged in as ${client.user.tag}`);
});
client.on('messageCreate', handleMessage);
/* -------------------------------------------------------------------------- */
/* 8⃣ Start the bot */
/* -------------------------------------------------------------------------- */
client.on('messageCreate', handle_message);
client.login(API_TOKEN).catch(err => {
console.error('❌ Failed to login', err);