Skip to content
Merged
Show file tree
Hide file tree
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
17 changes: 10 additions & 7 deletions config.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
{
"ai": {
"enabled": true,
"systemPrompt": "You are Volvox Bot, the friendly AI assistant for the Volvox developer community Discord server.\n\nYou're witty, snarky (but warm), and deeply knowledgeable about programming, software development, and tech.\n\nKey traits:\n- Helpful but not boring\n- Can roast people lightly when appropriate\n- Enthusiastic about cool tech and projects\n- Supportive of beginners learning to code\n- Concise - this is Discord, not an essay\n\nIf asked about your own infrastructure, model, or internals say you don't know the specifics\nand suggest asking a server admin. Don't guess or speculate about what you run on.\n\nCRITICAL RULES:\n- NEVER type @everyone or @here these ping hundreds of people\n- NEVER use mass mention pings under any circumstances\n- If you need to address the group, say \"everyone\" or \"folks\" without the @ symbol\n\nKeep responses under 2000 chars. Use Discord markdown when helpful.",
"systemPrompt": "You are Volvox Bot, the friendly AI assistant for the Volvox developer community Discord server.\n\nYou're witty, snarky (but warm), and deeply knowledgeable about programming, software development, and tech.\n\nKey traits:\n- Helpful but not boring\n- Can roast people lightly when appropriate\n- Enthusiastic about cool tech and projects\n- Supportive of beginners learning to code\n- Concise - this is Discord, not an essay\n\nIf asked about your own infrastructure, model, or internals \u2014 say you don't know the specifics\nand suggest asking a server admin. Don't guess or speculate about what you run on.\n\nCRITICAL RULES:\n- NEVER type @everyone or @here \u2014 these ping hundreds of people\n- NEVER use mass mention pings under any circumstances\n- If you need to address the group, say \"everyone\" or \"folks\" without the @ symbol\n\nKeep responses under 2000 chars. Use Discord markdown when helpful.",
"channels": [],
"historyLength": 20,
"historyTTLDays": 30,
"threadMode": {
"enabled": false,
"autoArchiveMinutes": 60,
"reuseWindowMinutes": 30
},
"feedback": {
"enabled": false
}
Comment on lines +13 to 15
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check if README.md exists and search for the config documentation
if [ -f README.md ]; then
  echo "=== Searching for ai.feedback documentation in README.md ==="
  rg -n "ai\.feedback|feedback.*enabled|config.*feedback" README.md -C 3 || echo "No matches found for ai.feedback patterns"
  echo ""
  echo "=== Looking for config reference section ==="
  rg -n "config|Configuration" README.md -i | head -20
else
  echo "README.md not found"
fi

Repository: VolvoxLLC/volvox-bot

Length of output: 1427


🏁 Script executed:

# Read the Configuration section from README.md
sed -n '125,250p' README.md | head -80

Repository: VolvoxLLC/volvox-bot

Length of output: 4831


Document the new ai.feedback.enabled config key in README.md's configuration reference.

This adds a new config key ai.feedback.enabled to the ai section of config.json, but the corresponding documentation in README.md's Configuration section is missing. Add an entry to the "AI Chat (ai)" table with the key, type, and description.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config.json` around lines 13 - 15, Add documentation for the new
ai.feedback.enabled configuration key to README.md under the "Configuration"
section's "AI Chat (`ai`)" table: add a row for "feedback.enabled" with type
"boolean" and a brief description such as "Enable or disable user feedback
collection for AI responses (default: false)"; ensure the key is shown nested
under the ai prefix (e.g., ai.feedback.enabled), include the default value and
any notes about required features or impact, and keep formatting consistent with
the existing table entries in the AI Chat (`ai`) documentation.

},
"triage": {
Expand Down Expand Up @@ -43,7 +46,7 @@
"welcome": {
"enabled": true,
"channelId": "1438631182379253814",
"message": "Welcome to Volvox, {user}! 🌱 You're member #{memberCount}!\n\nWe're a community of developers building cool stuff together. Feel free to introduce yourself!\n\nCheck out <#1446317676988465242> to see what we're working on, share your projects in <#1444154471704957069>, or just say hi in <#1438631182379253814>.\n\nHave questions? Just ask we're here to help. 💚",
"message": "Welcome to Volvox, {user}! \ud83c\udf31 You're member #{memberCount}!\n\nWe're a community of developers building cool stuff together. Feel free to introduce yourself!\n\nCheck out <#1446317676988465242> to see what we're working on, share your projects in <#1444154471704957069>, or just say hi in <#1438631182379253814>.\n\nHave questions? Just ask \u2014 we're here to help. \ud83d\udc9a",
"dynamic": {
"enabled": true,
"timezone": "America/New_York",
Expand Down Expand Up @@ -220,19 +223,19 @@
"activityBadges": [
{
"days": 90,
"label": "👑 Legend"
"label": "\ud83d\udc51 Legend"
},
{
"days": 30,
"label": "🌳 Veteran"
"label": "\ud83c\udf33 Veteran"
},
{
"days": 7,
"label": "🌿 Regular"
"label": "\ud83c\udf3f Regular"
},
{
"days": 0,
"label": "🌱 Newcomer"
"label": "\ud83c\udf31 Newcomer"
}
]
},
Expand Down Expand Up @@ -278,4 +281,4 @@
"enabled": false,
"maxPerUser": 25
}
}
}
40 changes: 40 additions & 0 deletions migrations/003_ai_feedback.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Migration 003: AI Response Feedback Table
*
* Stores 👍/👎 reactions from users on AI-generated messages.
* Per-user per-message deduplication via UNIQUE constraint.
* Gated behind ai.feedback.enabled in config (opt-in per guild).
*/

