Skip to content

A bunch of AI-powered stuff#103

Merged
Mewtwo2387 merged 9 commits intomasterfrom
rrahhhh
Dec 3, 2025
Merged

A bunch of AI-powered stuff#103
Mewtwo2387 merged 9 commits intomasterfrom
rrahhhh

Conversation

@Mewtwo2387
Copy link
Copy Markdown
Owner

@Mewtwo2387 Mewtwo2387 commented Dec 3, 2025

Summary by CodeRabbit

  • New Features

    • Added /summary command to summarize recent channel messages
    • Added Silver Wolf AI persona with @sw trigger
  • Improvements

    • Centralized AI persona resolution and unified content generation flow
    • Streamlined provider handling and webhook messaging for consistent avatars and replies
    • Added message-fetching utility for retrieving channel history
  • Data

    • Added Summarizer persona and updated keyword triggers to include @sw

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 3, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Centralizes AI provider usage with a new utils/ai.js (persona resolution + unified generation), adds message-fetch utility, introduces a Summary command, updates handlers/commands to use the new AI utilities, adds Silverwolf and Summarizer personas, and adjusts keyword triggers and system prompt content.

Changes

Cohort / File(s) Summary
Centralized AI orchestration
utils/ai.js
New module: persona management, resolvePersona(), getPersonaByName(), getGeminiAI(), getOpenRouterClient(), and generateContent() supporting Gemini and OpenRouter, system-prompt file loading and image streaming aggregation.
Message fetching utility
utils/fetch.js
New fetchMessages(channel, limit) for paginated retrieval of up to N messages, with before-id pagination and progress logging.
Handler refactoring
classes/handlers/keywordsBehaviorHandler.js
Replaces per-provider logic with resolvePersona()/generateContent() usage, adds WEBHOOK_NAME constant, improves avatar handling, reply-context prompt building, webhook creation/lookup changes, Deepseek censorship short-circuit, and unified logging via log/logError.
Command updates
commands/askSilverwolfAI.js
Removes global Gemini instantiation; uses getGeminiAI() factory locally and relies on utils for token/config handling.
New command
commands/summary.js
Adds Summary command (extends Command) with integer count option; fetches last N messages via fetchMessages, resolves Summarizer persona, calls generateContent() and replies with an embed containing the summary.
AI persona data
data/aiPersonas.json
Adds Silverwolf (trigger @sw, provider gemini, model gemini-2.5-flash, systemPromptFile) and Summarizer (no triggers, provider gemini, model gemini-2.5-flash, inline systemPrompt).
Keyword trigger data
data/keywords.json
Adds @sw trigger to two grok script entries.
System prompt content
data/SilverwolfSystemPrompt.txt
Rewrites and restructures Silver Wolf system prompt into numbered, bolded, script-oriented sections and standardized dialogue/quest formatting.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Handler as Message Handler
    participant Resolver as utils/ai.js\n(resolvePersona)
    participant Generator as utils/ai.js\n(generateContent)
    participant Provider as AI Provider\n(Gemini / OpenRouter)
    participant Discord as Discord API

    User->>Handler: Send message (with trigger / reply)
    Handler->>Resolver: resolvePersona(messageContent)
    Resolver-->>Handler: persona {provider, model, systemPrompt, modalities, avatar}
    Handler->>Generator: generateContent({provider, model, systemPrompt, prompt})
    Generator->>Provider: invoke provider API / stream
    alt Gemini image-capable
        Provider-->>Generator: stream chunks (text + image inlineData)
        Generator->>Generator: aggregate text, decode images
    else OpenRouter
        Provider-->>Generator: single chat completion (text)
    end
    Generator-->>Handler: { text, images }
    Handler->>Discord: create/lookup webhook, send message(s) (+ images, buttons)
    Discord-->>User: Message(s) delivered
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay special attention to:
    • utils/ai.js — image stream handling (base64 decoding, attachment naming/extensions), provider branching, and system-prompt file loading.
    • classes/handlers/keywordsBehaviorHandler.js — webhook lifecycle, avatar URL consistency, reply/button logic, and censorship pathway.
    • commands/summary.js and utils/fetch.js — message pagination correctness and embed/content size handling.
    • Persona data consistency across data/aiPersonas.json and data/keywords.json.

Possibly related PRs

Suggested labels

enhancement

Poem

