-
Notifications
You must be signed in to change notification settings - Fork 70
Expand file tree
/
Copy pathhandler.js
More file actions
226 lines (194 loc) · 6.23 KB
/
handler.js
File metadata and controls
226 lines (194 loc) · 6.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/**
* Smart Follow-ups Handler
*
* Integrates with OpenClaw to generate follow-up suggestions using
* the current session's model and authentication.
*
* Supports all OpenClaw channels with adaptive formatting:
* - Buttons: Telegram, Discord, Slack
* - Text: Signal, WhatsApp, iMessage, SMS, Matrix, Email
*/
// Channels with native button support
const BUTTON_CHANNELS = ['telegram', 'discord', 'slack'];
// Prompt for generating follow-ups
const FOLLOWUPS_PROMPT = `Based on our recent conversation, generate exactly 3 follow-up questions.
**Categories (one question each):**
1. ⚡ Quick — Short clarification or immediate next step (max 50 chars)
2. 🧠 Deep Dive — Technical depth or detailed exploration (max 50 chars)
3. 🔗 Related — Connected topic or broader context (max 50 chars)
**Rules:**
- Make questions natural and conversational
- Directly relevant to what we just discussed
- Avoid yes/no questions
- Keep each under 50 characters for button display
**Output format (strict JSON only, no markdown):**
{"quick":"question here","deep":"question here","related":"question here"}`;
/**
* Check if channel supports inline buttons
*/
function supportsButtons(channel, capabilities = []) {
return BUTTON_CHANNELS.includes(channel?.toLowerCase()) &&
(capabilities.includes('inlineButtons') || capabilities.includes('buttons'));
}
/**
* Parse follow-up suggestions from agent response
*/
function parseSuggestions(text) {
try {
// Try to extract JSON from response
const jsonMatch = text.match(/\{[^{}]*"quick"[^{}]*"deep"[^{}]*"related"[^{}]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
}
// Fallback: try parsing entire response as JSON
return JSON.parse(text.trim());
} catch (e) {
console.error('[smart-followups] Failed to parse suggestions:', e.message);
return null;
}
}
/**
* Format suggestions as Telegram/Discord/Slack inline buttons
*/
function formatButtons(suggestions) {
return [
[{ text: `⚡ ${suggestions.quick}`, callback_data: suggestions.quick }],
[{ text: `🧠 ${suggestions.deep}`, callback_data: suggestions.deep }],
[{ text: `🔗 ${suggestions.related}`, callback_data: suggestions.related }]
];
}
/**
* Format suggestions as text list (for channels without buttons)
*/
function formatTextList(suggestions, options = {}) {
const { compact = false, stripEmoji = false } = options;
if (compact) {
// Minimal format for SMS
let text = `Follow-ups:\n1. ${suggestions.quick}\n2. ${suggestions.deep}\n3. ${suggestions.related}\n\nReply 1, 2, or 3`;
if (stripEmoji) {
text = text.replace(/[⚡🧠🔗💡]/g, '');
}
return text;
}
// Full format with categories
return `💡 **Smart Follow-up Suggestions**
⚡ **Quick**
1. ${suggestions.quick}
🧠 **Deep Dive**
2. ${suggestions.deep}
🔗 **Related**
3. ${suggestions.related}
Reply with 1, 2, or 3 to ask that question.`;
}
/**
* Store suggestions in session for number reply handling
*/
function storeSuggestions(ctx, suggestions) {
if (ctx.session) {
ctx.session.lastFollowups = {
'1': suggestions.quick,
'2': suggestions.deep,
'3': suggestions.related,
timestamp: Date.now()
};
}
}
/**
* Check if message is a follow-up number reply
*/
function isNumberReply(text) {
return /^[123]$/.test(text?.trim());
}
/**
* Get question from number reply
*/
function getQuestionFromNumber(ctx, number) {
if (ctx.session?.lastFollowups) {
const elapsed = Date.now() - ctx.session.lastFollowups.timestamp;
// Only valid for 10 minutes
if (elapsed < 10 * 60 * 1000) {
return ctx.session.lastFollowups[number];
}
}
return null;
}
/**
* Skill module export
*/
module.exports = {
name: 'smart-followups',
version: '1.0.0',
description: 'Generate contextual follow-up suggestions after AI responses',
// Supported channels (all of them!)
channels: ['telegram', 'discord', 'slack', 'signal', 'whatsapp', 'imessage', 'sms', 'matrix', 'email'],
commands: {
followups: {
description: 'Generate 3 smart follow-up suggestions',
aliases: ['fu', 'next', 'suggest'],
/**
* Handle /followups command
*/
async execute(ctx) {
const channel = ctx.channel?.toLowerCase() || 'unknown';
const capabilities = ctx.capabilities || [];
const useButtons = supportsButtons(channel, capabilities);
// Return prompt that makes the agent generate follow-ups
return {
type: 'agent-prompt',
prompt: FOLLOWUPS_PROMPT,
// Post-process the agent's response
transform: (response) => {
const suggestions = parseSuggestions(response);
if (!suggestions) {
return {
text: "Sorry, I couldn't generate follow-up suggestions. Try `/followups` again?"
};
}
// Store for number reply handling (text mode)
storeSuggestions(ctx, suggestions);
if (useButtons) {
return {
text: '💡 **What would you like to explore next?**',
buttons: formatButtons(suggestions)
};
} else {
// Determine text format options based on channel
const options = {
compact: channel === 'sms',
stripEmoji: channel === 'sms'
};
return {
text: formatTextList(suggestions, options)
};
}
}
};
}
}
},
/**
* Message interceptor for handling number replies
*/
onMessage: async (ctx, next) => {
const text = ctx.message?.text?.trim();
// Check if this is a number reply to follow-ups
if (isNumberReply(text)) {
const question = getQuestionFromNumber(ctx, text);
if (question) {
// Replace the number with the actual question
ctx.message.text = question;
ctx.message._followupExpanded = true;
}
}
return next();
},
// Export utilities for CLI/testing
utils: {
parseSuggestions,
formatButtons,
formatTextList,
supportsButtons,
FOLLOWUPS_PROMPT,
BUTTON_CHANNELS
}
};