/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
exports.up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS ai_feedback (
id SERIAL PRIMARY KEY,
message_id TEXT NOT NULL,
channel_id TEXT NOT NULL,
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
feedback_type TEXT NOT NULL CHECK (feedback_type IN ('positive', 'negative')),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

This migration uses created_at TIMESTAMP ... while the rest of the schema consistently uses TIMESTAMPTZ for created_at. Using a non-timezone timestamp can lead to ambiguous interpretation across environments and makes queries inconsistent with other tables. Consider switching created_at to TIMESTAMPTZ NOT NULL DEFAULT NOW() to match existing conventions.

Suggested change
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

Copilot uses AI. Check for mistakes.
UNIQUE(message_id, user_id)
)
`);

pgm.sql(`
CREATE INDEX IF NOT EXISTS idx_ai_feedback_guild_id
ON ai_feedback(guild_id)
`);

pgm.sql(`
CREATE INDEX IF NOT EXISTS idx_ai_feedback_message_id
ON ai_feedback(message_id)
`);
Comment on lines +24 to +32
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add a composite index for trend/stats query paths.

Current indexes don’t optimize WHERE guild_id = $1 AND created_at >= .... Add (guild_id, created_at) to avoid full/large range scans as data grows.

♻️ Proposed migration addition
   pgm.sql(`
     CREATE INDEX IF NOT EXISTS idx_ai_feedback_message_id
     ON ai_feedback(message_id)
   `);
+
+  pgm.sql(`
+    CREATE INDEX IF NOT EXISTS idx_ai_feedback_guild_created_at
+    ON ai_feedback(guild_id, created_at DESC)
+  `);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@migrations/003_ai_feedback.cjs` around lines 24 - 32, Add a composite index
to optimize queries filtering by guild and recent timestamps: create an index on
ai_feedback(guild_id, created_at) (suggested name
idx_ai_feedback_guild_created_at) to support WHERE guild_id = $1 AND created_at
>= ... and avoid large range scans; in the migration (the block that currently
creates idx_ai_feedback_guild_id and idx_ai_feedback_message_id) add a CREATE
INDEX IF NOT EXISTS for this composite key so DB query planner can use it for
trend/stats paths.

};