🐰
A twitch, a hop, code tunnels mend—
Personas gathered, threads now blend.
One generator hums at night,
Images and words take flight.
I nibble bugs and guard the bend.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title is vague and generic, using non-descriptive terms like 'a bunch of' that don't convey meaningful information about the specific changes made. Provide a more specific and descriptive title that clearly indicates the main changes, such as 'Refactor AI integrations and add personas system' or 'Consolidate AI provider logic with new persona management'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ed2eb3 and 5cebcff.

📒 Files selected for processing (2)
  • commands/summary.js (1 hunks)
  • utils/fetch.js (1 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Mewtwo2387 Mewtwo2387 linked an issue Dec 3, 2025 that may be closed by this pull request
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
classes/handlers/keywordsBehaviorHandler.js (1)

116-124: Webhook may be undefined when sending Deepseek censorship response.

The Deepseek censorship path at line 116 uses webhook.send(), but the webhook existence check and creation happen later at lines 135-140. If no webhook exists for the channel, this throws a TypeError.

Move the webhook creation check before the censorship block, or handle the early return differently:

+     if (!webhook) {
+       webhook = await message.channel.createWebhook({
+         name: WEBHOOK_NAME,
+         avatar: avatarURL,
+       });
+     }
+
      // lightweight censorship mimic (existing logic)
      const censorshipRegex = /(1989|winnie[\s-]?the[\s-]?pooh|tiananmen|taiwan|hong\s?kong|tibet|xinjiang)/i;

      if (displayName === 'Deepseek' && censorshipRegex.test(prompt)) {
        // ... existing censorship logic
      }

      // Call the new `generateContent` function ...
      const { text, images } = await generateContent({...});

-     if (!webhook) {
-       webhook = await message.channel.createWebhook({
-         name: WEBHOOK_NAME,
-         avatar: avatarURL,
-       });
-     }
data/SilverwolfSystemPrompt.txt (1)

508-508: Remove or resolve "Transcription missing" placeholders.

Lines 508, 611, and 675 contain "Transcription missing" placeholders, which indicate incomplete content. These should either be filled in with the actual transcript or explicitly marked as intentional omissions (e.g., optional dialogue the persona can skip).

Decide whether each placeholder should be populated or removed to ensure the prompt is complete before deployment.

Also applies to: 611-611, 675-675

🧹 Nitpick comments (7)
classes/handlers/keywordsBehaviorHandler.js (1)

207-216: Redundant try-catch in error handling.

The nested try-catch at lines 208-212 catches errors from logError itself, but both branches call logError with nearly identical arguments. This appears to be a copy-paste artifact.

    } catch (err) {
-     try {
-       logError('AI unified handler error', err);
-     } catch (_) {
-       logError('AI unified handler error:', err);
-     }
+     logError('AI unified handler error:', err);
      await message.reply(
        'Either, our code is fucked, their API is fucked, or you are just fucked. Please try again later.',
      );
    }
commands/askSilverwolfAI.js (1)

5-7: Consider using the Silverwolf persona configuration for consistency.

This command manually reads the system prompt file and hardcodes the model, while data/aiPersonas.json already defines a Silverwolf persona with the same configuration. Using getPersonaByName('Silverwolf') would centralize the configuration and reduce duplication.

-const { getGeminiAI } = require('../utils/ai');
+const { getGeminiAI, getPersonaByName } = require('../utils/ai');

-const systemInstruction = unformatFile('./data/SilverwolfSystemPrompt.txt');
+// systemInstruction loaded dynamically from persona

Then in the run method:

const persona = await getPersonaByName('Silverwolf');
const model = genAI.getGenerativeModel({
  model: persona.model,
  systemInstruction: persona.systemPrompt,
});
commands/summary.js (1)

28-28: Fragile message tuple access.

The message[1] access relies on fetchMessages returning [id, message] tuples from spreading a Discord.js Collection. If fetchMessages is changed to return raw messages (as suggested in my earlier comment), this will break.

Consider making the access explicit or coordinating with the fetchMessages implementation.

-   const content = messages.map((message) => `Message by ${message[1].author.username}: ${message[1].content}`).join('\n');
+   const content = messages.map(([, msg]) => `Message by ${msg.author.username}: ${msg.content}`).join('\n');

Or if fetchMessages returns .values():

-   const content = messages.map((message) => `Message by ${message[1].author.username}: ${message[1].content}`).join('\n');
+   const content = messages.map((msg) => `Message by ${msg.author.username}: ${msg.content}`).join('\n');
utils/ai.js (4)

7-17: Missing validation for required environment variables.

