Fixed LLM bot support

This commit is contained in:
cameron 2026-03-01 09:59:26 -05:00
parent 792f12b925
commit e1d059d72c
3 changed files with 50 additions and 30 deletions

View File

@ -1,17 +1,19 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { LLM } = require('@themaximalist/llm.js'); // or wherever your LLM package lives const OpenAI = require('openai');
// read optional config file // read optional config file
const configPath = path.join(__dirname, '..', 'llm_config.json'); const configPath = path.join(__dirname, 'llm_config.json');
const llmOptions = fs.existsSync(configPath) const llmOptions = fs.existsSync(configPath)
? JSON.parse(fs.readFileSync(configPath, 'utf8')) ? JSON.parse(fs.readFileSync(configPath, 'utf8'))
: {}; : {};
console.log(`Read config from ${configPath}`);
class LLMBot { class LLMBot {
constructor() { constructor() {
// One instance keeps the whole conversation in memory // Instantiate the OpenAI client with the config options we just read
this.llm = new LLM(llmOptions); this.llm = new OpenAI(llmOptions);
// Prompt fragments well reuse // Prompt fragments well reuse
this.writePrompt = this.writePrompt =
@ -31,23 +33,39 @@ Keep it short, fun, and on-topic.`;
async Write(story_so_far) { async Write(story_so_far) {
const msg = `${this.writePrompt}\n\nStory so far:\n${story_so_far}`; const msg = `${this.writePrompt}\n\nStory so far:\n${story_so_far}`;
const llm_response = await this.llm.chat(msg); const response = await this.llm.chat.completions.create({
console.log(`Received response from LLM: ${llm_response}`) model: llmOptions.model || 'meta-llama/Llama-3.2-3B-Instruct-Turbo',
return llm_response.trim(); messages: [{ role: 'user', content: msg }],
temperature: llmOptions.temperature ?? 0.8,
max_tokens: llmOptions.max_tokens ?? 80,
});
console.log(`Received response from LLM: ${response.choices[0].message.content}`);
return response.choices[0].message.content.trim();
} }
async Vote(story_so_far, choices) { async Vote(story_so_far, choices) {
const choicesBlock = choices.map((c, i) => `${i}: ${c}`).join('\n'); const choicesBlock = choices.map((c, i) => `${i}: ${c}`).join('\n');
const msg = `${this.votePrompt}\n\nStory:\n${story_so_far}\n\nChoices:\n${choicesBlock}`; const msg = `${this.votePrompt}\n\nStory:\n${story_so_far}\n\nChoices:\n${choicesBlock}`;
const reply = (await this.llm.chat(msg)).trim(); const response = await this.llm.chat.completions.create({
// extract first digit found, fallback to 0 model: llmOptions.model || 'meta-llama/Llama-3.2-3B-Instruct-Turbo',
messages: [{ role: 'user', content: msg }],
temperature: 0.2,
max_tokens: 5,
});
const reply = response.choices[0].message.content.trim();
const match = reply.match(/\d+/); const match = reply.match(/\d+/);
return match ? parseInt(match[0], 10) : 0; return match ? parseInt(match[0], 10) : 0;
} }
async Banter(chat_so_far) { async Banter(chat_so_far) {
const msg = `${this.banterPrompt}\n\nChat so far:\n${chat_so_far}`; const msg = `${this.banterPrompt}\n\nChat so far:\n${chat_so_far}`;
return (await this.llm.chat(msg)).trim(); const response = await this.llm.chat.completions.create({
model: llmOptions.model || 'meta-llama/Llama-3.2-3B-Instruct-Turbo',
messages: [{ role: 'user', content: msg }],
temperature: llmOptions.temperature ?? 0.8,
max_tokens: llmOptions.max_tokens ?? 60,
});
return response.choices[0].message.content.trim();
} }
} }

View File

@ -15,6 +15,7 @@
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"express": "^5.2.1", "express": "^5.2.1",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"openai": "^6.25.0",
"socket.io": "^4.8.3", "socket.io": "^4.8.3",
"uuid": "^13.0.0" "uuid": "^13.0.0"
} }

View File

@ -117,7 +117,7 @@ class YarnGame {
console.log("Creating bots...") console.log("Creating bots...")
for (let i = 0; i < botCount; i++) { for (let i = 0; i < botCount; i++) {
const bot_id = randomUUID() const bot_id = randomUUID()
const bot = BotFactory.createBot("random"); const bot = BotFactory.createBot("llm");
this.bots.push({ bot_id, bot }); this.bots.push({ bot_id, bot });
const bot_player = { const bot_player = {
player_id: bot_id, player_id: bot_id,
@ -130,23 +130,24 @@ class YarnGame {
} }
} }
startGame(settings) { async startGame(settings) {
this.gameSettings = { ...this.gameSettings, ...settings }; this.gameSettings = { ...this.gameSettings, ...settings };
this.startBots(this.gameSettings.botCount); this.startBots(this.gameSettings.botCount);
this.inProgress = true; this.inProgress = true;
this.yarnStory = [{ player: "Narrator", str: this.gameSettings.startText }]; this.yarnStory = [{ player: "Narrator", str: this.gameSettings.startText }];
this.players.forEach(p => p.score = 0); this.players.forEach(p => p.score = 0);
this.startWritingPhase(); await this.startWritingPhase();
} }
processBotWriting() { async processBotWriting() {
for (const botObj of this.bots) { for (const botObj of this.bots) {
const botText = botObj.bot.Write(this.getStoryText()); const botText = await botObj.bot.Write(this.getStoryText());
console.log(`Bot text: ${botText}`)
this.submitEntry(botObj.bot_id, botText) this.submitEntry(botObj.bot_id, botText)
} }
} }
startWritingPhase() { async startWritingPhase() {
this.currentPhase = 'writing'; this.currentPhase = 'writing';
this.round_data = []; this.round_data = [];
this.submittedPlayers = new Set(); this.submittedPlayers = new Set();
@ -176,18 +177,18 @@ class YarnGame {
return this.submittedPlayers.size >= this.players.length; return this.submittedPlayers.size >= this.players.length;
} }
processBotVoting() { async processBotVoting() {
for (const botObj of this.bots) { for (const botObj of this.bots) {
const botVote = botObj.bot.Vote(this.getStoryText(), this.round_data); const botVote = await botObj.bot.Vote(this.getStoryText(), this.round_data);
this.submitVote(botObj.bot_id, botVote) this.submitVote(botObj.bot_id, botVote)
} }
} }
startVotingPhase() { async startVotingPhase() {
this.currentPhase = 'voting'; this.currentPhase = 'voting';
this.votedPlayers = new Set(); this.votedPlayers = new Set();
this.timeRemaining = 30; // 30 seconds for voting this.timeRemaining = 30; // 30 seconds for voting
this.processBotVoting() await this.processBotVoting()
} }
submitVote(playerId, entryIndex) { submitVote(playerId, entryIndex) {
@ -390,7 +391,7 @@ io.on('connection', (socket) => {
io.to(currentRoom).emit('settingsUpdate', game.gameSettings); io.to(currentRoom).emit('settingsUpdate', game.gameSettings);
}); });
socket.on('startGame', (settings) => { socket.on('startGame', async (settings) => {
if (!currentRoom || !playerId) return; if (!currentRoom || !playerId) return;
const game = games[currentRoom]; const game = games[currentRoom];
@ -401,13 +402,13 @@ io.on('connection', (socket) => {
} }
console.log(`Starting new game with settings ${JSON.stringify(settings)}`) console.log(`Starting new game with settings ${JSON.stringify(settings)}`)
game.startGame(settings); await game.startGame(settings);
io.to(currentRoom).emit('gameStarted', game.getState()); io.to(currentRoom).emit('gameStarted', game.getState());
startTimer(game); await startTimer(game);
}); });
socket.on('submitEntry', (data) => { socket.on('submitEntry', async (data) => {
if (!currentRoom || !playerId) return; if (!currentRoom || !playerId) return;
const game = games[currentRoom]; const game = games[currentRoom];
@ -420,7 +421,7 @@ io.on('connection', (socket) => {
if (game.allPlayersSubmitted()) { if (game.allPlayersSubmitted()) {
clearInterval(game.timer); clearInterval(game.timer);
game.startVotingPhase(); await game.startVotingPhase();
io.to(currentRoom).emit('votingPhase', { io.to(currentRoom).emit('votingPhase', {
entries: game.round_data.map((e, i) => ({ entries: game.round_data.map((e, i) => ({
index: i, index: i,
@ -495,10 +496,10 @@ io.on('connection', (socket) => {
}); });
}); });
function startTimer(game) { async function startTimer(game) {
if (game.timer) clearInterval(game.timer); if (game.timer) clearInterval(game.timer);
game.timer = setInterval(() => { game.timer = setInterval(async () => {
game.timeRemaining--; game.timeRemaining--;
io.to(game.roomName).emit('timerUpdate', game.timeRemaining); io.to(game.roomName).emit('timerUpdate', game.timeRemaining);
@ -512,7 +513,7 @@ function startTimer(game) {
} }
} }
game.startVotingPhase(); await game.startVotingPhase();
io.to(game.roomName).emit('votingPhase', { io.to(game.roomName).emit('votingPhase', {
entries: game.round_data.map((e, i) => ({ entries: game.round_data.map((e, i) => ({
index: i, index: i,
@ -558,10 +559,10 @@ function endVotingPhase(game) {
}); });
} else { } else {
// Start next round after a short delay // Start next round after a short delay
setTimeout(() => { setTimeout(async () => {
game.startWritingPhase(); game.startWritingPhase();
io.to(game.roomName).emit('newRound', game.getState()); io.to(game.roomName).emit('newRound', game.getState());
startTimer(game); await startTimer(game);
}, 5000); }, 5000);
} }
} }