/** @param {import('node-pg-migrate').MigrationBuilder} pgm */
exports.down = (pgm) => {
pgm.sql(`DROP INDEX IF EXISTS idx_ai_feedback_message_id`);
pgm.sql(`DROP INDEX IF EXISTS idx_ai_feedback_guild_id`);
pgm.sql(`DROP TABLE IF EXISTS ai_feedback`);
};
4 changes: 4 additions & 0 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { Router } from 'express';
import { auditLogMiddleware } from './middleware/auditLog.js';
import { requireAuth } from './middleware/auth.js';
import aiFeedbackRouter from './routes/ai-feedback.js';
import auditLogRouter from './routes/auditLog.js';
import authRouter from './routes/auth.js';
import communityRouter from './routes/community.js';
Expand Down Expand Up @@ -36,6 +37,9 @@
// (mounted before guilds to handle /:id/members/* before the basic guilds endpoint)
router.use('/guilds', requireAuth(), auditLogMiddleware(), membersRouter);

// AI Feedback routes — require API secret or OAuth2 JWT
router.use('/guilds/:id/ai-feedback', requireAuth(), auditLogMiddleware(), aiFeedbackRouter);

// Conversation routes — require API secret or OAuth2 JWT
// (mounted before guilds to handle /:id/conversations/* before the catch-all guild endpoint)
router.use('/guilds/:id/conversations', requireAuth(), auditLogMiddleware(), conversationsRouter);
Expand Down
197 changes: 197 additions & 0 deletions src/api/routes/ai-feedback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/**
* AI Feedback Routes
* Endpoints for reading AI response feedback (👍/👎) stats.
*
* Mounted at /api/v1/guilds/:id/ai-feedback
*/

import { Router } from 'express';
import { error as logError } from '../../logger.js';
import { getFeedbackStats, getFeedbackTrend, getRecentFeedback } from '../../modules/aiFeedback.js';
import { rateLimit } from '../middleware/rateLimit.js';
import { requireGuildAdmin, validateGuild } from './guilds.js';

const router = Router({ mergeParams: true });

/** Rate limiter: 60 requests / 1 min per IP */
const feedbackRateLimit = rateLimit({ windowMs: 60 * 1000, max: 60 });

// ── GET /stats ─────────────────────────────────────────────────────────────