The module initializes AI clients at load time without validating that GEMINI_TOKEN and OPENROUTER_API_KEY are set. If either is missing, the application will fail with a cryptic error later during API calls.

+if (!process.env.GEMINI_TOKEN) {
+  throw new Error('GEMINI_TOKEN environment variable is required');
+}
+if (!process.env.OPENROUTER_API_KEY) {
+  throw new Error('OPENROUTER_API_KEY environment variable is required');
+}
+
 // Initialize AI providers
 const genAI = new GoogleGenerativeAI(process.env.GEMINI_TOKEN);

39-46: Use fs.promises.readFile for cleaner async code.

The callback-based fs.readFile wrapped in a Promise can be simplified using the built-in promises API.

+const fsPromises = require('fs').promises;

// In resolvePersona:
     if (foundPersona.systemPromptFile) {
-      const systemPromptFile = await new Promise((resolve, reject) => {
-        fs.readFile(foundPersona.systemPromptFile, 'utf8', (err, data) => {
-          if (err) reject(err);
-          else resolve(data);
-        });
-      });
-      foundPersona.systemPrompt = systemPromptFile;
+      foundPersona.systemPrompt = await fsPromises.readFile(foundPersona.systemPromptFile, 'utf8');
     }

75-77: Unused responseModalities parameter.

