Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 src/modules/ai.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,14 +197,16 @@ export async function getHistoryAsync(channelId) {
/**
* Append a message to the in-memory conversation history for a channel and attempt to persist it to the database.
*
* Also attempts a fire-and-forget write to the DB; database errors are logged and do not throw.
* @param {string} channelId - Channel identifier used to scope the conversation.
* The in-memory history is trimmed to the configured maximum length. If a database pool is configured, the message
* is written to the conversations table in a fire-and-forget manner; DB errors are logged and do not throw.
* @param {string} channelId - Channel identifier that scopes the conversation.
* @param {string} role - Message role (e.g., "user" or "assistant").
* @param {string} content - Message text content.
* @param {string} [username] - Optional display name associated with the message.
* @param {string} [discordMessageId] - Optional native Discord message ID (used to construct jump URLs in the dashboard).
* @param {string} [discordMessageId] - Optional native Discord message ID.
* @param {string} [guildId] - Optional guild ID for the conversation (used for dashboard/jump URLs).
*/
export function addToHistory(channelId, role, content, username, discordMessageId) {
export function addToHistory(channelId, role, content, username, discordMessageId, guildId) {
if (!conversationHistory.has(channelId)) {
conversationHistory.set(channelId, []);
}
Expand All @@ -223,15 +225,16 @@ export function addToHistory(channelId, role, content, username, discordMessageI
if (pool) {
pool
.query(
`INSERT INTO conversations (channel_id, role, content, username, discord_message_id)
VALUES ($1, $2, $3, $4, $5)`,
[channelId, role, content, username || null, discordMessageId || null],
`INSERT INTO conversations (channel_id, role, content, username, discord_message_id, guild_id)
VALUES ($1, $2, $3, $4, $5, $6)`,
[channelId, role, content, username || null, discordMessageId || null, guildId || null],
)
.catch((err) => {
logError('Failed to persist message to DB', {
channelId,
role,
username: username || null,
guildId: guildId || null,
error: err.message,
});
});
Expand Down
15 changes: 14 additions & 1 deletion src/modules/triage-respond.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { info, error as logError, warn } from '../logger.js';
import { buildDebugEmbed, extractStats, logAiUsage } from '../utils/debugFooter.js';
import { safeSend } from '../utils/safeSend.js';
import { splitMessage } from '../utils/splitMessage.js';
import { addToHistory } from './ai.js';
import { resolveMessageId, sanitizeText } from './triage-filter.js';

/** Maximum characters to keep from fetched context messages. */
Expand Down Expand Up @@ -214,7 +215,19 @@ export async function sendResponses(
const msgOpts = { content: chunks[i] };
if (debugEmbed && i === 0) msgOpts.embeds = [debugEmbed];
if (replyRef && i === 0) msgOpts.reply = { messageReference: replyRef };
await safeSend(channel, msgOpts);
const sentMsg = await safeSend(channel, msgOpts);

// Log AI response to conversation history
if (sentMsg && !Array.isArray(sentMsg)) {
addToHistory(
channelId,
'assistant',
chunks[i],
null, // username - bot doesn't have one in this context
sentMsg.id,
channel.guild?.id || null
);
}
}

info('Triage response sent', {
Expand Down
20 changes: 18 additions & 2 deletions src/modules/triage.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
pushToBuffer,
} from './triage-buffer.js';
import { getDynamicInterval, isChannelEligible, resolveTriageConfig } from './triage-config.js';
import { addToHistory } from './ai.js';
import { checkTriggerWords, sanitizeText } from './triage-filter.js';
import { parseClassifyResult, parseRespondResult } from './triage-parse.js';
import { buildClassifyPrompt, buildRespondPrompt } from './triage-prompt.js';
Expand Down Expand Up @@ -545,10 +546,15 @@ export function stopTriage() {
}

/**
* Append a Discord message to the channel's triage buffer and trigger evaluation when necessary.
* Append a Discord message to the channel's triage buffer and trigger evaluation when conditions are met.
*
* Skips processing if triage is disabled, the channel is not eligible, or the message is empty/attachment-only.
* Truncates message content to 1000 characters and, when the message is a reply, captures up to 500 characters of the referenced message as reply context.
* Adds the entry to the per-channel bounded ring buffer and records the message in conversation history.
* If configured trigger words are present, forces an immediate evaluation (and falls back to scheduling if forcing fails); otherwise schedules a dynamic evaluation timer for the channel.
*
* @param {import('discord.js').Message} message - The Discord message to accumulate.
* @param {Object} msgConfig - Bot configuration containing the `triage` settings.
* @param {Object} msgConfig - Bot configuration containing triage settings (e.g., enabled, maxBufferSize, trigger words).
*/
export async function accumulateMessage(message, msgConfig) {
const triageConfig = msgConfig.triage;
Expand Down Expand Up @@ -597,6 +603,16 @@ export async function accumulateMessage(message, msgConfig) {
// Push to ring buffer (with truncation warning)
pushToBuffer(channelId, entry, maxBufferSize);

// Log user message to conversation history
addToHistory(
channelId,
'user',
entry.content,
entry.author,
entry.messageId,
message.guild?.id || null
);

// Check for trigger words -- instant evaluation
if (checkTriggerWords(message.content, msgConfig)) {
info('Trigger word detected, forcing evaluation', { channelId });
Expand Down
11 changes: 4 additions & 7 deletions tests/modules/ai.coverage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,10 @@ describe('ai module coverage', () => {

// Give the fire-and-forget a tick to run
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO conversations'), [
'ch1',
'user',
'hello',
'testuser',
null,
]);
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining('INSERT INTO conversations'),
['ch1', 'user', 'hello', 'testuser', null, null]
);
});

it('logs error when DB write fails', async () => {
Expand Down
11 changes: 4 additions & 7 deletions tests/modules/ai.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,10 @@ describe('ai module', () => {

addToHistory('ch1', 'user', 'hello', 'testuser');

expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO conversations'), [
'ch1',
'user',
'hello',
'testuser',
null,
]);
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining('INSERT INTO conversations'),
['ch1', 'user', 'hello', 'testuser', null, null]
);
});
});

Expand Down
Loading