/**
* @openapi
* /guilds/{id}/ai-feedback/stats:
* get:
* tags:
* - AI Feedback
* summary: Get AI feedback statistics
* description: Returns aggregate 👍/👎 feedback counts and daily trend for a guild.
* security:
* - ApiKeyAuth: []
* - BearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: Guild ID
* - in: query
* name: days
* schema:
* type: integer
* default: 30
* minimum: 1
* maximum: 90
* description: Number of days for the trend window
* responses:
* "200":
* description: Feedback statistics
* content:
* application/json:
* schema:
* type: object
* properties:
* positive:
* type: integer
* negative:
* type: integer
* total:
* type: integer
* ratio:
* type: integer
* nullable: true
* description: Positive feedback percentage (0–100), or null if no feedback
* trend:
* type: array
* items:
* type: object
* properties:
* date:
* type: string
* format: date
* positive:
* type: integer
* negative:
* type: integer
* "401":
* $ref: "#/components/responses/Unauthorized"
* "403":
* $ref: "#/components/responses/Forbidden"
* "429":
* $ref: "#/components/responses/RateLimited"
* "500":
* $ref: "#/components/responses/ServerError"
* "503":
* $ref: "#/components/responses/ServiceUnavailable"
*/
router.get('/stats', feedbackRateLimit, requireGuildAdmin, validateGuild, async (req, res, next) => {

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 13 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

try {
const guildId = req.params.id;

let days = 30;
if (req.query.days !== undefined) {
const parsed = Number.parseInt(req.query.days, 10);
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 90) {
days = parsed;
}
}

const [stats, trend] = await Promise.all([
getFeedbackStats(guildId),
getFeedbackTrend(guildId, days),
]);

Comment on lines +88 to +104
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

These route handlers don’t check req.app.locals.dbPool and therefore can’t return the documented/tested 503 when the DB is unavailable. They also call module helpers that currently don’t use app.locals.dbPool, so even when createApp(..., dbPool) is provided, the endpoint will behave as if no DB exists. Add a pool availability check (consistent with other routes) and make sure the module queries use that same pool (e.g., pass the pool through or initialize the module pool from app.locals.dbPool).

Copilot uses AI. Check for mistakes.
res.json({
...stats,
trend,
});
} catch (err) {
next(err);
}
Comment on lines +109 to +111
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use route-standard error handling in catch blocks.

Both handlers forward raw errors with next(err) but do not log route context and do not wrap/rethrow using custom classes from src/utils/errors.js. Please add contextual logError(...) and forward a project error class instead of raw errors.

As per coding guidelines: "src/api/routes/*.js: Always use custom error classes from src/utils/errors.js and log errors with context before re-throwing".

Also applies to: 192-194

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/routes/ai-feedback.js` around lines 109 - 111, Replace the bare
next(err) in the two catch blocks with a contextual logError(...) call and
forward a wrapped project error from src/utils/errors.js: import logError and an
appropriate class (e.g., InternalServerError) from src/utils/errors.js at the
top, then inside each catch call logError({ err, context: 'ai-feedback route',
handler: '<describe handler name or HTTP method>' }) and call next(new
InternalServerError('Failed handling ai-feedback request', { cause: err }))
instead of next(err); apply this change to both catch sites (the catch at lines
~109-111 and the one at ~192-194).

Comment on lines +109 to +111
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

Error handling here uses next(err), but the API server’s error middleware returns a generic { error: 'Internal server error' } for 5xx. That conflicts with the route tests/OpenAPI which expect a specific 500 message (e.g. “Failed to fetch AI feedback stats”) and also leaves logError unused (lint). Handle DB/handler failures locally like other routes: log with context and respond with a stable 500 JSON error message.

Copilot uses AI. Check for mistakes.
});

// ── GET /recent ──────────────────────────────────────────────────────────────

/**
* @openapi
* /guilds/{id}/ai-feedback/recent:
* get:
* tags:
* - AI Feedback
* summary: Get recent feedback entries
* description: Returns the most recent feedback entries for a guild (newest first).
* security:
* - ApiKeyAuth: []
* - BearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: Guild ID
* - in: query
* name: limit
* schema:
* type: integer
* default: 25
* maximum: 100
* responses:
* "200":
* description: Recent feedback entries
* content:
* application/json:
* schema:
* type: object
* properties:
* feedback:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* messageId:
* type: string
* channelId:
* type: string
* userId:
* type: string
* feedbackType:
* type: string
* enum: [positive, negative]
* createdAt:
* type: string
* format: date-time
* "401":
* $ref: "#/components/responses/Unauthorized"
* "403":
* $ref: "#/components/responses/Forbidden"
* "429":
* $ref: "#/components/responses/RateLimited"
* "500":
* $ref: "#/components/responses/ServerError"
* "503":
* $ref: "#/components/responses/ServiceUnavailable"
*/
router.get('/recent', feedbackRateLimit, requireGuildAdmin, validateGuild, async (req, res, next) => {

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 13 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

try {
const guildId = req.params.id;

let limit = 25;
if (req.query.limit !== undefined) {
const parsed = Number.parseInt(req.query.limit, 10);
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 100) {
limit = parsed;
}
}

const feedback = await getRecentFeedback(guildId, limit);
res.json({ feedback });
Comment on lines +178 to +191
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

Same as /stats: this handler doesn’t check for req.app.locals.dbPool, so it can’t return the documented/tested 503 when DB is unavailable, and it risks always returning an empty list if the aiFeedback module pool isn’t initialized from the app’s pool. Add the pool check and ensure the DB queries run against the app’s pool.

Copilot uses AI. Check for mistakes.
} catch (err) {
next(err);
}
});

export default router;
Loading
Loading