← back to Norma
agents/twitch-agent/skills/reply.js
119 lines
/**
* Twitch Reply Skill
*
* Generates a contextual AI reply to a Twitch chat message.
* Mentions @username and keeps it concise for chat format.
*
* Rate limit: 20/30sec (shared with post)
*/
const { RateLimiter } = require('../../shared/rate-limiter');
const { logAction } = require('../../shared/audit-logger');
const { generateReply } = require('../../shared/ai');
const { sendMessage, initChat } = require('../lib/twitch');
const AGENT = 'twitch-agent';
const PLATFORM = 'twitch';
const rateLimiter = new RateLimiter(AGENT, PLATFORM);
/**
* @param {Object} body - Request body
* @param {string} body.channel - Twitch channel name (required)
* @param {string} body.username - Username to reply to (required)
* @param {string} body.message - The message being replied to (required)
* @param {string} [body.context] - Additional context about the conversation
* @param {string} [body.petition_url] - Optional petition URL if naturally relevant
* @returns {Promise<Object>}
*/
module.exports = async function reply(body) {
const { channel, username, message } = body;
if (!channel) throw new Error('channel is required');
if (!username) throw new Error('username is required');
if (!message) throw new Error('message is required');
// Check rate limit (shared with post)
const limit = await rateLimiter.checkLimit('post');
if (!limit.allowed) {
await logAction({
agent: AGENT,
actionType: 'reply',
platform: PLATFORM,
channel,
status: 'rate_limited',
errorMessage: `Rate limited. Retry after ${Math.ceil(limit.retryAfterMs / 1000)}s`,
});
return {
rate_limited: true,
retry_after_seconds: Math.ceil(limit.retryAfterMs / 1000),
current: limit.current,
max: limit.max,
};
}
// Initialize chat client
await initChat();
// Build context for AI
const fullContext = body.context
? `Channel: #${channel}\n${body.context}\n\n@${username} said: "${message}"`
: `Twitch chat in #${channel}.\n@${username} said: "${message}"`;
// Generate AI reply
let replyText;
try {
const aiResult = await generateReply({
context: fullContext,
petitionUrl: body.petition_url || null,
platform: 'twitch',
});
replyText = aiResult.reply;
} catch (err) {
console.error(`[${AGENT}] AI reply generation failed:`, err.message);
// Fallback to a generic empathetic response
replyText = "That's a really good point. These advocacy issues affect so many people.";
}
// Prepend @username mention
const fullReply = `@${username} ${replyText}`;
// Truncate to Twitch's limit
const truncated = fullReply.substring(0, 500);
// Send the reply
const result = await sendMessage(channel, truncated);
// Record the action
await rateLimiter.recordAction('post');
await logAction({
agent: AGENT,
actionType: 'reply',
platform: PLATFORM,
channel,
content: truncated,
targetId: username,
targetUrl: `https://twitch.tv/${channel.replace('#', '')}`,
responseData: {
replyTo: username,
originalMessage: message.substring(0, 200),
sent: result.sent,
simulated: result.simulated,
replyLength: truncated.length,
},
status: 'success',
});
return {
channel: result.channel,
reply_to: username,
original_message: message,
reply: truncated,
sent: result.sent,
simulated: result.simulated,
replied_at: new Date().toISOString(),
};
};