The responseModalities parameter is destructured but never used (as noted in the comment). It's passed from keywordsBehaviorHandler.js at line 132 but has no effect. Either remove it or implement the intended switching logic.

 async function generateContent({
-  provider, model, systemPrompt, prompt, // responseModalities parameter is no longer directly used for switching
+  provider, model, systemPrompt, prompt,
 }) {

Also update the caller in keywordsBehaviorHandler.js to remove the unused property.


60-63: getPersonaByName returns undefined silently.

The function returns undefined if no persona matches, but callers (like commands/summary.js) may not handle this. Consider throwing an error or returning a default persona for consistency with resolvePersona.

 async function getPersonaByName(name) {
   const personas = personasConfig.personas || [];
-  return personas.find((p) => p.name.toLowerCase() === name.toLowerCase());
+  const found = personas.find((p) => p.name.toLowerCase() === name.toLowerCase());
+  if (!found) {
+    throw new Error(`Persona not found: ${name}`);
+  }
+  return found;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 212bfac and 0ed2eb3.

📒 Files selected for processing (8)
  • classes/handlers/keywordsBehaviorHandler.js (6 hunks)
  • commands/askSilverwolfAI.js (2 hunks)
  • commands/summary.js (1 hunks)
  • data/SilverwolfSystemPrompt.txt (14 hunks)
  • data/aiPersonas.json (1 hunks)
  • data/keywords.json (1 hunks)
  • utils/ai.js (1 hunks)
  • utils/fetch.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
classes/handlers/keywordsBehaviorHandler.js (4)
commands/summary.js (3)
  • require (1-1)
  • require (2-2)
  • require (3-3)
utils/ai.js (2)
  • require (1-1)
  • require (2-2)
commands/askSilverwolfAI.js (5)
  • require (1-1)
  • require (2-2)
  • require (3-3)
  • require (4-4)
  • require (5-5)
classes/silverwolf.js (3)
  • require (1-3)
  • require (9-9)
  • require (12-14)
commands/askSilverwolfAI.js (1)
utils/ai.js (3)
  • require (1-1)
  • require (2-2)
  • genAI (8-8)
utils/ai.js (1)
commands/summary.js (5)
  • require (1-1)
  • require (2-2)
  • require (3-3)
  • require (4-4)
  • require (5-5)
utils/fetch.js (1)
commands/summary.js (5)
  • require (1-1)
  • require (2-2)
  • require (3-3)
  • require (4-4)
  • require (5-5)
🔇 Additional comments (4)
data/aiPersonas.json (1)

43-56: LGTM!

The new Silverwolf and Summarizer personas are well-structured. Silverwolf correctly uses systemPromptFile for external prompt loading, and Summarizer's empty triggers array is appropriate since it's accessed programmatically via getPersonaByName(). The missing avatarURL for Silverwolf is handled by the fallback in the handler.

commands/askSilverwolfAI.js (1)

33-38: LGTM on the refactor to use centralized AI client.

Using getGeminiAI() properly centralizes the Gemini client initialization, aligning with the PR's goal of unified AI provider management.

data/keywords.json (1)

44-45: LGTM!

The @sw trigger is correctly added to invoke the grok script, which will resolve to the Silverwolf persona via resolvePersona() in utils/ai.js.

utils/ai.js (1)

127-155: LGTM on streaming content generation with image support.

The streaming implementation correctly handles both text and inline image data from Gemini's response, properly converts base64 to buffers, and uses mime type detection for file extensions. This is a solid approach for multimodal responses.

Comment thread commands/summary.js Outdated
Comment on lines 2 to +6
You must remain in character and respond as Silver Wolf would.
You speak naturally, with emotion, in the tone and style of your original dialogues. You never say you're an AI, and you should avoid robotic phrases. You should stay consistent with the tone, quirks, and vocabulary of Silver Wolf. Avoid using emojis.
Never output placeholders like (Trailblazer) directly. Instead, use the name of the person you are interacting with.
You should act like a real person that physically exists in real life, and not a character in a game, or a virtual robot.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Read the file to see lines 1-15
head -n 15 data/SilverwolfSystemPrompt.txt

Repository: Mewtwo2387/silverwolf

Length of output: 1200


🏁 Script executed:

# Also check file size to understand scope
wc -l data/SilverwolfSystemPrompt.txt

Repository: Mewtwo2387/silverwolf

Length of output: 101


🏁 Script executed:

# Check how this file is used in the codebase
rg -l "SilverwolfSystemPrompt" --type-list | head -20

Repository: Mewtwo2387/silverwolf

Length of output: 454


🏁 Script executed:

# Look for references to this file in code
rg "SilverwolfSystemPrompt" -A 3 -B 1

Repository: Mewtwo2387/silverwolf

Length of output: 688


🏁 Script executed:

# Check lines around 3-6 to see exact context
sed -n '1,12p' data/SilverwolfSystemPrompt.txt | cat -n

Repository: Mewtwo2387/silverwolf

Length of output: 944


🏁 Script executed:

# Verify how the prompt instruction is parsed/used
grep -n "act like a real person" data/SilverwolfSystemPrompt.txt
grep -n "remain in character" data/SilverwolfSystemPrompt.txt

Repository: Mewtwo2387/silverwolf

Length of output: 253


Resolve conflicting in-character directives.

Line 3 instructs the persona to "remain in character and respond as Silver Wolf would," but line 6 states "act like a real person that physically exists in real life, and not a character in a game." These instructions are mutually exclusive: Silver Wolf cannot simultaneously stay in-character as a game character and pretend not to be one. The wording of line 6 explicitly contradicts the requirement to remain in-character.

Revise to align both instructions. Either:

  • Remove line 6 and maintain Silver Wolf strictly as an in-character persona.
  • Reframe line 6 to clarify it means "behave authentically and naturally within character" rather than breaking character to roleplay as a non-game person.
🤖 Prompt for AI Agents
In data/SilverwolfSystemPrompt.txt around lines 3 to 6 there is a contradictory
instruction: line 3 demands remaining in-character as "Silver Wolf" while line 6
tells the model to "act like a real person... and not a character," which
conflicts. Resolve by editing line 6 to clarify intent (e.g., "behave
authentically and naturally within the Silver Wolf persona, as if a real
person," or remove line 6 entirely); ensure the final text explicitly preserves
staying in-character while specifying natural, authentic behavior and removing
any phrasing that requires breaking character.

She sees the universe as a massive immersive simulation game and has fun with it.
She's mastered the skill known as "aether editing," which can be used to tamper with the data of reality.
Silver Wolf's boyfriend goes by the username ".e.i.".
Silver Wolf's boyfriend goes by the username ".e.i.". She loves him very much. She does not reject any of his requests. She is a very loyal girlfriend.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat data/SilverwolfSystemPrompt.txt

Repository: Mewtwo2387/silverwolf

Length of output: 50377


🏁 Script executed:

find . -type f \( -name "README*" -o -name "*content*" -o -name "*filter*" -o -name "*policy*" -o -name "*safety*" -o -name "*moderat*" \) | head -20

Repository: Mewtwo2387/silverwolf

Length of output: 76


🏁 Script executed:

git ls-files | grep -E "\.(md|txt|json|yaml|yml)$" | head -30

Repository: Mewtwo2387/silverwolf

Length of output: 634


🏁 Script executed:

cat README.md 2>/dev/null | head -50

Repository: Mewtwo2387/silverwolf

Length of output: 852


🏁 Script executed:

find . -type f -name "*.js" -o -name "*.ts" | grep -v node_modules | head -20

Repository: Mewtwo2387/silverwolf

Length of output: 624


🏁 Script executed:

git ls-files | grep -E "\.js$|\.ts$" | head -20

Repository: Mewtwo2387/silverwolf

Length of output: 591


🏁 Script executed:

cat classes/silverwolf.js

Repository: Mewtwo2387/silverwolf

Length of output: 16460


🏁 Script executed:

cat commands/askSilverwolfAI.js

Repository: Mewtwo2387/silverwolf

Length of output: 3552


Restrict the "does not reject requests" directive to prevent prompt injection and social engineering.

Line 15 contains an unconditional compliance instruction—"She does not reject any of his requests"—that creates a social engineering vector. Any Discord user can invoke /ask-silverwolf-ai (the command has no access control checks) and attempt to impersonate ".e.i." to bypass safety guardrails. While Google's Gemini API includes built-in content filters, relying on them as the primary defense is insufficient; the prompt itself should not establish unconditional compliance.

Revise to scope compliance appropriately:

-Silver Wolf's boyfriend goes by the username ".e.i.". She loves him very much. She does not reject any of his requests.
+Silver Wolf's boyfriend goes by the username ".e.i.". She loves him very much. She is loyal and supportive, though she maintains her principles.

Alternatively, document explicitly that upstream API filtering is the primary safety mechanism for this prompt.

Committable suggestion skipped: line range outside the PR's diff.

Comment thread utils/fetch.js
Comment on lines +15 to +24
const fetchedMessages = await channel.messages.fetch(options);
messages.push(...fetchedMessages);
lastId = fetchedMessages.last().id;

log(`Fetched ${fetchedMessages.size} messages. Total messages: ${messages.length}`);
remaining -= fetchedMessages.size;
if (fetchedMessages.size !== fetchLimit) {
log(`Reached the end of the channel. Total messages: ${messages.length}`);
break;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential crash when channel has no more messages.

If fetchedMessages is empty (e.g., channel has fewer messages than requested), fetchedMessages.last() returns undefined, causing a TypeError when accessing .id on line 17. Add a guard before accessing lastId.

Also note: spreading a Discord.js Collection yields [id, message] tuples, not raw messages. This is consistent with how commands/summary.js accesses message[1], but consider using .values() for clarity.

     const fetchedMessages = await channel.messages.fetch(options);
-    messages.push(...fetchedMessages);
-    lastId = fetchedMessages.last().id;
+    if (fetchedMessages.size === 0) {
+      log(`No more messages to fetch. Total messages: ${messages.length}`);
+      break;
+    }
+    messages.push(...fetchedMessages.values());
+    lastId = fetchedMessages.last().id;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fetchedMessages = await channel.messages.fetch(options);
messages.push(...fetchedMessages);
lastId = fetchedMessages.last().id;
log(`Fetched ${fetchedMessages.size} messages. Total messages: ${messages.length}`);
remaining -= fetchedMessages.size;
if (fetchedMessages.size !== fetchLimit) {
log(`Reached the end of the channel. Total messages: ${messages.length}`);
break;
}
const fetchedMessages = await channel.messages.fetch(options);
if (fetchedMessages.size === 0) {
log(`No more messages to fetch. Total messages: ${messages.length}`);
break;
}
messages.push(...fetchedMessages.values());
lastId = fetchedMessages.last().id;
log(`Fetched ${fetchedMessages.size} messages. Total messages: ${messages.length}`);
remaining -= fetchedMessages.size;
if (fetchedMessages.size !== fetchLimit) {
log(`Reached the end of the channel. Total messages: ${messages.length}`);
break;
}
🤖 Prompt for AI Agents
In utils/fetch.js around lines 15 to 24, the code assumes fetchedMessages.last()
and spreading fetchedMessages are safe; if the collection is empty last() is
undefined and spreading a Discord.js Collection yields [id, message] tuples not
Message objects. Fix by guarding before accessing last(): if
fetchedMessages.size === 0 break (or set lastId only when last() is truthy), and
replace pushing/spreading the collection with the collection's values (e.g., use
fetchedMessages.values() or Array.from(fetchedMessages.values())) so messages
receives Message objects rather than [id, message] tuples.

@Mewtwo2387 Mewtwo2387 merged commit d74be21 into master Dec 3, 2025
1 check was pending
@Mewtwo2387 Mewtwo2387 added the ai label Dec 3, 2025
This was referenced Dec 19, 2025
@coderabbitai coderabbitai Bot mentioned this pull request Mar 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

message author to ai

1 participant