@operor/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/index.d.ts +611 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2863 -0
- package/dist/index.js.map +1 -0
- package/package.json +33 -0
- package/src/Agent.ts +267 -0
- package/src/AgentLoader.ts +165 -0
- package/src/AgentVersionStore.ts +59 -0
- package/src/Guardrails.ts +60 -0
- package/src/InMemoryStore.ts +89 -0
- package/src/KeywordIntentClassifier.ts +74 -0
- package/src/LLMIntentClassifier.ts +68 -0
- package/src/Operor.ts +2972 -0
- package/src/__tests__/AgentLoader.test.ts +315 -0
- package/src/__tests__/InMemoryStore.test.ts +57 -0
- package/src/guardrails.test.ts +180 -0
- package/src/index.ts +11 -0
- package/src/types.ts +286 -0
- package/test/integration.test.ts +114 -0
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +10 -0
- package/vitest.config.ts +10 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2863 @@
|
|
|
1
|
+
import EventEmitter from "eventemitter3";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import matter from "gray-matter";
|
|
6
|
+
|
|
7
|
+
//#region src/Agent.ts
|
|
8
|
+
var Agent = class extends EventEmitter {
|
|
9
|
+
id;
|
|
10
|
+
name;
|
|
11
|
+
config;
|
|
12
|
+
tools = /* @__PURE__ */ new Map();
|
|
13
|
+
constructor(config) {
|
|
14
|
+
super();
|
|
15
|
+
this.id = `agent_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
16
|
+
this.name = config.name;
|
|
17
|
+
this.config = config;
|
|
18
|
+
config.tools?.forEach((tool) => {
|
|
19
|
+
this.tools.set(tool.name, tool);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Check if this agent should handle the given intent
|
|
24
|
+
*/
|
|
25
|
+
matchesIntent(intent) {
|
|
26
|
+
if (!this.config.triggers || this.config.triggers.length === 0) return true;
|
|
27
|
+
const lowerIntent = intent.toLowerCase();
|
|
28
|
+
return this.config.triggers.some((trigger) => trigger === "*" || lowerIntent.includes(trigger.toLowerCase()));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Process a message
|
|
32
|
+
*/
|
|
33
|
+
async process(context) {
|
|
34
|
+
const startTime = Date.now();
|
|
35
|
+
try {
|
|
36
|
+
const response = await this.generateResponse(context);
|
|
37
|
+
const duration = Date.now() - startTime;
|
|
38
|
+
return {
|
|
39
|
+
text: response.text,
|
|
40
|
+
toolCalls: response.toolCalls,
|
|
41
|
+
duration,
|
|
42
|
+
cost: .001
|
|
43
|
+
};
|
|
44
|
+
} catch (error) {
|
|
45
|
+
Date.now() - startTime;
|
|
46
|
+
throw new Error(`Agent processing failed: ${error}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Generate response (simplified - would use LLM in production)
|
|
51
|
+
*/
|
|
52
|
+
async generateResponse(context) {
|
|
53
|
+
const { currentMessage, customer, history } = context;
|
|
54
|
+
const toolCalls = [];
|
|
55
|
+
const text = currentMessage.text.toLowerCase();
|
|
56
|
+
const matchedKeyword = [
|
|
57
|
+
"headphones",
|
|
58
|
+
"keyboard",
|
|
59
|
+
"mouse",
|
|
60
|
+
"cable",
|
|
61
|
+
"product",
|
|
62
|
+
"electronics",
|
|
63
|
+
"stock",
|
|
64
|
+
"available",
|
|
65
|
+
"carry",
|
|
66
|
+
"sell"
|
|
67
|
+
].find((kw) => text.includes(kw));
|
|
68
|
+
const hasProductQuery = !!matchedKeyword;
|
|
69
|
+
if (hasProductQuery) {
|
|
70
|
+
const searchProductsTool = this.tools.get("search_products");
|
|
71
|
+
if (searchProductsTool) {
|
|
72
|
+
const query = matchedKeyword || currentMessage.text;
|
|
73
|
+
const toolCall = {
|
|
74
|
+
id: `tool_${Date.now()}`,
|
|
75
|
+
name: "search_products",
|
|
76
|
+
params: {
|
|
77
|
+
query,
|
|
78
|
+
limit: 5
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
toolCall.result = await searchProductsTool.execute({
|
|
83
|
+
query,
|
|
84
|
+
limit: 5
|
|
85
|
+
});
|
|
86
|
+
toolCall.success = true;
|
|
87
|
+
toolCall.duration = 100;
|
|
88
|
+
toolCalls.push(toolCall);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
toolCall.success = false;
|
|
91
|
+
toolCall.error = String(error);
|
|
92
|
+
toolCalls.push(toolCall);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const orderMatch = text.match(/#?(\d{4,})/);
|
|
97
|
+
if (orderMatch) {
|
|
98
|
+
const orderId = orderMatch[1];
|
|
99
|
+
const getOrderTool = this.tools.get("get_order");
|
|
100
|
+
if (getOrderTool) {
|
|
101
|
+
const toolCall = {
|
|
102
|
+
id: `tool_${Date.now()}`,
|
|
103
|
+
name: "get_order",
|
|
104
|
+
params: { orderId }
|
|
105
|
+
};
|
|
106
|
+
try {
|
|
107
|
+
const result = await getOrderTool.execute({ orderId });
|
|
108
|
+
toolCall.result = result;
|
|
109
|
+
toolCall.success = true;
|
|
110
|
+
toolCall.duration = 100;
|
|
111
|
+
toolCalls.push(toolCall);
|
|
112
|
+
const ruleActions = await this.applyBusinessRules(context, toolCalls);
|
|
113
|
+
let responseText = `I found your order #${orderId}.\n\n`;
|
|
114
|
+
responseText += `Status: ${result.status}\n`;
|
|
115
|
+
if (result.tracking) responseText += `Tracking: ${result.tracking}\n`;
|
|
116
|
+
if (ruleActions.length > 0) {
|
|
117
|
+
for (const action of ruleActions) if (action.type === "discount_created") responseText += `\nI apologize for the delay! I've created a ${action.percent}% discount code for you: ${action.code} (valid ${action.validDays} days)`;
|
|
118
|
+
}
|
|
119
|
+
if (hasProductQuery) {
|
|
120
|
+
const searchResult = toolCalls.find((tc) => tc.name === "search_products");
|
|
121
|
+
if (searchResult?.success && searchResult.result?.found > 0) {
|
|
122
|
+
responseText += `\n\nI also found ${searchResult.result.found} product(s) matching your search:\n\n`;
|
|
123
|
+
for (const product of searchResult.result.products) {
|
|
124
|
+
responseText += `• ${product.title} by ${product.vendor} - $${product.price}`;
|
|
125
|
+
if (!product.available) responseText += " (Out of stock)";
|
|
126
|
+
responseText += "\n";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
text: responseText,
|
|
132
|
+
toolCalls
|
|
133
|
+
};
|
|
134
|
+
} catch (error) {
|
|
135
|
+
toolCall.success = false;
|
|
136
|
+
toolCall.error = String(error);
|
|
137
|
+
toolCalls.push(toolCall);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (hasProductQuery && toolCalls.some((tc) => tc.name === "search_products")) {
|
|
142
|
+
const searchResult = toolCalls.find((tc) => tc.name === "search_products");
|
|
143
|
+
if (searchResult?.success) {
|
|
144
|
+
let responseText = "";
|
|
145
|
+
if (searchResult.result?.found > 0) {
|
|
146
|
+
responseText = `I found ${searchResult.result.found} product(s) matching your search:\n\n`;
|
|
147
|
+
for (const product of searchResult.result.products) {
|
|
148
|
+
responseText += `• ${product.title} by ${product.vendor} - $${product.price}`;
|
|
149
|
+
if (!product.available) responseText += " (Out of stock)";
|
|
150
|
+
responseText += "\n";
|
|
151
|
+
}
|
|
152
|
+
} else responseText = `I couldn't find any products matching "${matchedKeyword}". Would you like to try a different search?`;
|
|
153
|
+
return {
|
|
154
|
+
text: responseText,
|
|
155
|
+
toolCalls
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const isGreeting = [
|
|
160
|
+
"hello",
|
|
161
|
+
"hi",
|
|
162
|
+
"hey",
|
|
163
|
+
"good morning",
|
|
164
|
+
"good afternoon",
|
|
165
|
+
"good evening"
|
|
166
|
+
].some((g) => text.trim().startsWith(g));
|
|
167
|
+
if (!history || history.length === 0 || isGreeting) return {
|
|
168
|
+
text: `Hi ${customer.name || "there"}! I'm ${this.name}. ${this.config.purpose || "How can I help you today?"}`,
|
|
169
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
text: `I understand. How can I assist you further?`,
|
|
173
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Apply business rules
|
|
178
|
+
*/
|
|
179
|
+
async applyBusinessRules(context, toolResults) {
|
|
180
|
+
const actions = [];
|
|
181
|
+
if (!this.config.rules) return actions;
|
|
182
|
+
for (const rule of this.config.rules) try {
|
|
183
|
+
if (await rule.condition(context, toolResults)) {
|
|
184
|
+
const action = await rule.action(context, toolResults);
|
|
185
|
+
actions.push(action);
|
|
186
|
+
if (action.toolName && this.tools.has(action.toolName)) action.result = await this.tools.get(action.toolName).execute(action.params);
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error(`Rule "${rule.name}" failed:`, error);
|
|
190
|
+
}
|
|
191
|
+
return actions;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get agent configuration
|
|
195
|
+
*/
|
|
196
|
+
getConfig() {
|
|
197
|
+
return { ...this.config };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get available tools
|
|
201
|
+
*/
|
|
202
|
+
getTools() {
|
|
203
|
+
return Array.from(this.tools.values());
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Register tools dynamically (e.g., from integrations)
|
|
207
|
+
*/
|
|
208
|
+
registerTools(tools) {
|
|
209
|
+
tools.forEach((tool) => {
|
|
210
|
+
this.tools.set(tool.name, tool);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/InMemoryStore.ts
|
|
217
|
+
/**
|
|
218
|
+
* Default in-memory implementation of MemoryStore.
|
|
219
|
+
* Data is lost on restart. Used as the default when no external store is provided.
|
|
220
|
+
*/
|
|
221
|
+
var InMemoryStore = class {
|
|
222
|
+
customers = /* @__PURE__ */ new Map();
|
|
223
|
+
phoneIndex = /* @__PURE__ */ new Map();
|
|
224
|
+
conversations = /* @__PURE__ */ new Map();
|
|
225
|
+
settings = /* @__PURE__ */ new Map();
|
|
226
|
+
async initialize() {}
|
|
227
|
+
async getCustomer(id) {
|
|
228
|
+
return this.customers.get(id) || null;
|
|
229
|
+
}
|
|
230
|
+
async getCustomerByPhone(phone) {
|
|
231
|
+
const customerId = this.phoneIndex.get(phone);
|
|
232
|
+
if (!customerId) return null;
|
|
233
|
+
return this.customers.get(customerId) || null;
|
|
234
|
+
}
|
|
235
|
+
async upsertCustomer(customer) {
|
|
236
|
+
this.customers.set(customer.id, customer);
|
|
237
|
+
if (customer.phone) this.phoneIndex.set(customer.phone, customer.id);
|
|
238
|
+
}
|
|
239
|
+
async getHistory(customerId, limit = 50, agentId) {
|
|
240
|
+
const key = agentId ? `${customerId}:${agentId}` : customerId;
|
|
241
|
+
return (this.conversations.get(key) || []).slice(-limit);
|
|
242
|
+
}
|
|
243
|
+
async addMessage(customerId, message, agentId) {
|
|
244
|
+
const key = agentId ? `${customerId}:${agentId}` : customerId;
|
|
245
|
+
if (!this.conversations.has(key)) this.conversations.set(key, []);
|
|
246
|
+
const history = this.conversations.get(key);
|
|
247
|
+
history.push(message);
|
|
248
|
+
if (history.length > 50) history.splice(0, history.length - 50);
|
|
249
|
+
}
|
|
250
|
+
async clearHistory(customerId, agentId) {
|
|
251
|
+
let deletedCount = 0;
|
|
252
|
+
if (customerId) for (const [key, messages] of this.conversations) {
|
|
253
|
+
const [cid, aid] = key.includes(":") ? key.split(":") : [key, void 0];
|
|
254
|
+
if (cid === customerId && (!agentId || aid === agentId)) {
|
|
255
|
+
deletedCount += messages.length;
|
|
256
|
+
this.conversations.delete(key);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
for (const messages of this.conversations.values()) deletedCount += messages.length;
|
|
261
|
+
this.conversations.clear();
|
|
262
|
+
}
|
|
263
|
+
return { deletedCount };
|
|
264
|
+
}
|
|
265
|
+
async getSetting(key) {
|
|
266
|
+
return this.settings.get(key) ?? null;
|
|
267
|
+
}
|
|
268
|
+
async setSetting(key, value) {
|
|
269
|
+
if (value === null) this.settings.delete(key);
|
|
270
|
+
else this.settings.set(key, value);
|
|
271
|
+
}
|
|
272
|
+
async close() {
|
|
273
|
+
this.customers.clear();
|
|
274
|
+
this.phoneIndex.clear();
|
|
275
|
+
this.conversations.clear();
|
|
276
|
+
this.settings.clear();
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/KeywordIntentClassifier.ts
|
|
282
|
+
/**
|
|
283
|
+
* Default keyword-based intent classifier.
|
|
284
|
+
* Uses simple keyword matching to classify user messages.
|
|
285
|
+
*/
|
|
286
|
+
var KeywordIntentClassifier = class {
|
|
287
|
+
async classify(message, agents, history) {
|
|
288
|
+
const lowerText = message.toLowerCase();
|
|
289
|
+
for (const agent of agents) {
|
|
290
|
+
if (!agent.triggers) continue;
|
|
291
|
+
for (const trigger of agent.triggers) if (lowerText.includes(trigger.toLowerCase())) return {
|
|
292
|
+
intent: trigger,
|
|
293
|
+
confidence: .9,
|
|
294
|
+
entities: {}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (lowerText.includes("order") || lowerText.includes("tracking") || lowerText.includes("where is") || lowerText.includes("delivery")) return {
|
|
298
|
+
intent: "order_tracking",
|
|
299
|
+
confidence: .95,
|
|
300
|
+
entities: {}
|
|
301
|
+
};
|
|
302
|
+
if (lowerText.includes("product") || lowerText.includes("buy") || lowerText.includes("price")) return {
|
|
303
|
+
intent: "product_inquiry",
|
|
304
|
+
confidence: .9,
|
|
305
|
+
entities: {}
|
|
306
|
+
};
|
|
307
|
+
if (lowerText.includes("help") || lowerText.includes("support") || lowerText.includes("problem")) return {
|
|
308
|
+
intent: "support",
|
|
309
|
+
confidence: .85,
|
|
310
|
+
entities: {}
|
|
311
|
+
};
|
|
312
|
+
return {
|
|
313
|
+
intent: "general",
|
|
314
|
+
confidence: .5,
|
|
315
|
+
entities: {}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region src/Guardrails.ts
|
|
322
|
+
var GuardrailEngine = class {
|
|
323
|
+
/**
|
|
324
|
+
* Check incoming message against guardrail config.
|
|
325
|
+
* Returns whether the message is allowed, and whether it should escalate.
|
|
326
|
+
*/
|
|
327
|
+
checkInput(message, config) {
|
|
328
|
+
const lowerMessage = message.toLowerCase();
|
|
329
|
+
if (config.escalationTriggers) {
|
|
330
|
+
for (const trigger of config.escalationTriggers) if (lowerMessage.includes(trigger.toLowerCase())) return {
|
|
331
|
+
allowed: false,
|
|
332
|
+
reason: `Escalation triggered: "${trigger}"`,
|
|
333
|
+
escalate: true
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (config.blockedTopics) {
|
|
337
|
+
for (const topic of config.blockedTopics) if (lowerMessage.includes(topic.toLowerCase())) return {
|
|
338
|
+
allowed: false,
|
|
339
|
+
reason: `Blocked topic: "${topic}"`
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return { allowed: true };
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Check outgoing response against guardrail config.
|
|
346
|
+
* Returns whether the response is allowed to be sent.
|
|
347
|
+
*/
|
|
348
|
+
checkOutput(response, config) {
|
|
349
|
+
if (config.maxResponseLength && response.length > config.maxResponseLength) return {
|
|
350
|
+
allowed: false,
|
|
351
|
+
reason: `Response exceeds max length (${response.length}/${config.maxResponseLength})`
|
|
352
|
+
};
|
|
353
|
+
if (config.blockedTopics) {
|
|
354
|
+
const lowerResponse = response.toLowerCase();
|
|
355
|
+
for (const topic of config.blockedTopics) if (lowerResponse.includes(topic.toLowerCase())) return {
|
|
356
|
+
allowed: false,
|
|
357
|
+
reason: `Response contains blocked topic: "${topic}"`
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return { allowed: true };
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
//#endregion
|
|
365
|
+
//#region src/AgentLoader.ts
|
|
366
|
+
/**
|
|
367
|
+
* Loads agent definitions from a file-based directory structure.
|
|
368
|
+
*
|
|
369
|
+
* Expected layout:
|
|
370
|
+
* agents/
|
|
371
|
+
* _defaults/
|
|
372
|
+
* SOUL.md (optional global soul/personality)
|
|
373
|
+
* <agent-name>/
|
|
374
|
+
* INSTRUCTIONS.md (required — YAML frontmatter + markdown body)
|
|
375
|
+
* IDENTITY.md (optional — identity/persona)
|
|
376
|
+
* SOUL.md (optional — overrides _defaults/SOUL.md)
|
|
377
|
+
* USER.md (optional — workspace-level user context)
|
|
378
|
+
*/
|
|
379
|
+
var AgentLoader = class {
|
|
380
|
+
agentsDir;
|
|
381
|
+
workspaceRoot;
|
|
382
|
+
constructor(workspaceRoot, agentsDir) {
|
|
383
|
+
this.workspaceRoot = workspaceRoot;
|
|
384
|
+
this.agentsDir = agentsDir ?? path.join(workspaceRoot, "agents");
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Load all agent definitions from the agents directory.
|
|
388
|
+
*/
|
|
389
|
+
async loadAll() {
|
|
390
|
+
const agentDirs = (await fs.readdir(this.agentsDir, { withFileTypes: true })).filter((e) => e.isDirectory() && !e.name.startsWith("_"));
|
|
391
|
+
const definitions = [];
|
|
392
|
+
for (const dir of agentDirs) {
|
|
393
|
+
const def = await this.loadAgent(dir.name);
|
|
394
|
+
if (def) definitions.push(def);
|
|
395
|
+
}
|
|
396
|
+
return definitions;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Load a single agent definition by directory name.
|
|
400
|
+
*/
|
|
401
|
+
async loadAgent(name) {
|
|
402
|
+
const agentDir = path.join(this.agentsDir, name);
|
|
403
|
+
const instructionsPath = path.join(agentDir, "INSTRUCTIONS.md");
|
|
404
|
+
const instructionsRaw = await readFileOrNull(instructionsPath);
|
|
405
|
+
if (instructionsRaw === null) return null;
|
|
406
|
+
const { data: frontmatter, content: body } = matter(instructionsRaw);
|
|
407
|
+
const [identity, agentSoul, defaultSoul, userContext] = await Promise.all([
|
|
408
|
+
readFileOrNull(path.join(agentDir, "IDENTITY.md")),
|
|
409
|
+
readFileOrNull(path.join(agentDir, "SOUL.md")),
|
|
410
|
+
readFileOrNull(path.join(this.agentsDir, "_defaults", "SOUL.md")),
|
|
411
|
+
readFileOrNull(path.join(this.workspaceRoot, "USER.md"))
|
|
412
|
+
]);
|
|
413
|
+
const soul = agentSoul ?? defaultSoul;
|
|
414
|
+
const systemPrompt = buildSystemPrompt({
|
|
415
|
+
identity,
|
|
416
|
+
soul,
|
|
417
|
+
body,
|
|
418
|
+
userContext,
|
|
419
|
+
guardrails: frontmatter.guardrails
|
|
420
|
+
});
|
|
421
|
+
const skills = frontmatter.skills;
|
|
422
|
+
return {
|
|
423
|
+
config: {
|
|
424
|
+
name: frontmatter.name ?? name,
|
|
425
|
+
purpose: frontmatter.purpose,
|
|
426
|
+
triggers: frontmatter.triggers,
|
|
427
|
+
channels: frontmatter.channels,
|
|
428
|
+
skills,
|
|
429
|
+
knowledgeBase: frontmatter.knowledgeBase,
|
|
430
|
+
priority: frontmatter.priority,
|
|
431
|
+
escalateTo: frontmatter.escalateTo,
|
|
432
|
+
guardrails: frontmatter.guardrails,
|
|
433
|
+
systemPrompt,
|
|
434
|
+
_rawInstructions: body.trim() || void 0,
|
|
435
|
+
_rawIdentity: identity?.trim() || void 0,
|
|
436
|
+
_rawSoul: soul?.trim() || void 0
|
|
437
|
+
},
|
|
438
|
+
systemPrompt,
|
|
439
|
+
instructionsPath
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
/**
|
|
444
|
+
* Read a file and return its contents, or null if it doesn't exist.
|
|
445
|
+
*/
|
|
446
|
+
async function readFileOrNull(filePath) {
|
|
447
|
+
try {
|
|
448
|
+
return await fs.readFile(filePath, "utf-8");
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Assemble the system prompt from structured sections.
|
|
455
|
+
*/
|
|
456
|
+
function buildSystemPrompt(parts) {
|
|
457
|
+
const sections = [];
|
|
458
|
+
if (parts.identity) sections.push(`## Identity\n\n${parts.identity.trim()}`);
|
|
459
|
+
if (parts.soul) sections.push(`## Soul\n\n${parts.soul.trim()}`);
|
|
460
|
+
if (parts.body.trim()) sections.push(`## Instructions\n\n${parts.body.trim()}`);
|
|
461
|
+
if (parts.userContext) sections.push(`## User Context\n\n${parts.userContext.trim()}`);
|
|
462
|
+
if (parts.guardrails?.systemRules && parts.guardrails.systemRules.length > 0) {
|
|
463
|
+
const rules = parts.guardrails.systemRules.map((rule, i) => `${i + 1}. ${rule}`).join("\n");
|
|
464
|
+
sections.push(`## System Rules\n\nYou MUST follow these rules at all times:\n${rules}`);
|
|
465
|
+
}
|
|
466
|
+
return sections.join("\n\n");
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
//#endregion
|
|
470
|
+
//#region src/AgentVersionStore.ts
|
|
471
|
+
const MAX_VERSIONS = 20;
|
|
472
|
+
var AgentVersionStore = class {
|
|
473
|
+
agentsDir;
|
|
474
|
+
memory = /* @__PURE__ */ new Map();
|
|
475
|
+
constructor(agentsDir) {
|
|
476
|
+
this.agentsDir = agentsDir;
|
|
477
|
+
}
|
|
478
|
+
async saveVersion(agentName, snapshot) {
|
|
479
|
+
const history = await this.loadHistory(agentName);
|
|
480
|
+
history.unshift(snapshot);
|
|
481
|
+
if (history.length > MAX_VERSIONS) history.length = MAX_VERSIONS;
|
|
482
|
+
if (this.agentsDir) {
|
|
483
|
+
const dir = path.join(this.agentsDir, "_versions");
|
|
484
|
+
await fs.mkdir(dir, { recursive: true });
|
|
485
|
+
await fs.writeFile(path.join(dir, `${agentName}.json`), JSON.stringify(history, null, 2));
|
|
486
|
+
} else this.memory.set(agentName, history);
|
|
487
|
+
}
|
|
488
|
+
async getHistory(agentName, limit) {
|
|
489
|
+
const history = await this.loadHistory(agentName);
|
|
490
|
+
return limit ? history.slice(0, limit) : history;
|
|
491
|
+
}
|
|
492
|
+
async getVersion(agentName, index) {
|
|
493
|
+
return (await this.loadHistory(agentName))[index] ?? null;
|
|
494
|
+
}
|
|
495
|
+
async loadHistory(agentName) {
|
|
496
|
+
if (this.agentsDir) try {
|
|
497
|
+
const data = await fs.readFile(path.join(this.agentsDir, "_versions", `${agentName}.json`), "utf-8");
|
|
498
|
+
return JSON.parse(data);
|
|
499
|
+
} catch {
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
return [...this.memory.get(agentName) ?? []];
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region src/Operor.ts
|
|
508
|
+
/**
|
|
509
|
+
* Truncate text at the last sentence boundary within maxLen.
|
|
510
|
+
* Falls back to last whitespace if no sentence-ending punctuation is found.
|
|
511
|
+
*/
|
|
512
|
+
function truncateAtSentence(text, maxLen) {
|
|
513
|
+
if (text.length <= maxLen) return text;
|
|
514
|
+
const truncated = text.slice(0, maxLen);
|
|
515
|
+
const sentenceEnd = Math.max(truncated.lastIndexOf(". "), truncated.lastIndexOf("! "), truncated.lastIndexOf("? "), truncated.lastIndexOf(".\n"), truncated.lastIndexOf("!\n"), truncated.lastIndexOf("?\n"));
|
|
516
|
+
if (sentenceEnd > maxLen * .5) return truncated.slice(0, sentenceEnd + 1).trimEnd();
|
|
517
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
518
|
+
if (lastSpace > maxLen * .5) return truncated.slice(0, lastSpace).trimEnd();
|
|
519
|
+
return truncated.trimEnd();
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Split text into chunks that fit within maxLen, breaking at paragraph
|
|
523
|
+
* boundaries (\n\n). Falls back to sentence splitting for oversized paragraphs.
|
|
524
|
+
*/
|
|
525
|
+
function splitIntoParagraphChunks(text, maxLen) {
|
|
526
|
+
if (text.length <= maxLen) return [text];
|
|
527
|
+
const paragraphs = text.split(/\n\n+/);
|
|
528
|
+
const chunks = [];
|
|
529
|
+
let current = "";
|
|
530
|
+
for (const para of paragraphs) {
|
|
531
|
+
const candidate = current ? current + "\n\n" + para : para;
|
|
532
|
+
if (candidate.length <= maxLen) {
|
|
533
|
+
current = candidate;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (current) {
|
|
537
|
+
chunks.push(current.trimEnd());
|
|
538
|
+
current = "";
|
|
539
|
+
}
|
|
540
|
+
if (para.length <= maxLen) {
|
|
541
|
+
current = para;
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
let remaining = para;
|
|
545
|
+
while (remaining.length > maxLen) {
|
|
546
|
+
const piece = truncateAtSentence(remaining, maxLen);
|
|
547
|
+
chunks.push(piece.trimEnd());
|
|
548
|
+
remaining = remaining.slice(piece.length).trimStart();
|
|
549
|
+
}
|
|
550
|
+
if (remaining) current = remaining;
|
|
551
|
+
}
|
|
552
|
+
if (current) chunks.push(current.trimEnd());
|
|
553
|
+
return chunks.filter(Boolean);
|
|
554
|
+
}
|
|
555
|
+
var Operor = class extends EventEmitter {
|
|
556
|
+
config;
|
|
557
|
+
providers = /* @__PURE__ */ new Map();
|
|
558
|
+
agents = /* @__PURE__ */ new Map();
|
|
559
|
+
skills = /* @__PURE__ */ new Map();
|
|
560
|
+
memory;
|
|
561
|
+
intentClassifier;
|
|
562
|
+
isRunning = false;
|
|
563
|
+
messageBatches = /* @__PURE__ */ new Map();
|
|
564
|
+
batchTimers = /* @__PURE__ */ new Map();
|
|
565
|
+
batchWindowMs = 2e3;
|
|
566
|
+
autoAddAssistantMessages = true;
|
|
567
|
+
pendingEdits = /* @__PURE__ */ new Map();
|
|
568
|
+
pendingQuickstarts = /* @__PURE__ */ new Map();
|
|
569
|
+
pendingSkillAdds = /* @__PURE__ */ new Map();
|
|
570
|
+
pendingFaqReplacements = /* @__PURE__ */ new Map();
|
|
571
|
+
versionStore;
|
|
572
|
+
constructor(config = {}) {
|
|
573
|
+
super();
|
|
574
|
+
this.config = config;
|
|
575
|
+
this.memory = config.memory || new InMemoryStore();
|
|
576
|
+
this.intentClassifier = config.intentClassifier || new KeywordIntentClassifier();
|
|
577
|
+
this.batchWindowMs = config.batchWindowMs ?? 2e3;
|
|
578
|
+
this.autoAddAssistantMessages = config.autoAddAssistantMessages ?? true;
|
|
579
|
+
this.versionStore = new AgentVersionStore(config.agentsDir);
|
|
580
|
+
this.log("Operor initialized");
|
|
581
|
+
if (config.analyticsCollector) {
|
|
582
|
+
config.analyticsCollector.attach(this);
|
|
583
|
+
this.log("Analytics collector attached");
|
|
584
|
+
}
|
|
585
|
+
if (config.copilotTracker) this.on("message:processed", (event) => {
|
|
586
|
+
config.copilotTracker.maybeTrack({
|
|
587
|
+
query: event.message?.text,
|
|
588
|
+
channel: event.message?.channel || event.message?.provider,
|
|
589
|
+
customerPhone: event.message?.from,
|
|
590
|
+
response: event.response
|
|
591
|
+
}).catch((err) => {
|
|
592
|
+
if (config.debug) console.warn("[Copilot] Tracking error:", err?.message);
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Register a message provider (WhatsApp, Instagram, etc.)
|
|
598
|
+
*/
|
|
599
|
+
async addProvider(provider) {
|
|
600
|
+
this.log(`Adding provider: ${provider.name}`);
|
|
601
|
+
provider.on("message", (message) => {
|
|
602
|
+
this.handleIncomingMessage(message).catch((error) => {
|
|
603
|
+
this.log(`Error handling message: ${error}`);
|
|
604
|
+
console.error("❌ Error in handleIncomingMessage:", error);
|
|
605
|
+
this.emit("error", {
|
|
606
|
+
error,
|
|
607
|
+
message
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
this.providers.set(provider.name, provider);
|
|
612
|
+
this.log(`Provider added: ${provider.name}`);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Create and register an agent
|
|
616
|
+
*/
|
|
617
|
+
createAgent(config) {
|
|
618
|
+
this.log(`Creating agent: ${config.name}`);
|
|
619
|
+
const agent = new Agent(config);
|
|
620
|
+
this.agents.set(agent.id, agent);
|
|
621
|
+
this.log(`Agent created: ${config.name} (${agent.id})`);
|
|
622
|
+
return agent;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Add a skill (MCP server, etc.)
|
|
626
|
+
*/
|
|
627
|
+
async addSkill(skill) {
|
|
628
|
+
this.log(`Adding skill: ${skill.name}`);
|
|
629
|
+
await skill.initialize();
|
|
630
|
+
this.skills.set(skill.name, skill);
|
|
631
|
+
this.log(`Skill added: ${skill.name}`);
|
|
632
|
+
}
|
|
633
|
+
async removeSkill(name) {
|
|
634
|
+
const skill = this.skills.get(name);
|
|
635
|
+
if (skill) {
|
|
636
|
+
await skill.close();
|
|
637
|
+
this.skills.delete(name);
|
|
638
|
+
this.log(`Skill removed: ${name}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Start Operor
|
|
643
|
+
*/
|
|
644
|
+
async start() {
|
|
645
|
+
if (this.isRunning) throw new Error("Operor is already running");
|
|
646
|
+
this.log("Starting Operor...");
|
|
647
|
+
await this.memory.initialize();
|
|
648
|
+
const providerConnections = Array.from(this.providers.values()).map((p) => p.connect());
|
|
649
|
+
await Promise.all(providerConnections);
|
|
650
|
+
this.isRunning = true;
|
|
651
|
+
this.log("✅ Operor started successfully");
|
|
652
|
+
this.emit("started");
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Stop Operor
|
|
656
|
+
*/
|
|
657
|
+
async stop() {
|
|
658
|
+
if (!this.isRunning) return;
|
|
659
|
+
this.log("Stopping Operor...");
|
|
660
|
+
for (const timer of this.batchTimers.values()) clearTimeout(timer);
|
|
661
|
+
this.batchTimers.clear();
|
|
662
|
+
this.messageBatches.clear();
|
|
663
|
+
const providerDisconnections = Array.from(this.providers.values()).map((p) => p.disconnect());
|
|
664
|
+
await Promise.all(providerDisconnections);
|
|
665
|
+
if (this.config.analyticsCollector?.detach) this.config.analyticsCollector.detach(this);
|
|
666
|
+
await this.memory.close();
|
|
667
|
+
this.isRunning = false;
|
|
668
|
+
this.log("Operor stopped");
|
|
669
|
+
this.emit("stopped");
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Handle incoming message from any provider
|
|
673
|
+
*/
|
|
674
|
+
async handleIncomingMessage(message) {
|
|
675
|
+
this.log(`📱 Incoming message from ${message.from}: "${message.text}"`);
|
|
676
|
+
this.emit("message:received", { message });
|
|
677
|
+
const groupJid = message.metadata?.groupJid;
|
|
678
|
+
const batchKey = groupJid ? `${message.provider}:${message.from}:${groupJid}` : `${message.provider}:${message.from}`;
|
|
679
|
+
if (!this.messageBatches.has(batchKey)) this.messageBatches.set(batchKey, []);
|
|
680
|
+
this.messageBatches.get(batchKey).push(message);
|
|
681
|
+
const existingTimer = this.batchTimers.get(batchKey);
|
|
682
|
+
if (existingTimer) clearTimeout(existingTimer);
|
|
683
|
+
const timer = setTimeout(() => {
|
|
684
|
+
this.processBatch(batchKey).catch((error) => {
|
|
685
|
+
this.log(`Error processing batch: ${error}`);
|
|
686
|
+
console.error("❌ Error in processBatch:", error);
|
|
687
|
+
});
|
|
688
|
+
}, this.batchWindowMs);
|
|
689
|
+
this.batchTimers.set(batchKey, timer);
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Process a batch of messages from the same sender
|
|
693
|
+
*/
|
|
694
|
+
async processBatch(batchKey) {
|
|
695
|
+
const messages = this.messageBatches.get(batchKey);
|
|
696
|
+
if (!messages || messages.length === 0) return;
|
|
697
|
+
this.messageBatches.delete(batchKey);
|
|
698
|
+
this.batchTimers.delete(batchKey);
|
|
699
|
+
const firstMessage = messages[0];
|
|
700
|
+
const combinedText = messages.map((m) => m.text).join("\n");
|
|
701
|
+
const combinedMessage = {
|
|
702
|
+
...firstMessage,
|
|
703
|
+
text: combinedText
|
|
704
|
+
};
|
|
705
|
+
if (messages.length > 1) this.log(`📦 Batched ${messages.length} messages: "${combinedText}"`);
|
|
706
|
+
if (this.config.trainingMode?.enabled) {
|
|
707
|
+
const pendingQs = this.pendingQuickstarts.get(combinedMessage.from);
|
|
708
|
+
if (pendingQs) {
|
|
709
|
+
if (await this.handlePendingQuickstart(combinedMessage, pendingQs)) return;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const pendingSkill = this.pendingSkillAdds.get(combinedMessage.from);
|
|
713
|
+
if (pendingSkill) {
|
|
714
|
+
if (await this.handlePendingSkillAdd(combinedMessage, pendingSkill)) return;
|
|
715
|
+
}
|
|
716
|
+
if (this.config.trainingMode?.enabled) {
|
|
717
|
+
const pendingEdit = this.pendingEdits.get(combinedMessage.from);
|
|
718
|
+
if (pendingEdit) {
|
|
719
|
+
if (await this.handlePendingEdit(combinedMessage, pendingEdit)) return;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (this.config.trainingMode?.enabled) {
|
|
723
|
+
const faqKey = `faq_replace_${combinedMessage.from}`;
|
|
724
|
+
const pending = this.pendingFaqReplacements.get(faqKey);
|
|
725
|
+
if (pending && Date.now() < pending.expiresAt) {
|
|
726
|
+
const text = combinedMessage.text.trim().toLowerCase();
|
|
727
|
+
const kb = this.config.kb;
|
|
728
|
+
const provider = this.providers.values().next().value;
|
|
729
|
+
const reply = async (t) => {
|
|
730
|
+
if (provider) await provider.sendMessage(combinedMessage.from, t);
|
|
731
|
+
};
|
|
732
|
+
this.pendingFaqReplacements.delete(faqKey);
|
|
733
|
+
if (text === "yes") {
|
|
734
|
+
await kb.ingestFaq(pending.question, pending.answer, {
|
|
735
|
+
forceReplace: true,
|
|
736
|
+
replaceId: pending.replaceId
|
|
737
|
+
});
|
|
738
|
+
await reply(`FAQ replaced:\nQ: ${pending.question}\nA: ${pending.answer}`);
|
|
739
|
+
} else {
|
|
740
|
+
await kb.ingestFaq(pending.question, pending.answer, { forceReplace: true });
|
|
741
|
+
await reply(`Both FAQs kept. New FAQ added:\nQ: ${pending.question}\nA: ${pending.answer}`);
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
744
|
+
} else if (pending) this.pendingFaqReplacements.delete(faqKey);
|
|
745
|
+
}
|
|
746
|
+
if (this.config.trainingMode?.enabled) {
|
|
747
|
+
const senderPhone = combinedMessage.from;
|
|
748
|
+
const normalizePhone = (p) => p.replace(/^\+/, "");
|
|
749
|
+
const normalizedSender = normalizePhone(senderPhone);
|
|
750
|
+
if (this.config.trainingMode.whitelist.some((w) => normalizePhone(w) === normalizedSender)) {
|
|
751
|
+
if (await this.handleTrainingCommand(combinedMessage)) return;
|
|
752
|
+
if (combinedMessage.text.trim().startsWith("/")) {
|
|
753
|
+
await this.sendMessage(combinedMessage.from, combinedMessage.provider, {
|
|
754
|
+
to: combinedMessage.from,
|
|
755
|
+
text: `Unknown command: ${combinedMessage.text.trim().split(/\s+/)[0]}\nType /help for available commands.`
|
|
756
|
+
});
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
const provider = this.providers.get(combinedMessage.provider);
|
|
763
|
+
if (provider?.sendTypingIndicator) await provider.sendTypingIndicator(combinedMessage.from).catch(() => {});
|
|
764
|
+
const customer = await this.getOrCreateCustomer(combinedMessage);
|
|
765
|
+
const globalHistory = await this.memory.getHistory(customer.id, 5);
|
|
766
|
+
const intentStart = Date.now();
|
|
767
|
+
const intent = await this.intentClassifier.classify(combinedMessage.text, Array.from(this.agents.values()).map((a) => a.config), globalHistory);
|
|
768
|
+
this.log(`🧠 Intent: ${intent.intent} (confidence: ${intent.confidence}) in ${((Date.now() - intentStart) / 1e3).toFixed(1)}s`);
|
|
769
|
+
const agent = this.selectAgent(intent, combinedMessage);
|
|
770
|
+
if (!agent) {
|
|
771
|
+
this.log("⚠️ No agent matched the intent");
|
|
772
|
+
await this.sendMessage(combinedMessage.from, combinedMessage.provider, {
|
|
773
|
+
to: combinedMessage.from,
|
|
774
|
+
text: "Sorry, I couldn't understand that. Could you rephrase?"
|
|
775
|
+
});
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
this.log(`🤖 Agent selected: ${agent.name}`);
|
|
779
|
+
const guardrails = agent.getConfig().guardrails;
|
|
780
|
+
if (guardrails) {
|
|
781
|
+
const inputCheck = new GuardrailEngine().checkInput(combinedMessage.text, guardrails);
|
|
782
|
+
if (!inputCheck.allowed) {
|
|
783
|
+
this.log(`🛡️ Guardrail blocked input: ${inputCheck.reason}`);
|
|
784
|
+
this.emit("guardrail:blocked", {
|
|
785
|
+
message: combinedMessage,
|
|
786
|
+
reason: inputCheck.reason,
|
|
787
|
+
escalate: !!inputCheck.escalate
|
|
788
|
+
});
|
|
789
|
+
const replyText = inputCheck.escalate ? "Let me connect you with a human agent for this." : "I'm not able to help with that topic. Is there something else I can assist you with?";
|
|
790
|
+
await this.sendMessage(combinedMessage.from, combinedMessage.provider, {
|
|
791
|
+
to: combinedMessage.from,
|
|
792
|
+
text: replyText
|
|
793
|
+
});
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const context = {
|
|
798
|
+
customer,
|
|
799
|
+
history: await this.memory.getHistory(customer.id, 50, agent.name),
|
|
800
|
+
currentMessage: combinedMessage,
|
|
801
|
+
intent
|
|
802
|
+
};
|
|
803
|
+
const response = await agent.process(context);
|
|
804
|
+
this.log(`✉️ Response generated in ${response.duration}ms`);
|
|
805
|
+
const fullResponseText = response.text;
|
|
806
|
+
if (guardrails) {
|
|
807
|
+
const guardrailEngine = new GuardrailEngine();
|
|
808
|
+
if (guardrails.maxResponseLength && response.text.length > guardrails.maxResponseLength) {
|
|
809
|
+
const chunks = splitIntoParagraphChunks(response.text, guardrails.maxResponseLength);
|
|
810
|
+
if (chunks.length > 1) {
|
|
811
|
+
this.log(`📨 Splitting long response (${response.text.length}/${guardrails.maxResponseLength}) into ${chunks.length} messages`);
|
|
812
|
+
for (let i = 0; i < chunks.length - 1; i++) await this.sendMessage(combinedMessage.from, combinedMessage.provider, {
|
|
813
|
+
to: combinedMessage.from,
|
|
814
|
+
text: chunks[i]
|
|
815
|
+
});
|
|
816
|
+
response.text = chunks[chunks.length - 1];
|
|
817
|
+
} else response.text = chunks[0];
|
|
818
|
+
}
|
|
819
|
+
const { maxResponseLength: _mrl, ...otherGuardrails } = guardrails;
|
|
820
|
+
if (otherGuardrails.blockedTopics?.length || otherGuardrails.escalationTriggers?.length) {
|
|
821
|
+
const outputCheck = guardrailEngine.checkOutput(response.text, otherGuardrails);
|
|
822
|
+
if (!outputCheck.allowed) {
|
|
823
|
+
this.log(`🛡️ Guardrail blocked output: ${outputCheck.reason}`);
|
|
824
|
+
this.emit("guardrail:output_blocked", {
|
|
825
|
+
message: combinedMessage,
|
|
826
|
+
reason: outputCheck.reason
|
|
827
|
+
});
|
|
828
|
+
response.text = "I apologize, but I'm unable to provide that information.";
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (response.toolCalls) for (const toolCall of response.toolCalls) {
|
|
833
|
+
this.log(`🔧 Tool: ${toolCall.name}(${JSON.stringify(toolCall.params)})`);
|
|
834
|
+
if (toolCall.success) this.log(` ✓ Success (${toolCall.duration}ms)`);
|
|
835
|
+
else this.log(` ✗ Failed: ${toolCall.error}`);
|
|
836
|
+
}
|
|
837
|
+
await this.sendMessage(combinedMessage.from, combinedMessage.provider, {
|
|
838
|
+
to: combinedMessage.from,
|
|
839
|
+
text: response.text,
|
|
840
|
+
...response.mediaBuffer && {
|
|
841
|
+
mediaBuffer: response.mediaBuffer,
|
|
842
|
+
mediaFileName: response.mediaFileName,
|
|
843
|
+
mediaMimeType: response.mediaMimeType
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
if (response.text) this.log(`📤 Sent: "${response.text.substring(0, 100)}${response.text.length > 100 ? "..." : ""}"`);
|
|
847
|
+
for (const msg of messages) await this.memory.addMessage(customer.id, {
|
|
848
|
+
role: "user",
|
|
849
|
+
content: msg.text,
|
|
850
|
+
timestamp: msg.timestamp
|
|
851
|
+
}, agent.name);
|
|
852
|
+
if (this.autoAddAssistantMessages) await this.memory.addMessage(customer.id, {
|
|
853
|
+
role: "assistant",
|
|
854
|
+
content: fullResponseText,
|
|
855
|
+
timestamp: Date.now(),
|
|
856
|
+
toolCalls: response.toolCalls
|
|
857
|
+
}, agent.name);
|
|
858
|
+
this.emit("message:processed", {
|
|
859
|
+
message: combinedMessage,
|
|
860
|
+
response,
|
|
861
|
+
agent: agent.name,
|
|
862
|
+
intent,
|
|
863
|
+
customer,
|
|
864
|
+
toolCalls: response.toolCalls,
|
|
865
|
+
duration: response.duration,
|
|
866
|
+
cost: response.cost
|
|
867
|
+
});
|
|
868
|
+
} catch (error) {
|
|
869
|
+
this.log(`❌ Error processing message: ${error}`);
|
|
870
|
+
this.emit("error", {
|
|
871
|
+
error,
|
|
872
|
+
message: combinedMessage
|
|
873
|
+
});
|
|
874
|
+
await this.sendMessage(combinedMessage.from, combinedMessage.provider, {
|
|
875
|
+
to: combinedMessage.from,
|
|
876
|
+
text: "Sorry, something went wrong. Please try again."
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Get or create customer from message
|
|
882
|
+
*/
|
|
883
|
+
async getOrCreateCustomer(message) {
|
|
884
|
+
let customer = await this.memory.getCustomerByPhone(message.from);
|
|
885
|
+
if (!customer) {
|
|
886
|
+
customer = {
|
|
887
|
+
id: `customer_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
888
|
+
phone: message.from,
|
|
889
|
+
name: message.from,
|
|
890
|
+
firstInteraction: /* @__PURE__ */ new Date(),
|
|
891
|
+
lastInteraction: /* @__PURE__ */ new Date()
|
|
892
|
+
};
|
|
893
|
+
if (message.channel === "whatsapp") customer.whatsappId = message.from;
|
|
894
|
+
await this.memory.upsertCustomer(customer);
|
|
895
|
+
this.log(`👤 New customer: ${customer.id}`);
|
|
896
|
+
} else {
|
|
897
|
+
customer.lastInteraction = /* @__PURE__ */ new Date();
|
|
898
|
+
await this.memory.upsertCustomer(customer);
|
|
899
|
+
}
|
|
900
|
+
return customer;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Handle training commands from whitelisted users.
|
|
904
|
+
* Returns true if the message was a training command, false otherwise.
|
|
905
|
+
*/
|
|
906
|
+
async handleTrainingCommand(message) {
|
|
907
|
+
const text = message.text.trim();
|
|
908
|
+
if (!text.startsWith("/")) return false;
|
|
909
|
+
const parts = text.split(/\s+/);
|
|
910
|
+
let command = parts[0].toLowerCase();
|
|
911
|
+
const args = parts.slice(1);
|
|
912
|
+
const commandAliases = { "/skill": "/skills" };
|
|
913
|
+
if (commandAliases[command]) command = commandAliases[command];
|
|
914
|
+
if (![
|
|
915
|
+
"/teach",
|
|
916
|
+
"/faq",
|
|
917
|
+
"/kb",
|
|
918
|
+
"/test",
|
|
919
|
+
"/status",
|
|
920
|
+
"/stats",
|
|
921
|
+
"/help",
|
|
922
|
+
"/learn-url",
|
|
923
|
+
"/learn-site",
|
|
924
|
+
"/learn-file",
|
|
925
|
+
"/rebuild",
|
|
926
|
+
"/review",
|
|
927
|
+
"/export",
|
|
928
|
+
"/import",
|
|
929
|
+
"/instructions",
|
|
930
|
+
"/identity",
|
|
931
|
+
"/soul",
|
|
932
|
+
"/config",
|
|
933
|
+
"/done",
|
|
934
|
+
"/cancel",
|
|
935
|
+
"/whitelist",
|
|
936
|
+
"/quickstart",
|
|
937
|
+
"/skills",
|
|
938
|
+
"/model",
|
|
939
|
+
"/create-skill",
|
|
940
|
+
"/update-skill",
|
|
941
|
+
"/clear-history"
|
|
942
|
+
].includes(command)) return false;
|
|
943
|
+
this.log(`🎓 Training command: ${command} ${args.join(" ")}`);
|
|
944
|
+
this.emit("training:command", {
|
|
945
|
+
command,
|
|
946
|
+
args,
|
|
947
|
+
message
|
|
948
|
+
});
|
|
949
|
+
const reply = async (text) => {
|
|
950
|
+
await this.sendMessage(message.from, message.provider, {
|
|
951
|
+
to: message.from,
|
|
952
|
+
text
|
|
953
|
+
});
|
|
954
|
+
};
|
|
955
|
+
const kb = this.config.kb;
|
|
956
|
+
if (command === "/help") {
|
|
957
|
+
if (args[0] === "all") {
|
|
958
|
+
const helpLines = [
|
|
959
|
+
"Training commands:",
|
|
960
|
+
" /teach <question> | <answer> — Add FAQ entry",
|
|
961
|
+
" /learn-url <url> — Ingest a webpage into KB",
|
|
962
|
+
" /learn-site <url> — Crawl and ingest a website",
|
|
963
|
+
" /learn-file — Ingest an attached file (send with caption)",
|
|
964
|
+
" /faq list — List all FAQ entries",
|
|
965
|
+
" /faq search <query> — Search FAQ entries",
|
|
966
|
+
" /faq delete <id> — Delete an FAQ entry",
|
|
967
|
+
" /kb list [page] — List ingested documents (URLs, files)",
|
|
968
|
+
" /export — Export KB bundle as YAML file",
|
|
969
|
+
" /import — Import KB bundle from YAML file (send with caption)",
|
|
970
|
+
" /import <url> — Import KB bundle from a URL",
|
|
971
|
+
" /test <message> — Test KB response",
|
|
972
|
+
" /rebuild — Rebuild all KB embeddings (after switching provider)",
|
|
973
|
+
" /status — Show KB stats and bot status",
|
|
974
|
+
" /stats — 7-day performance summary",
|
|
975
|
+
" /stats today — Today's performance",
|
|
976
|
+
" /stats kb — KB-specific analytics",
|
|
977
|
+
" /help — Show quick help",
|
|
978
|
+
" /help all — Show this full help message"
|
|
979
|
+
];
|
|
980
|
+
helpLines.push("", "Agent editor commands:", " /instructions — View instructions summary", " /instructions view [section] — View full or section", " /instructions edit [section] — Edit instructions", " /instructions history — Version history", " /instructions rollback [n] — Revert to version n", " /identity — View identity", " /identity edit — Edit identity", " /soul — View soul", " /soul edit — Edit soul", " /config show — View agent config", " /config set <key> <value> — Set config field", " /config add <key> <value> — Add to array field", " /config remove <key> <value> — Remove from array field", " /done — Finish editing (in edit mode)", " /cancel — Cancel editing");
|
|
981
|
+
if (this.config.copilotHandler) {
|
|
982
|
+
helpLines.push(" /review — Review unanswered questions (Copilot)");
|
|
983
|
+
helpLines.push(" /review help — See all review commands");
|
|
984
|
+
}
|
|
985
|
+
helpLines.push("", "Admin commands:", " /whitelist — View whitelisted phone numbers", " /whitelist add <phone> — Add a phone number", " /whitelist remove <phone> — Remove a phone number", " /clear-history — Clear your conversation history");
|
|
986
|
+
helpLines.push("", "Skills:", " /skills — List registered skills and tools", " /skills info <name> — Show skill details", " /skills catalog [category] — Browse available skills", " /skills add <name> — Install a skill from the catalog", " /skills remove <name> — Remove a skill", " /create-skill <name> | <instructions> — Create a prompt skill", " /update-skill <name> | <instructions> — Update a prompt skill");
|
|
987
|
+
helpLines.push("", "Model management:", " /model — Show current LLM provider and model", " /model list — List available models per provider", " /model set <provider/model> — Switch LLM model", " /model key <provider> <key> — Add API key for a provider");
|
|
988
|
+
await reply(helpLines.join("\n"));
|
|
989
|
+
} else await reply([
|
|
990
|
+
"Quick start:",
|
|
991
|
+
" /teach <question> | <answer> — Add FAQ entry",
|
|
992
|
+
" /test <message> — Test how your bot responds",
|
|
993
|
+
" /status — Show bot status and KB stats",
|
|
994
|
+
" /learn-url <url> — Ingest a webpage",
|
|
995
|
+
" /learn-file — Ingest an attached document",
|
|
996
|
+
" /skills — List registered skills and tools",
|
|
997
|
+
" /skills catalog — Browse available skills",
|
|
998
|
+
" /quickstart — Guided first FAQ setup",
|
|
999
|
+
" /help all — See all commands",
|
|
1000
|
+
"",
|
|
1001
|
+
"Example: /teach What are your hours? | We're open 9am-5pm Mon-Fri"
|
|
1002
|
+
].join("\n"));
|
|
1003
|
+
return true;
|
|
1004
|
+
}
|
|
1005
|
+
if (command === "/quickstart") {
|
|
1006
|
+
if (!kb) {
|
|
1007
|
+
await reply("Knowledge Base is not enabled. Set KB_ENABLED=true to use /quickstart.");
|
|
1008
|
+
return true;
|
|
1009
|
+
}
|
|
1010
|
+
this.pendingQuickstarts.set(message.from, {
|
|
1011
|
+
step: "question",
|
|
1012
|
+
createdAt: Date.now()
|
|
1013
|
+
});
|
|
1014
|
+
await reply("Let's teach your agent its first FAQ!\n\nStep 1/3: What question do your customers ask?\n\n(Send the question, or /cancel to abort)");
|
|
1015
|
+
return true;
|
|
1016
|
+
}
|
|
1017
|
+
if (command === "/whitelist") {
|
|
1018
|
+
await this.handleWhitelistCommand(message, args, reply);
|
|
1019
|
+
return true;
|
|
1020
|
+
}
|
|
1021
|
+
if (command === "/skills") {
|
|
1022
|
+
await this.handleSkillsCommand(message, args, reply);
|
|
1023
|
+
return true;
|
|
1024
|
+
}
|
|
1025
|
+
if (command === "/create-skill") {
|
|
1026
|
+
await this.handleCreateSkillCommand(message, args, reply);
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
1029
|
+
if (command === "/update-skill") {
|
|
1030
|
+
await this.handleUpdateSkillCommand(message, args, reply);
|
|
1031
|
+
return true;
|
|
1032
|
+
}
|
|
1033
|
+
if (command === "/instructions" || command === "/identity" || command === "/soul" || command === "/config" || command === "/done" || command === "/cancel") {
|
|
1034
|
+
try {
|
|
1035
|
+
if (command === "/instructions") await this.handleInstructionsCommand(message, args, reply);
|
|
1036
|
+
else if (command === "/identity") await this.handleFieldViewOrEdit("identity", message, args, reply);
|
|
1037
|
+
else if (command === "/soul") await this.handleFieldViewOrEdit("soul", message, args, reply);
|
|
1038
|
+
else if (command === "/config") await this.handleConfigCommand(message, args, reply);
|
|
1039
|
+
else await reply("No active edit session. Use /instructions edit, /identity edit, or /soul edit to start.");
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
this.log(`Training command error: ${error.message}`);
|
|
1042
|
+
await reply(`Error: ${error.message}`);
|
|
1043
|
+
}
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
1046
|
+
if (command === "/model") {
|
|
1047
|
+
try {
|
|
1048
|
+
await this.handleModelCommand(args, reply);
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
this.log(`Training command error: ${error.message}`);
|
|
1051
|
+
await reply(`Error: ${error.message}`);
|
|
1052
|
+
}
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
if (command === "/clear-history") {
|
|
1056
|
+
if (!this.memory?.clearHistory) {
|
|
1057
|
+
await reply("Clear history is not supported by the current memory store.");
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1060
|
+
try {
|
|
1061
|
+
const customer = await this.memory.getCustomerByPhone?.(message.from);
|
|
1062
|
+
if (!customer) {
|
|
1063
|
+
await reply("No conversation history found for your number.");
|
|
1064
|
+
return true;
|
|
1065
|
+
}
|
|
1066
|
+
const { deletedCount } = await this.memory.clearHistory(customer.id);
|
|
1067
|
+
await reply(`Cleared ${deletedCount} message(s) from your conversation history.`);
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
this.log(`Training command error: ${error.message}`);
|
|
1070
|
+
await reply(`Error clearing history: ${error.message}`);
|
|
1071
|
+
}
|
|
1072
|
+
return true;
|
|
1073
|
+
}
|
|
1074
|
+
if (!kb) {
|
|
1075
|
+
await reply("Knowledge Base is not enabled. Set KB_ENABLED=true and configure embedding settings to use training commands.");
|
|
1076
|
+
return true;
|
|
1077
|
+
}
|
|
1078
|
+
if (command === "/review") {
|
|
1079
|
+
if (this.config.copilotHandler) await this.config.copilotHandler.handleCommand(command, args.join(" "), message.from, reply);
|
|
1080
|
+
else await reply("Training Copilot is not available. If COPILOT_ENABLED=true is set, check startup logs for initialization errors.");
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
try {
|
|
1084
|
+
if (command === "/teach") {
|
|
1085
|
+
const raw = args.join(" ");
|
|
1086
|
+
const sepIndex = raw.indexOf("|");
|
|
1087
|
+
if (sepIndex === -1) {
|
|
1088
|
+
await reply("Usage: /teach <question> | <answer>");
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
1091
|
+
const stripQuotes = (s) => s.replace(/^["']|["']$/g, "");
|
|
1092
|
+
const question = stripQuotes(raw.slice(0, sepIndex).trim());
|
|
1093
|
+
const answer = stripQuotes(raw.slice(sepIndex + 1).trim());
|
|
1094
|
+
if (!question || !answer) {
|
|
1095
|
+
await reply("Usage: /teach <question> | <answer>");
|
|
1096
|
+
return true;
|
|
1097
|
+
}
|
|
1098
|
+
const doc = await kb.ingestFaq(question, answer);
|
|
1099
|
+
if (doc.existingMatch) {
|
|
1100
|
+
const match = doc.existingMatch;
|
|
1101
|
+
const pendingKey = `faq_replace_${msg.from}`;
|
|
1102
|
+
this.pendingFaqReplacements.set(pendingKey, {
|
|
1103
|
+
question,
|
|
1104
|
+
answer,
|
|
1105
|
+
replaceId: match.id,
|
|
1106
|
+
expiresAt: Date.now() + 12e4
|
|
1107
|
+
});
|
|
1108
|
+
await reply(`Similar FAQ already exists (${(match.score * 100).toFixed(0)}% match):\nQ: ${match.question}\nA: ${match.answer}\n\nReply "yes" to replace it with your new answer, or "no" to keep both.`);
|
|
1109
|
+
return true;
|
|
1110
|
+
}
|
|
1111
|
+
await reply(`FAQ added (${doc.id.slice(0, 8)}...):\nQ: ${question}\nA: ${answer}`);
|
|
1112
|
+
if (/\b(your name|you are\s+\w+|you work|you're\s+\w+)\b/i.test(answer)) await reply("⚠️ Heads up: This answer uses \"you/your\" which may sound odd coming from the bot. Consider rephrasing (e.g., \"my name is...\" instead of \"your name is...\").");
|
|
1113
|
+
} else if (command === "/faq") {
|
|
1114
|
+
const subcommand = args[0]?.toLowerCase();
|
|
1115
|
+
if (subcommand === "list") {
|
|
1116
|
+
const faqs = (await kb.listDocuments()).filter((d) => d.sourceType === "faq");
|
|
1117
|
+
if (faqs.length === 0) await reply("No FAQ entries found.");
|
|
1118
|
+
else {
|
|
1119
|
+
const lines = faqs.map((d) => `[${d.id.slice(0, 8)}] ${d.title || d.content.slice(0, 60)}`);
|
|
1120
|
+
await reply(`FAQ entries (${faqs.length}):\n${lines.join("\n")}`);
|
|
1121
|
+
}
|
|
1122
|
+
} else if (subcommand === "search") {
|
|
1123
|
+
const query = args.slice(1).join(" ");
|
|
1124
|
+
if (!query) {
|
|
1125
|
+
await reply("Usage: /faq search <query>");
|
|
1126
|
+
return true;
|
|
1127
|
+
}
|
|
1128
|
+
const result = await kb.retrieve(query);
|
|
1129
|
+
if (result.results.length === 0) await reply(`No results for "${query}".`);
|
|
1130
|
+
else await reply(`Search results for "${query}":\n${result.results.slice(0, 5).map((r, i) => `${i + 1}. [${r.score.toFixed(2)}] ${r.document.title || r.chunk.content.slice(0, 60)}`).join("\n")}`);
|
|
1131
|
+
} else if (subcommand === "delete") {
|
|
1132
|
+
const id = args[1];
|
|
1133
|
+
if (!id) {
|
|
1134
|
+
await reply("Usage: /faq delete <id>");
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
const match = (await kb.listDocuments()).find((d) => d.id === id || d.id.startsWith(id));
|
|
1138
|
+
if (!match) await reply(`No document found with ID starting with "${id}".`);
|
|
1139
|
+
else {
|
|
1140
|
+
await kb.deleteDocument(match.id);
|
|
1141
|
+
await reply(`Deleted: ${match.title || match.id.slice(0, 8)}`);
|
|
1142
|
+
}
|
|
1143
|
+
} else await reply("Usage: /faq list | /faq search <query> | /faq delete <id>");
|
|
1144
|
+
} else if (command === "/kb") if (args[0]?.toLowerCase() === "list") {
|
|
1145
|
+
const page = parseInt(args[1]) || 1;
|
|
1146
|
+
const PAGE_SIZE = 10;
|
|
1147
|
+
const nonFaq = (await kb.listDocuments()).filter((d) => d.sourceType !== "faq");
|
|
1148
|
+
if (nonFaq.length === 0) await reply("No KB documents found. Use /learn-url, /learn-site, or /learn-file to add content.");
|
|
1149
|
+
else {
|
|
1150
|
+
const totalPages = Math.ceil(nonFaq.length / PAGE_SIZE);
|
|
1151
|
+
const safePage = Math.max(1, Math.min(page, totalPages));
|
|
1152
|
+
const lines = nonFaq.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE).map((d) => {
|
|
1153
|
+
const label = d.title || d.sourceUrl || d.fileName || d.content.slice(0, 60);
|
|
1154
|
+
const truncLabel = label.length > 60 ? label.slice(0, 57) + "..." : label;
|
|
1155
|
+
const source = d.sourceUrl ? `\n ${d.sourceUrl.length > 60 ? d.sourceUrl.slice(0, 57) + "..." : d.sourceUrl}` : d.fileName ? `\n ${d.fileName}` : "";
|
|
1156
|
+
return `[${d.id.slice(0, 8)}] ${d.sourceType} — ${truncLabel}${source}`;
|
|
1157
|
+
});
|
|
1158
|
+
let header = `KB Documents (${nonFaq.length} total`;
|
|
1159
|
+
if (totalPages > 1) header += `, page ${safePage}/${totalPages}`;
|
|
1160
|
+
header += "):";
|
|
1161
|
+
let msg = `${header}\n${lines.join("\n")}`;
|
|
1162
|
+
if (totalPages > 1 && safePage < totalPages) msg += `\n\nSend /kb list ${safePage + 1} for next page.`;
|
|
1163
|
+
await reply(msg);
|
|
1164
|
+
}
|
|
1165
|
+
} else await reply("Unknown /kb command. Available: /kb list");
|
|
1166
|
+
else if (command === "/test") {
|
|
1167
|
+
const query = args.join(" ");
|
|
1168
|
+
if (!query) {
|
|
1169
|
+
await reply("Usage: /test <message>");
|
|
1170
|
+
return true;
|
|
1171
|
+
}
|
|
1172
|
+
const result = await kb.retrieve(query);
|
|
1173
|
+
if (result.results.length === 0) await reply(`No KB match for: "${query}"`);
|
|
1174
|
+
else {
|
|
1175
|
+
const top = result.results[0];
|
|
1176
|
+
await reply(`Test: "${query}"\nMatch type: ${result.isFaqMatch ? "FAQ direct match" : "KB search"}\nScore: ${top.score.toFixed(2)}\nContent: ${top.chunk.content.slice(0, 200)}`);
|
|
1177
|
+
}
|
|
1178
|
+
} else if (command === "/stats") {
|
|
1179
|
+
const analyticsStore = this.getAnalyticsStore();
|
|
1180
|
+
if (!analyticsStore) {
|
|
1181
|
+
await reply("Analytics is not enabled. Set ANALYTICS_ENABLED=true to track metrics.");
|
|
1182
|
+
return true;
|
|
1183
|
+
}
|
|
1184
|
+
const subcommand = args[0]?.toLowerCase();
|
|
1185
|
+
if (subcommand === "kb") {
|
|
1186
|
+
const now = Date.now();
|
|
1187
|
+
const range = {
|
|
1188
|
+
from: now - 10080 * 60 * 1e3,
|
|
1189
|
+
to: now
|
|
1190
|
+
};
|
|
1191
|
+
const kbStats = await analyticsStore.getKbStats(range);
|
|
1192
|
+
const total = kbStats.totalQueries;
|
|
1193
|
+
const lines = [
|
|
1194
|
+
"📚 *KB Analytics (last 7 days)*",
|
|
1195
|
+
"",
|
|
1196
|
+
`🔍 ${total} total queries`
|
|
1197
|
+
];
|
|
1198
|
+
if (total > 0) {
|
|
1199
|
+
const faqPct = kbStats.faqHitRate.toFixed(0);
|
|
1200
|
+
const hybridPct = ((total - kbStats.faqHits) / total * 100).toFixed(0);
|
|
1201
|
+
const noMatchPct = ((kbStats.topMisses?.reduce((s, m) => s + m.count, 0) || 0) / total * 100).toFixed(0);
|
|
1202
|
+
lines.push(`⚡ FAQ fast-path: ${faqPct}%`);
|
|
1203
|
+
lines.push(`🔄 Hybrid search: ${hybridPct}%`);
|
|
1204
|
+
lines.push(`❌ No match: ${noMatchPct}%`);
|
|
1205
|
+
lines.push(`📊 Avg top score: ${kbStats.avgTopScore.toFixed(2)}`);
|
|
1206
|
+
}
|
|
1207
|
+
if (kbStats.topFaqHits && kbStats.topFaqHits.length > 0) {
|
|
1208
|
+
lines.push("");
|
|
1209
|
+
lines.push("🎯 *Top FAQ hits:*");
|
|
1210
|
+
for (const hit of kbStats.topFaqHits.slice(0, 5)) lines.push(` ${hit.count}x — ${hit.intent}`);
|
|
1211
|
+
}
|
|
1212
|
+
if (kbStats.topMisses && kbStats.topMisses.length > 0) {
|
|
1213
|
+
lines.push("");
|
|
1214
|
+
lines.push("❓ *Top misses:*");
|
|
1215
|
+
for (const miss of kbStats.topMisses.slice(0, 5)) lines.push(` ${miss.count}x [${miss.avgScore.toFixed(2)}] — ${miss.intent}`);
|
|
1216
|
+
}
|
|
1217
|
+
await reply(lines.join("\n"));
|
|
1218
|
+
} else {
|
|
1219
|
+
const isToday = subcommand === "today";
|
|
1220
|
+
const now = Date.now();
|
|
1221
|
+
const range = isToday ? {
|
|
1222
|
+
from: new Date((/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0)).getTime(),
|
|
1223
|
+
to: now
|
|
1224
|
+
} : {
|
|
1225
|
+
from: now - 10080 * 60 * 1e3,
|
|
1226
|
+
to: now
|
|
1227
|
+
};
|
|
1228
|
+
const summary = await analyticsStore.getSummary(range);
|
|
1229
|
+
const label = isToday ? "today" : "last 7 days";
|
|
1230
|
+
const noAnswerCount = Math.round(summary.noAnswerPct / 100 * summary.totalMessages);
|
|
1231
|
+
const avgPerDay = Math.round(summary.avgPerDay);
|
|
1232
|
+
const lines = [
|
|
1233
|
+
`📊 *Bot Performance (${label})*`,
|
|
1234
|
+
"",
|
|
1235
|
+
`💬 ${summary.totalMessages} messages from ${summary.uniqueCustomers} customers`
|
|
1236
|
+
];
|
|
1237
|
+
if (!isToday && summary.totalMessages > 0 && summary.peakDay) {
|
|
1238
|
+
const peakLabel = this.formatDayName(summary.peakDay);
|
|
1239
|
+
lines.push(`📈 Avg ${avgPerDay}/day — busiest: ${peakLabel}`);
|
|
1240
|
+
}
|
|
1241
|
+
lines.push("");
|
|
1242
|
+
lines.push(`✅ ${summary.kbAnsweredPct.toFixed(0)}% answered by KB`);
|
|
1243
|
+
lines.push(`🤖 ${summary.llmFallbackPct.toFixed(0)}% needed LLM`);
|
|
1244
|
+
lines.push(`❌ ${summary.noAnswerPct.toFixed(0)}% couldn't answer (${noAnswerCount} msgs)`);
|
|
1245
|
+
lines.push("");
|
|
1246
|
+
lines.push(`⚡ Avg response: ${(summary.avgResponseTime / 1e3).toFixed(1)}s`);
|
|
1247
|
+
lines.push(`🎯 FAQ hit rate: ${summary.faqHitRate.toFixed(0)}%`);
|
|
1248
|
+
lines.push("");
|
|
1249
|
+
lines.push(`👥 ${summary.newCustomers} new customers${isToday ? " today" : " this week"}`);
|
|
1250
|
+
lines.push(`🔄 ${summary.returningCustomers} returning customers`);
|
|
1251
|
+
if (summary.pendingReviews > 0) {
|
|
1252
|
+
lines.push("");
|
|
1253
|
+
lines.push(`📋 ${summary.pendingReviews} questions pending review`);
|
|
1254
|
+
lines.push("→ /review to start teaching");
|
|
1255
|
+
}
|
|
1256
|
+
await reply(lines.join("\n"));
|
|
1257
|
+
}
|
|
1258
|
+
} else if (command === "/status") {
|
|
1259
|
+
const stats = await kb.getStats();
|
|
1260
|
+
const statusLines = [
|
|
1261
|
+
"📦 *Knowledge Base Status:*",
|
|
1262
|
+
` Documents: ${stats.documentCount}`,
|
|
1263
|
+
` Chunks: ${stats.chunkCount}`,
|
|
1264
|
+
` Embedding dimensions: ${stats.embeddingDimensions}`,
|
|
1265
|
+
` DB size: ${(stats.dbSizeBytes / 1024).toFixed(1)} KB`
|
|
1266
|
+
];
|
|
1267
|
+
const analyticsStore = this.getAnalyticsStore();
|
|
1268
|
+
if (analyticsStore) try {
|
|
1269
|
+
const summary = await analyticsStore.getSummary();
|
|
1270
|
+
statusLines.push("");
|
|
1271
|
+
statusLines.push("🤖 *Bot Status:*");
|
|
1272
|
+
statusLines.push(` Total messages processed: ${summary.totalMessages}`);
|
|
1273
|
+
statusLines.push(` Unique customers: ${summary.uniqueCustomers}`);
|
|
1274
|
+
if (summary.pendingReviews > 0) statusLines.push(` Pending reviews: ${summary.pendingReviews}`);
|
|
1275
|
+
const uptime = process.uptime();
|
|
1276
|
+
const uptimeStr = uptime >= 86400 ? `${Math.floor(uptime / 86400)}d ${Math.floor(uptime % 86400 / 3600)}h` : uptime >= 3600 ? `${Math.floor(uptime / 3600)}h ${Math.floor(uptime % 3600 / 60)}m` : `${Math.floor(uptime / 60)}m`;
|
|
1277
|
+
statusLines.push(` Uptime: ${uptimeStr}`);
|
|
1278
|
+
} catch {}
|
|
1279
|
+
await reply(statusLines.join("\n"));
|
|
1280
|
+
} else if (command === "/learn-url") {
|
|
1281
|
+
const url = args[0];
|
|
1282
|
+
if (!url) {
|
|
1283
|
+
await reply("Usage: /learn-url <url>");
|
|
1284
|
+
return true;
|
|
1285
|
+
}
|
|
1286
|
+
if (!kb.ingestUrl) {
|
|
1287
|
+
await reply("URL ingestion is not available. Ensure the KB runtime supports ingestUrl.");
|
|
1288
|
+
return true;
|
|
1289
|
+
}
|
|
1290
|
+
try {
|
|
1291
|
+
new URL(url);
|
|
1292
|
+
} catch {
|
|
1293
|
+
await reply(`Invalid URL: ${url}`);
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
await reply(`Ingesting URL: ${url}...`);
|
|
1297
|
+
const result = await kb.ingestUrl(url);
|
|
1298
|
+
const countLabel = result.faqCount ? `${result.faqCount} Q&A pairs` : `${result.chunks} chunks`;
|
|
1299
|
+
await reply(`Done! Added "${result.title || url}" (${countLabel}).`);
|
|
1300
|
+
} else if (command === "/learn-site") {
|
|
1301
|
+
const url = args[0];
|
|
1302
|
+
if (!url) {
|
|
1303
|
+
await reply("Usage: /learn-site <url>");
|
|
1304
|
+
return true;
|
|
1305
|
+
}
|
|
1306
|
+
if (!kb.ingestSite) {
|
|
1307
|
+
await reply("Site ingestion is not available. Ensure the KB runtime supports ingestSite.");
|
|
1308
|
+
return true;
|
|
1309
|
+
}
|
|
1310
|
+
try {
|
|
1311
|
+
new URL(url);
|
|
1312
|
+
} catch {
|
|
1313
|
+
await reply(`Invalid URL: ${url}`);
|
|
1314
|
+
return true;
|
|
1315
|
+
}
|
|
1316
|
+
await reply(`Crawling site: ${url} (this may take a while)...`);
|
|
1317
|
+
let lastProgressUpdate = 0;
|
|
1318
|
+
const result = await kb.ingestSite(url, {
|
|
1319
|
+
maxDepth: 2,
|
|
1320
|
+
maxPages: 50,
|
|
1321
|
+
onProgress: (crawled, total, pageUrl) => {
|
|
1322
|
+
if (crawled - lastProgressUpdate >= 5) {
|
|
1323
|
+
lastProgressUpdate = crawled;
|
|
1324
|
+
reply(`Crawling page ${crawled}/${total}: ${pageUrl.length > 60 ? pageUrl.slice(0, 57) + "..." : pageUrl}`);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
await reply(`Done! Crawled ${result.documents} pages (${result.chunks} total chunks).`);
|
|
1329
|
+
} else if (command === "/learn-file") {
|
|
1330
|
+
if (!kb.ingestFile) {
|
|
1331
|
+
await reply("File ingestion is not available. Ensure the KB runtime supports ingestFile.");
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1334
|
+
if (!message.mediaBuffer || !message.mediaFileName) {
|
|
1335
|
+
await reply("No file attached. Send a document (PDF, DOCX, TXT, CSV, etc.) with /learn-file as the caption.");
|
|
1336
|
+
return true;
|
|
1337
|
+
}
|
|
1338
|
+
await reply(`Ingesting file: ${message.mediaFileName}...`);
|
|
1339
|
+
const result = await kb.ingestFile(message.mediaBuffer, message.mediaFileName);
|
|
1340
|
+
await reply(`Done! Added "${result.title || message.mediaFileName}" (${result.chunks} chunks).`);
|
|
1341
|
+
} else if (command === "/rebuild") {
|
|
1342
|
+
if (!kb.rebuild) {
|
|
1343
|
+
await reply("Rebuild is not available. Ensure the KB runtime supports rebuild.");
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1346
|
+
await reply("Rebuilding KB embeddings... This may take a while.");
|
|
1347
|
+
const result = await kb.rebuild();
|
|
1348
|
+
await reply([
|
|
1349
|
+
"Rebuild complete!",
|
|
1350
|
+
` Documents: ${result.documentsRebuilt}`,
|
|
1351
|
+
` Chunks: ${result.chunksRebuilt}`,
|
|
1352
|
+
` Old dimensions: ${result.oldDimensions}`,
|
|
1353
|
+
` New dimensions: ${result.newDimensions}`
|
|
1354
|
+
].join("\n"));
|
|
1355
|
+
} else if (command === "/export") await this.handleExportCommand(message, args);
|
|
1356
|
+
else if (command === "/import") await this.handleImportCommand(message, args);
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
this.log(`Training command error: ${error.message}`);
|
|
1359
|
+
await reply(`Error: ${error.message}`);
|
|
1360
|
+
}
|
|
1361
|
+
return true;
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Handle /export command — generates YAML bundle and sends as file attachment
|
|
1365
|
+
*/
|
|
1366
|
+
async handleExportCommand(message, args) {
|
|
1367
|
+
const kb = this.config.kb;
|
|
1368
|
+
const reply = async (text) => {
|
|
1369
|
+
await this.sendMessage(message.from, message.provider, {
|
|
1370
|
+
to: message.from,
|
|
1371
|
+
text
|
|
1372
|
+
});
|
|
1373
|
+
};
|
|
1374
|
+
const replyWithFile = async (text, buffer, fileName, mimeType) => {
|
|
1375
|
+
await this.sendMessage(message.from, message.provider, {
|
|
1376
|
+
to: message.from,
|
|
1377
|
+
text,
|
|
1378
|
+
mediaBuffer: buffer,
|
|
1379
|
+
mediaFileName: fileName,
|
|
1380
|
+
mediaMimeType: mimeType
|
|
1381
|
+
});
|
|
1382
|
+
};
|
|
1383
|
+
try {
|
|
1384
|
+
await reply("Generating export bundle...");
|
|
1385
|
+
const docs = await kb.listDocuments();
|
|
1386
|
+
const faqs = docs.filter((d) => d.sourceType === "faq");
|
|
1387
|
+
const nonFaqDocs = docs.filter((d) => d.sourceType !== "faq");
|
|
1388
|
+
const faqEntries = faqs.map((d) => {
|
|
1389
|
+
const content = d.content;
|
|
1390
|
+
const aIndex = content.indexOf("\nA: ");
|
|
1391
|
+
if (aIndex !== -1 && content.startsWith("Q: ")) return {
|
|
1392
|
+
question: content.slice(3, aIndex).trim(),
|
|
1393
|
+
answer: content.slice(aIndex + 4).trim()
|
|
1394
|
+
};
|
|
1395
|
+
return {
|
|
1396
|
+
question: (d.title || content).trim(),
|
|
1397
|
+
answer: content.trim()
|
|
1398
|
+
};
|
|
1399
|
+
});
|
|
1400
|
+
const documentRefs = nonFaqDocs.map((d) => {
|
|
1401
|
+
const ref = {
|
|
1402
|
+
title: d.title || void 0,
|
|
1403
|
+
sourceType: d.sourceType
|
|
1404
|
+
};
|
|
1405
|
+
if (d.sourceUrl) ref.sourceUrl = d.sourceUrl;
|
|
1406
|
+
if (d.fileName) ref.fileName = d.fileName;
|
|
1407
|
+
return ref;
|
|
1408
|
+
});
|
|
1409
|
+
const agentConfig = Array.from(this.agents.values())[0]?.getConfig();
|
|
1410
|
+
const bundle = {
|
|
1411
|
+
version: 1,
|
|
1412
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1413
|
+
};
|
|
1414
|
+
if (agentConfig) {
|
|
1415
|
+
const agentSection = { name: agentConfig.name };
|
|
1416
|
+
if (agentConfig.purpose) agentSection.purpose = agentConfig.purpose;
|
|
1417
|
+
if (agentConfig.triggers?.length) agentSection.triggers = agentConfig.triggers;
|
|
1418
|
+
if (agentConfig.channels?.length) agentSection.channels = agentConfig.channels;
|
|
1419
|
+
if (agentConfig.skills?.length) agentSection.skills = agentConfig.skills;
|
|
1420
|
+
if (agentConfig.knowledgeBase != null) agentSection.knowledgeBase = agentConfig.knowledgeBase;
|
|
1421
|
+
if (agentConfig.priority != null) agentSection.priority = agentConfig.priority;
|
|
1422
|
+
if (agentConfig.escalateTo) agentSection.escalateTo = agentConfig.escalateTo;
|
|
1423
|
+
if (agentConfig._rawInstructions) agentSection.instructions = agentConfig._rawInstructions;
|
|
1424
|
+
if (agentConfig._rawIdentity) agentSection.identity = agentConfig._rawIdentity;
|
|
1425
|
+
if (agentConfig._rawSoul) agentSection.soul = agentConfig._rawSoul;
|
|
1426
|
+
bundle.agent = agentSection;
|
|
1427
|
+
if (agentConfig.guardrails) {
|
|
1428
|
+
const g = {};
|
|
1429
|
+
if (agentConfig.guardrails.blockedTopics?.length) g.blockedTopics = agentConfig.guardrails.blockedTopics;
|
|
1430
|
+
if (agentConfig.guardrails.systemRules?.length) g.systemRules = agentConfig.guardrails.systemRules;
|
|
1431
|
+
if (agentConfig.guardrails.escalationTriggers?.length) g.escalationTriggers = agentConfig.guardrails.escalationTriggers;
|
|
1432
|
+
if (agentConfig.guardrails.maxResponseLength) g.maxResponseLength = agentConfig.guardrails.maxResponseLength;
|
|
1433
|
+
if (Object.keys(g).length > 0) bundle.guardrails = g;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
bundle.faqs = faqEntries;
|
|
1437
|
+
if (documentRefs.length > 0) bundle.documents = documentRefs;
|
|
1438
|
+
const promptSkills = Array.from(this.skills.values()).filter((s) => typeof s.getContent === "function").map((s) => {
|
|
1439
|
+
const cfg = s.getConfig?.() || {};
|
|
1440
|
+
const entry = {
|
|
1441
|
+
name: s.name,
|
|
1442
|
+
content: s.getContent()
|
|
1443
|
+
};
|
|
1444
|
+
if (cfg.summary) entry.summary = cfg.summary;
|
|
1445
|
+
if (cfg.tags?.length) entry.tags = cfg.tags;
|
|
1446
|
+
if (cfg.author) entry.author = cfg.author;
|
|
1447
|
+
return entry;
|
|
1448
|
+
});
|
|
1449
|
+
if (promptSkills.length > 0) bundle.promptSkills = promptSkills;
|
|
1450
|
+
const yamlStr = "# operor-export-v1\n" + yaml.dump(bundle, {
|
|
1451
|
+
lineWidth: -1,
|
|
1452
|
+
noRefs: true
|
|
1453
|
+
});
|
|
1454
|
+
const yamlBuffer = Buffer.from(yamlStr, "utf-8");
|
|
1455
|
+
const fileName = `operor-export-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.yaml`;
|
|
1456
|
+
if (args[0]?.toLowerCase() === "gist") {
|
|
1457
|
+
const token = process.env.GITHUB_GIST_TOKEN;
|
|
1458
|
+
if (!token) {
|
|
1459
|
+
await reply("GitHub token not configured. Set GITHUB_GIST_TOKEN env var to use /export gist.");
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
try {
|
|
1463
|
+
const res = await fetch("https://api.github.com/gists", {
|
|
1464
|
+
method: "POST",
|
|
1465
|
+
headers: {
|
|
1466
|
+
"Authorization": `token ${token}`,
|
|
1467
|
+
"Content-Type": "application/json",
|
|
1468
|
+
"Accept": "application/vnd.github.v3+json"
|
|
1469
|
+
},
|
|
1470
|
+
body: JSON.stringify({
|
|
1471
|
+
description: `Operor Export - ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`,
|
|
1472
|
+
public: false,
|
|
1473
|
+
files: { [fileName]: { content: yamlStr } }
|
|
1474
|
+
})
|
|
1475
|
+
});
|
|
1476
|
+
if (!res.ok) {
|
|
1477
|
+
const errText = await res.text();
|
|
1478
|
+
await reply(`GitHub Gist creation failed (${res.status}): ${errText.slice(0, 200)}`);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
const gist = await res.json();
|
|
1482
|
+
const rawUrl = gist.files[fileName]?.raw_url || gist.html_url;
|
|
1483
|
+
await reply([
|
|
1484
|
+
`Export published to GitHub Gist!`,
|
|
1485
|
+
`${faqEntries.length} FAQs, ${documentRefs.length} document references.`,
|
|
1486
|
+
``,
|
|
1487
|
+
`View: ${gist.html_url}`,
|
|
1488
|
+
`Import URL: ${rawUrl}`,
|
|
1489
|
+
``,
|
|
1490
|
+
`Share this URL with others: /import ${rawUrl}`
|
|
1491
|
+
].join("\n"));
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
await reply(`Error creating GitHub Gist: ${err.message}`);
|
|
1494
|
+
}
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
await replyWithFile(`Export complete! ${faqEntries.length} FAQs, ${documentRefs.length} document references.`, yamlBuffer, fileName, "text/yaml");
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
this.log(`Export error: ${error.message}`);
|
|
1500
|
+
await reply(`Error: ${error.message}`);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Handle /import command — imports YAML bundle from file attachment or URL
|
|
1505
|
+
*/
|
|
1506
|
+
async handleImportCommand(message, args) {
|
|
1507
|
+
const kb = this.config.kb;
|
|
1508
|
+
const reply = async (text) => {
|
|
1509
|
+
await this.sendMessage(message.from, message.provider, {
|
|
1510
|
+
to: message.from,
|
|
1511
|
+
text
|
|
1512
|
+
});
|
|
1513
|
+
};
|
|
1514
|
+
try {
|
|
1515
|
+
let yamlStr = null;
|
|
1516
|
+
if (message.mediaBuffer && message.mediaFileName) yamlStr = message.mediaBuffer.toString("utf-8");
|
|
1517
|
+
else if (args[0]) {
|
|
1518
|
+
const url = args[0];
|
|
1519
|
+
try {
|
|
1520
|
+
new URL(url);
|
|
1521
|
+
} catch {
|
|
1522
|
+
await reply(`Invalid URL: ${url}`);
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
await reply(`Fetching bundle from ${url}...`);
|
|
1526
|
+
const res = await fetch(url);
|
|
1527
|
+
if (!res.ok) {
|
|
1528
|
+
await reply(`Failed to fetch URL (${res.status}): ${res.statusText}`);
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
yamlStr = await res.text();
|
|
1532
|
+
} else {
|
|
1533
|
+
await reply("Usage: /import <url> — or send a YAML file with /import as caption.");
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
let bundle;
|
|
1537
|
+
try {
|
|
1538
|
+
bundle = yaml.load(yamlStr);
|
|
1539
|
+
} catch (err) {
|
|
1540
|
+
await reply(`Invalid YAML: ${err.message}`);
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
if (!bundle || typeof bundle !== "object") {
|
|
1544
|
+
await reply("Invalid bundle: expected a YAML object.");
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
if (bundle.version !== 1) {
|
|
1548
|
+
await reply(`Unsupported bundle version: ${bundle.version}. Expected version 1.`);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
const faqs = bundle.faqs;
|
|
1552
|
+
if (!Array.isArray(faqs)) {
|
|
1553
|
+
await reply("Invalid bundle: missing or invalid \"faqs\" array.");
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
await reply(`Importing ${faqs.length} FAQs...`);
|
|
1557
|
+
let imported = 0;
|
|
1558
|
+
let skipped = 0;
|
|
1559
|
+
for (const faq of faqs) {
|
|
1560
|
+
const question = typeof faq.question === "string" ? faq.question.trim() : "";
|
|
1561
|
+
const answer = typeof faq.answer === "string" ? faq.answer.trim() : "";
|
|
1562
|
+
if (question && answer) {
|
|
1563
|
+
await kb.ingestFaq(question, answer);
|
|
1564
|
+
imported++;
|
|
1565
|
+
} else skipped++;
|
|
1566
|
+
}
|
|
1567
|
+
const docRefs = Array.isArray(bundle.documents) ? bundle.documents : [];
|
|
1568
|
+
const urlRefs = docRefs.filter((d) => d.sourceType === "url" && d.sourceUrl);
|
|
1569
|
+
const lines = [`Import complete! ${imported} FAQs imported.`];
|
|
1570
|
+
if (skipped > 0) lines.push(`${skipped} FAQs skipped (missing question or answer).`);
|
|
1571
|
+
if (docRefs.length > 0) {
|
|
1572
|
+
lines.push(`${docRefs.length} document reference(s) found:`);
|
|
1573
|
+
for (const ref of docRefs) if (ref.sourceUrl) lines.push(` - ${ref.title || ref.sourceUrl} (${ref.sourceType})`);
|
|
1574
|
+
else if (ref.fileName) lines.push(` - ${ref.fileName} (${ref.sourceType})`);
|
|
1575
|
+
if (urlRefs.length > 0) lines.push(`\nTo re-ingest URLs, send /learn-url for each.`);
|
|
1576
|
+
}
|
|
1577
|
+
const bundleAgent = bundle.agent;
|
|
1578
|
+
if (bundleAgent && (bundleAgent.instructions || bundleAgent.identity || bundleAgent.soul)) {
|
|
1579
|
+
const agentsDir = this.config.agentsDir;
|
|
1580
|
+
const agentName = bundleAgent.name || "support";
|
|
1581
|
+
if (agentsDir) {
|
|
1582
|
+
const agentDir = path.join(agentsDir, agentName);
|
|
1583
|
+
await fs.mkdir(agentDir, { recursive: true });
|
|
1584
|
+
if (bundleAgent.instructions) {
|
|
1585
|
+
const frontmatter = { name: agentName };
|
|
1586
|
+
if (bundleAgent.purpose) frontmatter.purpose = bundleAgent.purpose;
|
|
1587
|
+
if (bundleAgent.triggers?.length) frontmatter.triggers = bundleAgent.triggers;
|
|
1588
|
+
if (bundleAgent.channels?.length) frontmatter.channels = bundleAgent.channels;
|
|
1589
|
+
if (bundleAgent.skills?.length) frontmatter.skills = bundleAgent.skills;
|
|
1590
|
+
if (bundleAgent.knowledgeBase != null) frontmatter.knowledgeBase = bundleAgent.knowledgeBase;
|
|
1591
|
+
if (bundleAgent.priority != null) frontmatter.priority = bundleAgent.priority;
|
|
1592
|
+
if (bundleAgent.escalateTo) frontmatter.escalateTo = bundleAgent.escalateTo;
|
|
1593
|
+
if (bundle.guardrails) frontmatter.guardrails = bundle.guardrails;
|
|
1594
|
+
const instructionsContent = `---\n${yaml.dump(frontmatter, {
|
|
1595
|
+
lineWidth: -1,
|
|
1596
|
+
noRefs: true
|
|
1597
|
+
}).trim()}\n---\n\n${bundleAgent.instructions.trim()}\n`;
|
|
1598
|
+
await fs.writeFile(path.join(agentDir, "INSTRUCTIONS.md"), instructionsContent, "utf-8");
|
|
1599
|
+
}
|
|
1600
|
+
if (bundleAgent.identity) await fs.writeFile(path.join(agentDir, "IDENTITY.md"), bundleAgent.identity.trim() + "\n", "utf-8");
|
|
1601
|
+
if (bundleAgent.soul) await fs.writeFile(path.join(agentDir, "SOUL.md"), bundleAgent.soul.trim() + "\n", "utf-8");
|
|
1602
|
+
lines.push(`Agent instructions written to ${agentDir}.`);
|
|
1603
|
+
} else {
|
|
1604
|
+
const agentList = Array.from(this.agents.values());
|
|
1605
|
+
const targetAgent = agentList.find((a) => a.getConfig().name === agentName) || agentList[0];
|
|
1606
|
+
if (targetAgent) {
|
|
1607
|
+
if (bundleAgent.instructions) targetAgent.config._rawInstructions = bundleAgent.instructions;
|
|
1608
|
+
if (bundleAgent.identity) targetAgent.config._rawIdentity = bundleAgent.identity;
|
|
1609
|
+
if (bundleAgent.soul) targetAgent.config._rawSoul = bundleAgent.soul;
|
|
1610
|
+
lines.push("Agent instructions applied in-memory.");
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (Array.isArray(bundle.promptSkills) && bundle.promptSkills.length > 0) {
|
|
1615
|
+
const mod = this.config.skillsModule;
|
|
1616
|
+
if (mod) try {
|
|
1617
|
+
const skillsConfig = mod.loadSkillsConfig();
|
|
1618
|
+
let added = 0;
|
|
1619
|
+
for (const ps of bundle.promptSkills) {
|
|
1620
|
+
if (!ps.name || !ps.content) continue;
|
|
1621
|
+
if (skillsConfig.skills.find((s) => s.name === ps.name)) continue;
|
|
1622
|
+
skillsConfig.skills.push({
|
|
1623
|
+
type: "prompt",
|
|
1624
|
+
name: ps.name,
|
|
1625
|
+
content: ps.content,
|
|
1626
|
+
summary: ps.summary,
|
|
1627
|
+
tags: ps.tags,
|
|
1628
|
+
author: ps.author,
|
|
1629
|
+
enabled: true
|
|
1630
|
+
});
|
|
1631
|
+
added++;
|
|
1632
|
+
}
|
|
1633
|
+
if (added > 0) {
|
|
1634
|
+
mod.saveSkillsConfig(skillsConfig);
|
|
1635
|
+
lines.push(`${added} prompt skill(s) imported to mcp.json.`);
|
|
1636
|
+
}
|
|
1637
|
+
} catch (err) {
|
|
1638
|
+
lines.push(`Warning: failed to import prompt skills: ${err.message}`);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
await reply(lines.join("\n"));
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
this.log(`Import error: ${error.message}`);
|
|
1644
|
+
await reply(`Error: ${error.message}`);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Handle messages when sender has an active edit session.
|
|
1649
|
+
*/
|
|
1650
|
+
async handlePendingEdit(message, edit) {
|
|
1651
|
+
const text = message.text.trim();
|
|
1652
|
+
const reply = async (t) => {
|
|
1653
|
+
await this.sendMessage(message.from, message.provider, {
|
|
1654
|
+
to: message.from,
|
|
1655
|
+
text: t
|
|
1656
|
+
});
|
|
1657
|
+
};
|
|
1658
|
+
if (Date.now() - edit.createdAt > 12e4) {
|
|
1659
|
+
this.pendingEdits.delete(message.from);
|
|
1660
|
+
await reply("Edit session expired (2 min timeout). Start again with /instructions edit, /identity edit, or /soul edit.");
|
|
1661
|
+
return true;
|
|
1662
|
+
}
|
|
1663
|
+
if (edit.state === "confirming") {
|
|
1664
|
+
if (text.toLowerCase() === "yes") await this.applyEdit(message.from, edit, reply);
|
|
1665
|
+
else {
|
|
1666
|
+
this.pendingEdits.delete(message.from);
|
|
1667
|
+
await reply("Edit cancelled.");
|
|
1668
|
+
}
|
|
1669
|
+
return true;
|
|
1670
|
+
}
|
|
1671
|
+
if (text === "/cancel") {
|
|
1672
|
+
this.pendingEdits.delete(message.from);
|
|
1673
|
+
await reply("Edit cancelled.");
|
|
1674
|
+
return true;
|
|
1675
|
+
}
|
|
1676
|
+
if (text === "/done") {
|
|
1677
|
+
const newContent = edit.content.join("\n");
|
|
1678
|
+
const agent = this.getFirstAgent();
|
|
1679
|
+
if (!agent) {
|
|
1680
|
+
await reply("No agent found.");
|
|
1681
|
+
return true;
|
|
1682
|
+
}
|
|
1683
|
+
const current = agent.config[{
|
|
1684
|
+
instructions: "_rawInstructions",
|
|
1685
|
+
identity: "_rawIdentity",
|
|
1686
|
+
soul: "_rawSoul"
|
|
1687
|
+
}[edit.field]] || "(empty)";
|
|
1688
|
+
let currentDisplay = current;
|
|
1689
|
+
let newDisplay = newContent;
|
|
1690
|
+
if (edit.field === "instructions" && edit.section) {
|
|
1691
|
+
currentDisplay = this.extractSection(current, edit.section) || "(section not found)";
|
|
1692
|
+
newDisplay = newContent;
|
|
1693
|
+
}
|
|
1694
|
+
const maxDisplay = 1500;
|
|
1695
|
+
if (currentDisplay.length > maxDisplay) currentDisplay = currentDisplay.slice(0, maxDisplay) + "\n...";
|
|
1696
|
+
if (newDisplay.length > maxDisplay) newDisplay = newDisplay.slice(0, maxDisplay) + "\n...";
|
|
1697
|
+
edit.state = "confirming";
|
|
1698
|
+
await reply(`*CURRENT ${edit.field}${edit.section ? ` (${edit.section})` : ""}:*\n${currentDisplay}\n\n*NEW:*\n${newDisplay}\n\nSend "yes" to apply or anything else to cancel.`);
|
|
1699
|
+
return true;
|
|
1700
|
+
}
|
|
1701
|
+
edit.content.push(text);
|
|
1702
|
+
return true;
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Apply a confirmed edit.
|
|
1706
|
+
*/
|
|
1707
|
+
async applyEdit(sender, edit, reply) {
|
|
1708
|
+
this.pendingEdits.delete(sender);
|
|
1709
|
+
const agent = this.getFirstAgent();
|
|
1710
|
+
if (!agent) {
|
|
1711
|
+
await reply("No agent found.");
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
const newContent = edit.content.join("\n");
|
|
1715
|
+
const rawKey = {
|
|
1716
|
+
instructions: "_rawInstructions",
|
|
1717
|
+
identity: "_rawIdentity",
|
|
1718
|
+
soul: "_rawSoul"
|
|
1719
|
+
}[edit.field];
|
|
1720
|
+
await this.versionStore.saveVersion(edit.agentName, {
|
|
1721
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1722
|
+
instructions: agent.config._rawInstructions,
|
|
1723
|
+
identity: agent.config._rawIdentity,
|
|
1724
|
+
soul: agent.config._rawSoul,
|
|
1725
|
+
frontmatter: this.extractFrontmatter(agent.config),
|
|
1726
|
+
editedBy: sender,
|
|
1727
|
+
changeDescription: `Edit ${edit.field}${edit.section ? ` section "${edit.section}"` : ""}`
|
|
1728
|
+
});
|
|
1729
|
+
if (edit.field === "instructions" && edit.section) {
|
|
1730
|
+
const current = agent.config._rawInstructions || "";
|
|
1731
|
+
agent.config._rawInstructions = this.replaceSection(current, edit.section, newContent);
|
|
1732
|
+
} else agent.config[rawKey] = newContent;
|
|
1733
|
+
this.rebuildAgentSystemPrompt(agent);
|
|
1734
|
+
await this.writeAgentFiles(edit.agentName, agent);
|
|
1735
|
+
await reply(`${edit.field} updated successfully.`);
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Handle messages when sender has an active /quickstart session.
|
|
1739
|
+
*/
|
|
1740
|
+
async handlePendingQuickstart(message, qs) {
|
|
1741
|
+
const text = message.text.trim();
|
|
1742
|
+
const reply = async (t) => {
|
|
1743
|
+
await this.sendMessage(message.from, message.provider, {
|
|
1744
|
+
to: message.from,
|
|
1745
|
+
text: t
|
|
1746
|
+
});
|
|
1747
|
+
};
|
|
1748
|
+
if (Date.now() - qs.createdAt > 12e4) {
|
|
1749
|
+
this.pendingQuickstarts.delete(message.from);
|
|
1750
|
+
await reply("Quickstart session expired (2 min timeout). Send /quickstart to try again.");
|
|
1751
|
+
return true;
|
|
1752
|
+
}
|
|
1753
|
+
if (text === "/cancel") {
|
|
1754
|
+
this.pendingQuickstarts.delete(message.from);
|
|
1755
|
+
await reply("Quickstart cancelled.");
|
|
1756
|
+
return true;
|
|
1757
|
+
}
|
|
1758
|
+
const kb = this.config.kb;
|
|
1759
|
+
if (!kb) {
|
|
1760
|
+
this.pendingQuickstarts.delete(message.from);
|
|
1761
|
+
await reply("Knowledge Base is not available.");
|
|
1762
|
+
return true;
|
|
1763
|
+
}
|
|
1764
|
+
if (qs.step === "question") {
|
|
1765
|
+
qs.question = text;
|
|
1766
|
+
qs.step = "answer";
|
|
1767
|
+
qs.createdAt = Date.now();
|
|
1768
|
+
await reply(`Got it!\n\nStep 2/3: What should the agent answer?\n\nQ: "${text}"\n\n(Send the answer, or /cancel to abort)`);
|
|
1769
|
+
return true;
|
|
1770
|
+
}
|
|
1771
|
+
if (qs.step === "answer") {
|
|
1772
|
+
const question = qs.question;
|
|
1773
|
+
const answer = text;
|
|
1774
|
+
this.pendingQuickstarts.delete(message.from);
|
|
1775
|
+
try {
|
|
1776
|
+
const doc = await kb.ingestFaq(question, answer);
|
|
1777
|
+
const result = await kb.retrieve(question);
|
|
1778
|
+
const top = result.results[0];
|
|
1779
|
+
const score = top ? top.score.toFixed(2) : "N/A";
|
|
1780
|
+
const matchType = result.isFaqMatch ? "FAQ direct match" : "KB search";
|
|
1781
|
+
await reply([
|
|
1782
|
+
"Step 3/3: Teaching & testing...",
|
|
1783
|
+
"",
|
|
1784
|
+
`FAQ added (${doc.id.slice(0, 8)}...)`,
|
|
1785
|
+
` Q: ${question}`,
|
|
1786
|
+
` A: ${answer}`,
|
|
1787
|
+
"",
|
|
1788
|
+
`Test result: ${matchType} (score: ${score})`,
|
|
1789
|
+
"",
|
|
1790
|
+
"Your agent just learned its first answer! Try sending the question as a regular message to see it in action.",
|
|
1791
|
+
"",
|
|
1792
|
+
"Next steps:",
|
|
1793
|
+
" /teach <question> | <answer> — add more FAQs",
|
|
1794
|
+
" /test <message> — test KB responses",
|
|
1795
|
+
" /help — see all commands"
|
|
1796
|
+
].join("\n"));
|
|
1797
|
+
} catch (error) {
|
|
1798
|
+
await reply(`Error during quickstart: ${error.message}`);
|
|
1799
|
+
}
|
|
1800
|
+
return true;
|
|
1801
|
+
}
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Add assistant response to conversation history (public API for manual use)
|
|
1806
|
+
*/
|
|
1807
|
+
async addAssistantMessage(customerId, text, toolCalls) {
|
|
1808
|
+
await this.memory.addMessage(customerId, {
|
|
1809
|
+
role: "assistant",
|
|
1810
|
+
content: text,
|
|
1811
|
+
timestamp: Date.now(),
|
|
1812
|
+
toolCalls
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
async handleInstructionsCommand(message, args, reply) {
|
|
1816
|
+
const agent = this.getFirstAgent();
|
|
1817
|
+
if (!agent) {
|
|
1818
|
+
await reply("No agent found.");
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
const sub = args[0]?.toLowerCase();
|
|
1822
|
+
const raw = agent.config._rawInstructions || "";
|
|
1823
|
+
if (sub === "edit") {
|
|
1824
|
+
const section = args.slice(1).join(" ") || void 0;
|
|
1825
|
+
this.pendingEdits.set(message.from, {
|
|
1826
|
+
agentName: agent.config.name,
|
|
1827
|
+
field: "instructions",
|
|
1828
|
+
section,
|
|
1829
|
+
content: [],
|
|
1830
|
+
createdAt: Date.now(),
|
|
1831
|
+
state: "capturing"
|
|
1832
|
+
});
|
|
1833
|
+
await reply(`Editing ${section ? `section "${section}"` : "full instructions"}. Send your new content, then /done to apply or /cancel to abort. (2 min timeout)`);
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
if (sub === "view") {
|
|
1837
|
+
const section = args.slice(1).join(" ") || void 0;
|
|
1838
|
+
let content = section ? this.extractSection(raw, section) || `Section "${section}" not found.` : raw;
|
|
1839
|
+
if (!content.trim()) {
|
|
1840
|
+
await reply("Instructions are empty.");
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
if (content.length > 2500) await reply(content.slice(0, 2500) + "\n\n(truncated — send /instructions view again for full)");
|
|
1844
|
+
else await reply(content);
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
if (sub === "history") {
|
|
1848
|
+
const history = await this.versionStore.getHistory(agent.config.name, 10);
|
|
1849
|
+
if (history.length === 0) {
|
|
1850
|
+
await reply("No version history.");
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
await reply(`Version history:\n${history.map((v, i) => `${i}. ${v.timestamp} — ${v.changeDescription || "no description"}${v.editedBy ? ` (by ${v.editedBy})` : ""}`).join("\n")}`);
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
if (sub === "rollback") {
|
|
1857
|
+
const n = args[1] != null ? parseInt(args[1]) : 0;
|
|
1858
|
+
const version = await this.versionStore.getVersion(agent.config.name, n);
|
|
1859
|
+
if (!version) {
|
|
1860
|
+
await reply(`Version ${n} not found.`);
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
await this.versionStore.saveVersion(agent.config.name, {
|
|
1864
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1865
|
+
instructions: agent.config._rawInstructions,
|
|
1866
|
+
identity: agent.config._rawIdentity,
|
|
1867
|
+
soul: agent.config._rawSoul,
|
|
1868
|
+
frontmatter: this.extractFrontmatter(agent.config),
|
|
1869
|
+
editedBy: message.from,
|
|
1870
|
+
changeDescription: `Rollback to version ${n}`
|
|
1871
|
+
});
|
|
1872
|
+
if (version.instructions !== void 0) agent.config._rawInstructions = version.instructions;
|
|
1873
|
+
if (version.identity !== void 0) agent.config._rawIdentity = version.identity;
|
|
1874
|
+
if (version.soul !== void 0) agent.config._rawSoul = version.soul;
|
|
1875
|
+
this.rebuildAgentSystemPrompt(agent);
|
|
1876
|
+
await this.writeAgentFiles(agent.config.name, agent);
|
|
1877
|
+
await reply(`Rolled back to version ${n} (${version.timestamp}).`);
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
if (!raw.trim()) {
|
|
1881
|
+
await reply("Instructions are empty.");
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const sections = this.parseSections(raw);
|
|
1885
|
+
if (sections.length === 0) await reply(`Instructions (${raw.length} chars):\n${raw.slice(0, 500)}${raw.length > 500 ? "\n..." : ""}`);
|
|
1886
|
+
else await reply(`Instructions sections:\n${sections.map((s) => ` ${s.heading} (${s.content.length} chars)`).join("\n")}\n\nUse /instructions view [section] to see details.`);
|
|
1887
|
+
}
|
|
1888
|
+
async handleFieldViewOrEdit(field, message, args, reply) {
|
|
1889
|
+
const agent = this.getFirstAgent();
|
|
1890
|
+
if (!agent) {
|
|
1891
|
+
await reply("No agent found.");
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
const rawKey = field === "identity" ? "_rawIdentity" : "_rawSoul";
|
|
1895
|
+
const current = agent.config[rawKey] || "";
|
|
1896
|
+
if (args[0]?.toLowerCase() === "edit") {
|
|
1897
|
+
this.pendingEdits.set(message.from, {
|
|
1898
|
+
agentName: agent.config.name,
|
|
1899
|
+
field,
|
|
1900
|
+
content: [],
|
|
1901
|
+
createdAt: Date.now(),
|
|
1902
|
+
state: "capturing"
|
|
1903
|
+
});
|
|
1904
|
+
await reply(`Editing ${field}. Send your new content, then /done to apply or /cancel to abort. (2 min timeout)`);
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
if (!current.trim()) {
|
|
1908
|
+
await reply(`${field} is empty.`);
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
if (current.length > 2500) await reply(current.slice(0, 2500) + "\n\n(truncated)");
|
|
1912
|
+
else await reply(current);
|
|
1913
|
+
}
|
|
1914
|
+
async handleConfigCommand(message, args, reply) {
|
|
1915
|
+
const agent = this.getFirstAgent();
|
|
1916
|
+
if (!agent) {
|
|
1917
|
+
await reply("No agent found.");
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
const sub = args[0]?.toLowerCase();
|
|
1921
|
+
if (sub === "show" || !sub) {
|
|
1922
|
+
const c = agent.config;
|
|
1923
|
+
const lines = [
|
|
1924
|
+
`*Agent Config: ${c.name}*`,
|
|
1925
|
+
` purpose: ${c.purpose || "(not set)"}`,
|
|
1926
|
+
` triggers: ${c.triggers?.join(", ") || "(none)"}`,
|
|
1927
|
+
` channels: ${c.channels?.join(", ") || "(all)"}`,
|
|
1928
|
+
` skills: ${c.skills?.join(", ") || "(none)"}`,
|
|
1929
|
+
` knowledgeBase: ${c.knowledgeBase ?? false}`,
|
|
1930
|
+
` priority: ${c.priority ?? 0}`,
|
|
1931
|
+
` escalateTo: ${c.escalateTo || "(not set)"}`
|
|
1932
|
+
];
|
|
1933
|
+
if (c.guardrails) {
|
|
1934
|
+
lines.push(` maxResponseLength: ${c.guardrails.maxResponseLength ?? "(not set)"}`);
|
|
1935
|
+
lines.push(` blockedTopics: ${c.guardrails.blockedTopics?.join(", ") || "(none)"}`);
|
|
1936
|
+
lines.push(` escalationTriggers: ${c.guardrails.escalationTriggers?.join(", ") || "(none)"}`);
|
|
1937
|
+
lines.push(` systemRules: ${c.guardrails.systemRules?.length || 0} rules`);
|
|
1938
|
+
}
|
|
1939
|
+
await reply(lines.join("\n"));
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
if (sub === "set") {
|
|
1943
|
+
const key = args[1];
|
|
1944
|
+
const value = args.slice(2).join(" ");
|
|
1945
|
+
if (!key || !value) {
|
|
1946
|
+
await reply("Usage: /config set <key> <value>");
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
await this.configSet(agent, key, value, message.from, reply);
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
if (sub === "add") {
|
|
1953
|
+
const key = args[1];
|
|
1954
|
+
const value = args.slice(2).join(" ");
|
|
1955
|
+
if (!key || !value) {
|
|
1956
|
+
await reply("Usage: /config add <key> <value>");
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
await this.configArrayOp(agent, "add", key, value, message.from, reply);
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
if (sub === "remove") {
|
|
1963
|
+
const key = args[1];
|
|
1964
|
+
const value = args.slice(2).join(" ");
|
|
1965
|
+
if (!key || !value) {
|
|
1966
|
+
await reply("Usage: /config remove <key> <value>");
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
await this.configArrayOp(agent, "remove", key, value, message.from, reply);
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
await reply("Usage: /config show | /config set <key> <value> | /config add <key> <value> | /config remove <key> <value>");
|
|
1973
|
+
}
|
|
1974
|
+
async handleModelCommand(args, reply) {
|
|
1975
|
+
const llm = this.config.llmProvider;
|
|
1976
|
+
if (!llm || !llm.getConfig) {
|
|
1977
|
+
await reply("LLM provider not available. Configure LLM_PROVIDER and LLM_API_KEY first.");
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
const sub = args[0]?.toLowerCase();
|
|
1981
|
+
if (!sub || sub === "show") {
|
|
1982
|
+
const cfg = llm.getConfig();
|
|
1983
|
+
const key = cfg.apiKey;
|
|
1984
|
+
const masked = key ? key.slice(0, 6) + "..." + key.slice(-4) : "(not set)";
|
|
1985
|
+
await reply([
|
|
1986
|
+
"*Current LLM Config*",
|
|
1987
|
+
` provider: ${cfg.provider}`,
|
|
1988
|
+
` model: ${cfg.model || "(default)"}`,
|
|
1989
|
+
` apiKey: ${masked}`
|
|
1990
|
+
].join("\n"));
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
if (sub === "list") {
|
|
1994
|
+
if (!llm.getModelCatalog) {
|
|
1995
|
+
await reply("Model catalog not available.");
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
const { catalog, defaults } = llm.getModelCatalog();
|
|
1999
|
+
const cfg = llm.getConfig();
|
|
2000
|
+
const lines = ["*Available Models*", ""];
|
|
2001
|
+
for (const [provider, models] of Object.entries(catalog)) {
|
|
2002
|
+
const isCurrent = provider === cfg.provider;
|
|
2003
|
+
const hasKey = isCurrent || !!llm.getApiKey?.(provider);
|
|
2004
|
+
const marker = isCurrent ? " (active)" : hasKey ? " (key set)" : "";
|
|
2005
|
+
lines.push(`*${provider}*${marker}`);
|
|
2006
|
+
for (const m of models) {
|
|
2007
|
+
const isDefault = m === (defaults[provider] || "");
|
|
2008
|
+
const prefix = isCurrent && (cfg.model === m || !cfg.model && isDefault) ? " → " : " ";
|
|
2009
|
+
lines.push(`${prefix}${m}${isDefault ? " (default)" : ""}`);
|
|
2010
|
+
}
|
|
2011
|
+
lines.push("");
|
|
2012
|
+
}
|
|
2013
|
+
await reply(lines.join("\n"));
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
if (sub === "set") {
|
|
2017
|
+
const value = args.slice(1).join(" ").trim();
|
|
2018
|
+
if (!value) {
|
|
2019
|
+
await reply("Usage: /model set <provider/model> or /model set <model>");
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
let provider;
|
|
2023
|
+
let model;
|
|
2024
|
+
if (value.includes("/")) {
|
|
2025
|
+
const slashIdx = value.indexOf("/");
|
|
2026
|
+
provider = value.slice(0, slashIdx);
|
|
2027
|
+
model = value.slice(slashIdx + 1);
|
|
2028
|
+
} else model = value;
|
|
2029
|
+
const cfg = llm.getConfig();
|
|
2030
|
+
const targetProvider = provider || cfg.provider;
|
|
2031
|
+
const validProviders = [
|
|
2032
|
+
"openai",
|
|
2033
|
+
"anthropic",
|
|
2034
|
+
"google",
|
|
2035
|
+
"groq",
|
|
2036
|
+
"ollama"
|
|
2037
|
+
];
|
|
2038
|
+
if (!validProviders.includes(targetProvider)) {
|
|
2039
|
+
await reply(`Unknown provider: ${targetProvider}\nValid: ${validProviders.join(", ")}`);
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
if (provider && provider !== cfg.provider) {
|
|
2043
|
+
const key = llm.getApiKey?.(provider);
|
|
2044
|
+
if (!key) {
|
|
2045
|
+
await reply(`No API key for ${provider}. Use /model key ${provider} <key> first.`);
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
llm.setConfig({
|
|
2049
|
+
provider,
|
|
2050
|
+
model,
|
|
2051
|
+
apiKey: key
|
|
2052
|
+
});
|
|
2053
|
+
} else llm.setConfig({ model });
|
|
2054
|
+
const memory = this.config.memory;
|
|
2055
|
+
if (memory?.setSetting) {
|
|
2056
|
+
await memory.setSetting("llm_provider", targetProvider);
|
|
2057
|
+
await memory.setSetting("llm_model", model);
|
|
2058
|
+
}
|
|
2059
|
+
await reply(`Model switched to *${targetProvider}/${model}*`);
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
if (sub === "key") {
|
|
2063
|
+
const provider = args[1]?.toLowerCase();
|
|
2064
|
+
const key = args[2];
|
|
2065
|
+
if (!provider || !key) {
|
|
2066
|
+
await reply("Usage: /model key <provider> <api-key>");
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
const validProviders = [
|
|
2070
|
+
"openai",
|
|
2071
|
+
"anthropic",
|
|
2072
|
+
"google",
|
|
2073
|
+
"groq",
|
|
2074
|
+
"ollama"
|
|
2075
|
+
];
|
|
2076
|
+
if (!validProviders.includes(provider)) {
|
|
2077
|
+
await reply(`Unknown provider: ${provider}\nValid: ${validProviders.join(", ")}`);
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
llm.setApiKey(provider, key);
|
|
2081
|
+
const memory = this.config.memory;
|
|
2082
|
+
if (memory?.setSetting) await memory.setSetting(`llm_apikey_${provider}`, key);
|
|
2083
|
+
await reply(`API key for *${provider}* saved: ${key.slice(0, 6) + "..." + key.slice(-4)}`);
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
await reply("Usage: /model | /model list | /model set <provider/model> | /model key <provider> <key>");
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Select agent based on intent.
|
|
2090
|
+
* Sorts by priority (higher first), then specificity (fewer triggers = more specific).
|
|
2091
|
+
* Agents with triggers: ['*'] are treated as fallback (checked last).
|
|
2092
|
+
*/
|
|
2093
|
+
selectAgent(intent, message) {
|
|
2094
|
+
const channelFiltered = Array.from(this.agents.values()).filter((agent) => {
|
|
2095
|
+
const cfg = agent.getConfig();
|
|
2096
|
+
if (cfg.channels && cfg.channels.length > 0) return cfg.channels.includes(message.channel) || cfg.channels.includes(message.provider);
|
|
2097
|
+
return true;
|
|
2098
|
+
});
|
|
2099
|
+
const specific = [];
|
|
2100
|
+
const fallback = [];
|
|
2101
|
+
for (const agent of channelFiltered) {
|
|
2102
|
+
const triggers = agent.getConfig().triggers || [];
|
|
2103
|
+
if (triggers.length === 1 && triggers[0] === "*") fallback.push(agent);
|
|
2104
|
+
else specific.push(agent);
|
|
2105
|
+
}
|
|
2106
|
+
const sortByPriority = (a, b) => {
|
|
2107
|
+
const pa = a.getConfig().priority ?? 0;
|
|
2108
|
+
const pb = b.getConfig().priority ?? 0;
|
|
2109
|
+
if (pb !== pa) return pb - pa;
|
|
2110
|
+
return (a.getConfig().triggers?.length ?? 0) - (b.getConfig().triggers?.length ?? 0);
|
|
2111
|
+
};
|
|
2112
|
+
specific.sort(sortByPriority);
|
|
2113
|
+
fallback.sort(sortByPriority);
|
|
2114
|
+
for (const agent of specific) if (agent.matchesIntent(intent.intent)) return agent;
|
|
2115
|
+
for (const agent of fallback) return agent;
|
|
2116
|
+
return null;
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Send message via provider
|
|
2120
|
+
*/
|
|
2121
|
+
async sendMessage(to, providerName, message) {
|
|
2122
|
+
const provider = this.providers.get(providerName);
|
|
2123
|
+
if (!provider) throw new Error(`Provider not found: ${providerName}`);
|
|
2124
|
+
await provider.sendMessage(to, message);
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Get all agents
|
|
2128
|
+
*/
|
|
2129
|
+
getAgents() {
|
|
2130
|
+
return Array.from(this.agents.values());
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2133
|
+
* Get all skills
|
|
2134
|
+
*/
|
|
2135
|
+
getSkills() {
|
|
2136
|
+
return Array.from(this.skills.values());
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Check if running
|
|
2140
|
+
*/
|
|
2141
|
+
isActive() {
|
|
2142
|
+
return this.isRunning;
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Get the analytics store (if configured) for querying metrics
|
|
2146
|
+
*/
|
|
2147
|
+
getAnalyticsStore() {
|
|
2148
|
+
return this.config.analyticsStore ?? null;
|
|
2149
|
+
}
|
|
2150
|
+
async configSet(agent, key, value, sender, reply) {
|
|
2151
|
+
const scalarKeys = {
|
|
2152
|
+
purpose: (v) => v,
|
|
2153
|
+
maxResponseLength: (v) => parseInt(v),
|
|
2154
|
+
priority: (v) => parseInt(v),
|
|
2155
|
+
escalateTo: (v) => v,
|
|
2156
|
+
knowledgeBase: (v) => v === "true"
|
|
2157
|
+
};
|
|
2158
|
+
const parser = scalarKeys[key];
|
|
2159
|
+
if (!parser) {
|
|
2160
|
+
await reply(`Unknown config key "${key}". Valid: ${Object.keys(scalarKeys).join(", ")}`);
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
await this.versionStore.saveVersion(agent.config.name, {
|
|
2164
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2165
|
+
instructions: agent.config._rawInstructions,
|
|
2166
|
+
identity: agent.config._rawIdentity,
|
|
2167
|
+
soul: agent.config._rawSoul,
|
|
2168
|
+
frontmatter: this.extractFrontmatter(agent.config),
|
|
2169
|
+
editedBy: sender,
|
|
2170
|
+
changeDescription: `Set ${key} = ${value}`
|
|
2171
|
+
});
|
|
2172
|
+
const parsed = parser(value);
|
|
2173
|
+
if (key === "maxResponseLength") {
|
|
2174
|
+
agent.config.guardrails = agent.config.guardrails || {};
|
|
2175
|
+
agent.config.guardrails.maxResponseLength = parsed;
|
|
2176
|
+
} else agent.config[key] = parsed;
|
|
2177
|
+
await this.writeAgentFrontmatter(agent.config.name, agent);
|
|
2178
|
+
await reply(`Config updated: ${key} = ${value}`);
|
|
2179
|
+
}
|
|
2180
|
+
async configArrayOp(agent, op, key, value, sender, reply) {
|
|
2181
|
+
const arrayKeys = {
|
|
2182
|
+
triggers: {
|
|
2183
|
+
get: () => agent.config.triggers || [],
|
|
2184
|
+
set: (v) => {
|
|
2185
|
+
agent.config.triggers = v;
|
|
2186
|
+
}
|
|
2187
|
+
},
|
|
2188
|
+
channels: {
|
|
2189
|
+
get: () => agent.config.channels || [],
|
|
2190
|
+
set: (v) => {
|
|
2191
|
+
agent.config.channels = v;
|
|
2192
|
+
}
|
|
2193
|
+
},
|
|
2194
|
+
skills: {
|
|
2195
|
+
get: () => agent.config.skills || [],
|
|
2196
|
+
set: (v) => {
|
|
2197
|
+
agent.config.skills = v;
|
|
2198
|
+
}
|
|
2199
|
+
},
|
|
2200
|
+
blockedTopics: {
|
|
2201
|
+
get: () => agent.config.guardrails?.blockedTopics || [],
|
|
2202
|
+
set: (v) => {
|
|
2203
|
+
agent.config.guardrails = agent.config.guardrails || {};
|
|
2204
|
+
agent.config.guardrails.blockedTopics = v;
|
|
2205
|
+
}
|
|
2206
|
+
},
|
|
2207
|
+
escalationTriggers: {
|
|
2208
|
+
get: () => agent.config.guardrails?.escalationTriggers || [],
|
|
2209
|
+
set: (v) => {
|
|
2210
|
+
agent.config.guardrails = agent.config.guardrails || {};
|
|
2211
|
+
agent.config.guardrails.escalationTriggers = v;
|
|
2212
|
+
}
|
|
2213
|
+
},
|
|
2214
|
+
systemRules: {
|
|
2215
|
+
get: () => agent.config.guardrails?.systemRules || [],
|
|
2216
|
+
set: (v) => {
|
|
2217
|
+
agent.config.guardrails = agent.config.guardrails || {};
|
|
2218
|
+
agent.config.guardrails.systemRules = v;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
const accessor = arrayKeys[key];
|
|
2223
|
+
if (!accessor) {
|
|
2224
|
+
await reply(`Unknown array key "${key}". Valid: ${Object.keys(arrayKeys).join(", ")}`);
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
await this.versionStore.saveVersion(agent.config.name, {
|
|
2228
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2229
|
+
instructions: agent.config._rawInstructions,
|
|
2230
|
+
identity: agent.config._rawIdentity,
|
|
2231
|
+
soul: agent.config._rawSoul,
|
|
2232
|
+
frontmatter: this.extractFrontmatter(agent.config),
|
|
2233
|
+
editedBy: sender,
|
|
2234
|
+
changeDescription: `${op} ${key}: ${value}`
|
|
2235
|
+
});
|
|
2236
|
+
const arr = accessor.get();
|
|
2237
|
+
if (op === "add") {
|
|
2238
|
+
if (!arr.includes(value)) arr.push(value);
|
|
2239
|
+
accessor.set(arr);
|
|
2240
|
+
} else accessor.set(arr.filter((v) => v !== value));
|
|
2241
|
+
if (key === "systemRules") this.rebuildAgentSystemPrompt(agent);
|
|
2242
|
+
await this.writeAgentFrontmatter(agent.config.name, agent);
|
|
2243
|
+
await reply(`Config updated: ${op} "${value}" ${op === "add" ? "to" : "from"} ${key}`);
|
|
2244
|
+
}
|
|
2245
|
+
async handleWhitelistCommand(message, args, reply) {
|
|
2246
|
+
const whitelist = this.config.trainingMode?.whitelist;
|
|
2247
|
+
if (!whitelist) {
|
|
2248
|
+
await reply("Training mode whitelist is not configured.");
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
const normalizePhone = (p) => p.replace(/[\s\-+]/g, "");
|
|
2252
|
+
const formatPhone = (p) => p.startsWith("+") ? p : `+${p}`;
|
|
2253
|
+
const sub = args[0]?.toLowerCase();
|
|
2254
|
+
if (!sub || sub === "list") {
|
|
2255
|
+
if (whitelist.length === 0) await reply("Whitelist is empty.");
|
|
2256
|
+
else {
|
|
2257
|
+
const lines = whitelist.map((p) => ` ${formatPhone(p)}`);
|
|
2258
|
+
await reply(`Whitelisted numbers (${whitelist.length}):\n${lines.join("\n")}`);
|
|
2259
|
+
}
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
if (sub === "add") {
|
|
2263
|
+
const phone = args.slice(1).join("").trim();
|
|
2264
|
+
if (!phone) {
|
|
2265
|
+
await reply("Usage: /whitelist add <phone>");
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
const normalized = normalizePhone(phone);
|
|
2269
|
+
if (whitelist.some((w) => normalizePhone(w) === normalized)) {
|
|
2270
|
+
await reply(`${formatPhone(phone)} is already whitelisted.`);
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
whitelist.push(phone.startsWith("+") ? phone : `+${phone}`);
|
|
2274
|
+
await this.persistWhitelist();
|
|
2275
|
+
await reply(`Added ${formatPhone(phone)} to whitelist.`);
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
if (sub === "remove") {
|
|
2279
|
+
const phone = args.slice(1).join("").trim();
|
|
2280
|
+
if (!phone) {
|
|
2281
|
+
await reply("Usage: /whitelist remove <phone>");
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
const normalized = normalizePhone(phone);
|
|
2285
|
+
if (normalized === normalizePhone(message.from)) {
|
|
2286
|
+
await reply("You cannot remove yourself from the whitelist.");
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
const idx = whitelist.findIndex((w) => normalizePhone(w) === normalized);
|
|
2290
|
+
if (idx === -1) {
|
|
2291
|
+
await reply(`${formatPhone(phone)} is not in the whitelist.`);
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
whitelist.splice(idx, 1);
|
|
2295
|
+
await this.persistWhitelist();
|
|
2296
|
+
await reply(`Removed ${formatPhone(phone)} from whitelist.`);
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
await reply("Usage: /whitelist | /whitelist add <phone> | /whitelist remove <phone>");
|
|
2300
|
+
}
|
|
2301
|
+
async handleSkillsCommand(message, args, reply) {
|
|
2302
|
+
const skills = Array.from(this.skills.values());
|
|
2303
|
+
const sub = args[0]?.toLowerCase();
|
|
2304
|
+
if (sub === "info") {
|
|
2305
|
+
const name = args.slice(1).join(" ").trim();
|
|
2306
|
+
if (!name) {
|
|
2307
|
+
await reply("Usage: /skills info <name>");
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
const skill = this.skills.get(name);
|
|
2311
|
+
if (!skill) {
|
|
2312
|
+
await reply(`Skill "${name}" not found. Available: ${skills.map((s) => s.name).join(", ") || "none"}`);
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
if (typeof skill.getContent === "function") {
|
|
2316
|
+
const cfg = skill.getConfig?.() || {};
|
|
2317
|
+
const content = skill.getContent() || "";
|
|
2318
|
+
const preview = content.length > 300 ? content.slice(0, 300) + "..." : content;
|
|
2319
|
+
const lines = [
|
|
2320
|
+
`📝 *Prompt Skill: ${skill.name}*`,
|
|
2321
|
+
` Status: ${skill.isReady() ? "ready" : "not ready"}`,
|
|
2322
|
+
` Type: prompt`
|
|
2323
|
+
];
|
|
2324
|
+
if (cfg.summary) lines.push(` Summary: ${cfg.summary}`);
|
|
2325
|
+
if (cfg.tags?.length) lines.push(` Tags: ${cfg.tags.join(", ")}`);
|
|
2326
|
+
if (cfg.author) lines.push(` Author: ${cfg.author}`);
|
|
2327
|
+
lines.push("", " Content:", ` ${preview}`);
|
|
2328
|
+
await reply(lines.join("\n"));
|
|
2329
|
+
} else {
|
|
2330
|
+
const tools = Object.values(skill.tools);
|
|
2331
|
+
const lines = [
|
|
2332
|
+
`🔧 *Skill: ${skill.name}*`,
|
|
2333
|
+
` Status: ${skill.isReady() ? "ready" : "not ready"}`,
|
|
2334
|
+
` Tools: ${tools.length}`
|
|
2335
|
+
];
|
|
2336
|
+
for (const tool of tools) {
|
|
2337
|
+
lines.push("");
|
|
2338
|
+
lines.push(` *${tool.name}*`);
|
|
2339
|
+
if (tool.description) lines.push(` ${tool.description}`);
|
|
2340
|
+
const params = Object.entries(tool.parameters);
|
|
2341
|
+
if (params.length > 0) {
|
|
2342
|
+
lines.push(" Parameters:");
|
|
2343
|
+
for (const [pName, pDef] of params) {
|
|
2344
|
+
const p = pDef;
|
|
2345
|
+
const req = p.required ? " (required)" : "";
|
|
2346
|
+
const type = p.type ? ` [${p.type}]` : "";
|
|
2347
|
+
lines.push(` ${pName}${type}${req}${p.description ? " — " + p.description : ""}`);
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
await reply(lines.join("\n"));
|
|
2352
|
+
}
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
if (sub === "catalog" || sub === "browse") {
|
|
2356
|
+
await this.handleSkillCatalog(args.slice(1), reply);
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
if (sub === "add") {
|
|
2360
|
+
await this.handleSkillAdd(message, args.slice(1), reply);
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
if (sub === "remove") {
|
|
2364
|
+
await this.handleSkillRemove(args.slice(1), reply);
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
if (skills.length === 0) {
|
|
2368
|
+
await reply("No skills registered. Use /skills catalog to browse available skills, or /create-skill to create a prompt skill.");
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
const lines = [`🔧 *Registered Skills (${skills.length}):*`];
|
|
2372
|
+
for (const skill of skills) {
|
|
2373
|
+
const status = skill.isReady() ? "✅" : "⏳";
|
|
2374
|
+
if (typeof skill.getContent === "function") lines.push(` ${status} 📝 ${skill.name} — prompt skill`);
|
|
2375
|
+
else {
|
|
2376
|
+
const toolNames = Object.keys(skill.tools);
|
|
2377
|
+
lines.push(` ${status} ${skill.name} — ${toolNames.length} tool${toolNames.length !== 1 ? "s" : ""}: ${toolNames.join(", ")}`);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
lines.push("");
|
|
2381
|
+
lines.push("Use /skills info <name> for details, /skills catalog to browse, /skills add <name> to install, /create-skill to create a prompt skill.");
|
|
2382
|
+
await reply(lines.join("\n"));
|
|
2383
|
+
}
|
|
2384
|
+
async handleCreateSkillCommand(message, args, reply) {
|
|
2385
|
+
const raw = args.join(" ");
|
|
2386
|
+
const pipeIdx = raw.indexOf("|");
|
|
2387
|
+
if (pipeIdx === -1 || !raw.trim()) {
|
|
2388
|
+
await reply("Usage: /create-skill <name> | <instructions>\n\nExample: /create-skill sentiment-monitor | Monitor customer sentiment and flag negative interactions for review.");
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
const name = raw.slice(0, pipeIdx).trim();
|
|
2392
|
+
const content = raw.slice(pipeIdx + 1).trim();
|
|
2393
|
+
if (!name || !content) {
|
|
2394
|
+
await reply("Both name and content are required.\nUsage: /create-skill <name> | <instructions>");
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
const mod = this.config.skillsModule;
|
|
2398
|
+
if (!mod) {
|
|
2399
|
+
await reply("Skills module not available. Make sure @operor/skills is installed.");
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
try {
|
|
2403
|
+
const config = mod.loadSkillsConfig();
|
|
2404
|
+
if (config.skills.find((s) => s.name === name)) {
|
|
2405
|
+
await reply(`A skill named "${name}" already exists. Remove it first or choose a different name.`);
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
const promptSkillConfig = {
|
|
2409
|
+
type: "prompt",
|
|
2410
|
+
name,
|
|
2411
|
+
content,
|
|
2412
|
+
enabled: true
|
|
2413
|
+
};
|
|
2414
|
+
config.skills.push(promptSkillConfig);
|
|
2415
|
+
mod.saveSkillsConfig(config);
|
|
2416
|
+
const agents = Array.from(this.agents.values());
|
|
2417
|
+
for (const agent of agents) {
|
|
2418
|
+
const cfg = agent.getConfig();
|
|
2419
|
+
if (cfg.skills && !cfg.skills.includes(name)) {
|
|
2420
|
+
cfg.skills.push(name);
|
|
2421
|
+
agent.config.skills = cfg.skills;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
for (const agent of agents) {
|
|
2425
|
+
await this.versionStore.saveVersion(agent.config.name, {
|
|
2426
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2427
|
+
instructions: agent.config._rawInstructions,
|
|
2428
|
+
identity: agent.config._rawIdentity,
|
|
2429
|
+
soul: agent.config._rawSoul,
|
|
2430
|
+
frontmatter: this.extractFrontmatter(agent.config),
|
|
2431
|
+
editedBy: message.from,
|
|
2432
|
+
changeDescription: `add skills: ${name}`
|
|
2433
|
+
});
|
|
2434
|
+
await this.writeAgentFrontmatter(agent.config.name, agent);
|
|
2435
|
+
}
|
|
2436
|
+
await reply(`📝 Prompt skill "${name}" created and added to your agent(s).\nSaving to mcp.json triggers hot-reload — skill is now active.`);
|
|
2437
|
+
} catch (err) {
|
|
2438
|
+
await reply(`Failed to create skill: ${err.message}`);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
async handleUpdateSkillCommand(message, args, reply) {
|
|
2442
|
+
const raw = args.join(" ");
|
|
2443
|
+
const pipeIdx = raw.indexOf("|");
|
|
2444
|
+
if (pipeIdx === -1 || !raw.trim()) {
|
|
2445
|
+
await reply("Usage: /update-skill <name> | <instructions>\n\nExample: /update-skill sentiment-monitor | Updated instructions for monitoring sentiment.");
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
const name = raw.slice(0, pipeIdx).trim();
|
|
2449
|
+
const content = raw.slice(pipeIdx + 1).trim();
|
|
2450
|
+
if (!name || !content) {
|
|
2451
|
+
await reply("Both name and content are required.\nUsage: /update-skill <name> | <instructions>");
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
const mod = this.config.skillsModule;
|
|
2455
|
+
if (!mod) {
|
|
2456
|
+
await reply("Skills module not available. Make sure @operor/skills is installed.");
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
try {
|
|
2460
|
+
const config = mod.loadSkillsConfig();
|
|
2461
|
+
const existing = config.skills.find((s) => s.name === name);
|
|
2462
|
+
if (!existing) {
|
|
2463
|
+
await reply(`Skill "${name}" not found. Use /skills to list registered skills.`);
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
if (existing.type !== "prompt") {
|
|
2467
|
+
await reply(`Skill "${name}" is not a prompt skill (type: ${existing.type}). Only prompt skills can be updated with this command.`);
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
existing.content = content;
|
|
2471
|
+
mod.saveSkillsConfig(config);
|
|
2472
|
+
const agents = Array.from(this.agents.values());
|
|
2473
|
+
for (const agent of agents) await this.versionStore.saveVersion(agent.config.name, {
|
|
2474
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2475
|
+
instructions: agent.config._rawInstructions,
|
|
2476
|
+
identity: agent.config._rawIdentity,
|
|
2477
|
+
soul: agent.config._rawSoul,
|
|
2478
|
+
frontmatter: this.extractFrontmatter(agent.config),
|
|
2479
|
+
editedBy: message.from,
|
|
2480
|
+
changeDescription: `update skill: ${name}`
|
|
2481
|
+
});
|
|
2482
|
+
await reply(`📝 Prompt skill "${name}" updated.\nSaving to mcp.json triggers hot-reload — changes are now active.`);
|
|
2483
|
+
} catch (err) {
|
|
2484
|
+
await reply(`Failed to update skill: ${err.message}`);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
async handleSkillCatalog(args, reply) {
|
|
2488
|
+
const mod = this.config.skillsModule;
|
|
2489
|
+
if (!mod) {
|
|
2490
|
+
await reply("Skill catalog is not available. Make sure @operor/skills is installed.");
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2493
|
+
const catalog = mod.loadSkillCatalog();
|
|
2494
|
+
const filter = args[0]?.toLowerCase();
|
|
2495
|
+
const validCategories = [
|
|
2496
|
+
"commerce",
|
|
2497
|
+
"payments",
|
|
2498
|
+
"crm",
|
|
2499
|
+
"support",
|
|
2500
|
+
"marketing",
|
|
2501
|
+
"search",
|
|
2502
|
+
"communication",
|
|
2503
|
+
"productivity"
|
|
2504
|
+
];
|
|
2505
|
+
let entries = catalog.skills;
|
|
2506
|
+
let categoryFilter;
|
|
2507
|
+
if (filter) if (validCategories.includes(filter)) {
|
|
2508
|
+
categoryFilter = filter;
|
|
2509
|
+
entries = entries.filter((s) => s.category === filter);
|
|
2510
|
+
} else entries = entries.filter((s) => s.name?.toLowerCase().includes(filter) || s.description?.toLowerCase().includes(filter) || s.category?.toLowerCase().includes(filter) || s.displayName?.toLowerCase().includes(filter));
|
|
2511
|
+
if (entries.length === 0) {
|
|
2512
|
+
await reply(filter ? `No skills found matching "${filter}".` : "Skill catalog is empty.");
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
const grouped = {};
|
|
2516
|
+
for (const entry of entries) {
|
|
2517
|
+
const cat = entry.category || "other";
|
|
2518
|
+
if (!grouped[cat]) grouped[cat] = [];
|
|
2519
|
+
grouped[cat].push(entry);
|
|
2520
|
+
}
|
|
2521
|
+
const lines = [`📦 *Skill Catalog (${entries.length} skills):*`];
|
|
2522
|
+
for (const [cat, catSkills] of Object.entries(grouped)) {
|
|
2523
|
+
lines.push("");
|
|
2524
|
+
lines.push(`*${cat.charAt(0).toUpperCase() + cat.slice(1)}:*`);
|
|
2525
|
+
for (const s of catSkills) {
|
|
2526
|
+
const installed = this.skills.has(s.name) ? " ✅" : "";
|
|
2527
|
+
const badge = s.maturity === "official" ? " 🏷️" : "";
|
|
2528
|
+
const typeTag = s.type === "prompt" ? " 📝" : "";
|
|
2529
|
+
lines.push(` ${s.displayName || s.name}${badge}${typeTag}${installed} — ${s.description}`);
|
|
2530
|
+
lines.push(` → /skills add ${s.name}`);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
lines.push("");
|
|
2534
|
+
if (!categoryFilter) lines.push(`Filter by category: /skills catalog <${validCategories.join("|")}>`);
|
|
2535
|
+
await reply(lines.join("\n"));
|
|
2536
|
+
}
|
|
2537
|
+
async handleSkillAdd(message, args, reply) {
|
|
2538
|
+
const name = args[0]?.trim();
|
|
2539
|
+
if (!name) {
|
|
2540
|
+
await reply("Usage: /skills add <name>\n\nBrowse available skills with /skills catalog");
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
if (this.skills.has(name)) {
|
|
2544
|
+
await reply(`Skill "${name}" is already installed and active.`);
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
const mod = this.config.skillsModule;
|
|
2548
|
+
if (!mod) {
|
|
2549
|
+
await reply("Skill catalog is not available. Make sure @operor/skills is installed.");
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
const root = this.config.projectRoot;
|
|
2553
|
+
if (mod.loadSkillsConfig(root).skills.some((s) => s.name === name)) {
|
|
2554
|
+
await reply(`Skill "${name}" already exists in mcp.json. Restart to activate.`);
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
const catalog = mod.loadSkillCatalog();
|
|
2558
|
+
const entry = mod.findSkillInCatalog(catalog, name);
|
|
2559
|
+
if (!entry) {
|
|
2560
|
+
const suggestions = catalog.skills.map((s) => s.name).filter((n) => n.includes(name) || name.includes(n));
|
|
2561
|
+
await reply(suggestions.length > 0 ? `Skill "${name}" not found. Did you mean: ${suggestions.join(", ")}?` : `Skill "${name}" not found in catalog. Use /skills catalog to browse available skills.`);
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
if (entry.type === "prompt") {
|
|
2565
|
+
await reply(`📝 *Adding prompt skill: ${entry.displayName}*\n ${entry.description}\n\nInstalling...`);
|
|
2566
|
+
await this.installSkillFromCatalog(entry, {}, reply);
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
const requiredKeys = Object.keys(entry.envVars || {}).filter((k) => entry.envVars[k].required);
|
|
2570
|
+
const lines = [
|
|
2571
|
+
`📦 *Adding: ${entry.displayName}*`,
|
|
2572
|
+
` ${entry.description}`,
|
|
2573
|
+
` Category: ${entry.category} | Maturity: ${entry.maturity}`,
|
|
2574
|
+
` Tools: ${(entry.tools || []).map((t) => t.name).join(", ")}`
|
|
2575
|
+
];
|
|
2576
|
+
if (entry.docsUrl) lines.push(` Docs: ${entry.docsUrl}`);
|
|
2577
|
+
if (requiredKeys.length === 0) {
|
|
2578
|
+
await reply(lines.join("\n") + "\n\nNo API keys required. Installing...");
|
|
2579
|
+
await this.installSkillFromCatalog(entry, {}, reply);
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
lines.push("");
|
|
2583
|
+
lines.push("*Required API keys:*");
|
|
2584
|
+
for (const key of requiredKeys) {
|
|
2585
|
+
const spec = entry.envVars[key];
|
|
2586
|
+
lines.push(` ${key} — ${spec.description}${spec.placeholder ? ` (e.g. ${spec.placeholder})` : ""}`);
|
|
2587
|
+
}
|
|
2588
|
+
lines.push("");
|
|
2589
|
+
lines.push(`Please send the value for *${requiredKeys[0]}*:`);
|
|
2590
|
+
lines.push("(Send \"cancel\" to abort)");
|
|
2591
|
+
this.pendingSkillAdds.set(message.from, {
|
|
2592
|
+
skillName: name,
|
|
2593
|
+
envVars: {},
|
|
2594
|
+
pendingKeys: [...requiredKeys],
|
|
2595
|
+
currentKey: requiredKeys[0],
|
|
2596
|
+
catalogEntry: entry,
|
|
2597
|
+
createdAt: Date.now()
|
|
2598
|
+
});
|
|
2599
|
+
await reply(lines.join("\n"));
|
|
2600
|
+
}
|
|
2601
|
+
async handlePendingSkillAdd(message, pending) {
|
|
2602
|
+
const reply = async (text) => {
|
|
2603
|
+
await this.sendMessage(message.from, message.provider, {
|
|
2604
|
+
to: message.from,
|
|
2605
|
+
text
|
|
2606
|
+
});
|
|
2607
|
+
};
|
|
2608
|
+
if (Date.now() - pending.createdAt > 300 * 1e3) {
|
|
2609
|
+
this.pendingSkillAdds.delete(message.from);
|
|
2610
|
+
await reply("Skill add session expired (5 min timeout). Send /skills add <name> to try again.");
|
|
2611
|
+
return true;
|
|
2612
|
+
}
|
|
2613
|
+
const text = message.text.trim();
|
|
2614
|
+
if (text.toLowerCase() === "cancel" || text.toLowerCase() === "/cancel") {
|
|
2615
|
+
this.pendingSkillAdds.delete(message.from);
|
|
2616
|
+
await reply("Skill installation cancelled.");
|
|
2617
|
+
return true;
|
|
2618
|
+
}
|
|
2619
|
+
if (!pending.currentKey) {
|
|
2620
|
+
this.pendingSkillAdds.delete(message.from);
|
|
2621
|
+
return false;
|
|
2622
|
+
}
|
|
2623
|
+
pending.envVars[pending.currentKey] = text;
|
|
2624
|
+
pending.pendingKeys.shift();
|
|
2625
|
+
if (pending.pendingKeys.length > 0) {
|
|
2626
|
+
pending.currentKey = pending.pendingKeys[0];
|
|
2627
|
+
const spec = pending.catalogEntry.envVars[pending.currentKey];
|
|
2628
|
+
await reply(`Got it. Now send the value for *${pending.currentKey}*:${spec?.placeholder ? ` (e.g. ${spec.placeholder})` : ""}`);
|
|
2629
|
+
return true;
|
|
2630
|
+
}
|
|
2631
|
+
this.pendingSkillAdds.delete(message.from);
|
|
2632
|
+
await reply("All keys received. Installing skill...");
|
|
2633
|
+
await this.installSkillFromCatalog(pending.catalogEntry, pending.envVars, reply);
|
|
2634
|
+
return true;
|
|
2635
|
+
}
|
|
2636
|
+
async installSkillFromCatalog(entry, envVars, reply) {
|
|
2637
|
+
try {
|
|
2638
|
+
const mod = this.config.skillsModule;
|
|
2639
|
+
if (!mod) {
|
|
2640
|
+
await reply("Skill catalog is not available.");
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
const config = mod.catalogEntryToConfig(entry);
|
|
2644
|
+
if (config.env && Object.keys(envVars).length > 0) for (const [key, value] of Object.entries(envVars)) config.env[key] = value;
|
|
2645
|
+
const root = this.config.projectRoot;
|
|
2646
|
+
const skillsConfig = mod.loadSkillsConfig(root);
|
|
2647
|
+
const existingIdx = skillsConfig.skills.findIndex((s) => s.name === config.name);
|
|
2648
|
+
if (existingIdx >= 0) skillsConfig.skills[existingIdx] = config;
|
|
2649
|
+
else skillsConfig.skills.push(config);
|
|
2650
|
+
mod.saveSkillsConfig(skillsConfig, root);
|
|
2651
|
+
const agents = Array.from(this.agents.values());
|
|
2652
|
+
for (const agent of agents) {
|
|
2653
|
+
const cfg = agent.getConfig();
|
|
2654
|
+
if (cfg.skills && !cfg.skills.includes(config.name)) {
|
|
2655
|
+
cfg.skills.push(config.name);
|
|
2656
|
+
agent.config.skills = cfg.skills;
|
|
2657
|
+
}
|
|
2658
|
+
await this.writeAgentFrontmatter(agent.config.name, agent);
|
|
2659
|
+
}
|
|
2660
|
+
await reply(`✅ *${entry.displayName}* added to mcp.json and enabled.\n\nRestart the agent to activate the skill, or use /reload if available.`);
|
|
2661
|
+
} catch (err) {
|
|
2662
|
+
await reply(`Failed to install skill: ${err.message}`);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
async handleSkillRemove(args, reply) {
|
|
2666
|
+
const name = args[0]?.trim();
|
|
2667
|
+
if (!name) {
|
|
2668
|
+
await reply("Usage: /skills remove <name>");
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
try {
|
|
2672
|
+
const mod = this.config.skillsModule;
|
|
2673
|
+
if (!mod) {
|
|
2674
|
+
await reply("Skill catalog is not available.");
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
const root = this.config.projectRoot;
|
|
2678
|
+
const skillsConfig = mod.loadSkillsConfig(root);
|
|
2679
|
+
const idx = skillsConfig.skills.findIndex((s) => s.name === name);
|
|
2680
|
+
if (idx < 0) {
|
|
2681
|
+
await reply(`Skill "${name}" not found in mcp.json. Configured: ${skillsConfig.skills.map((s) => s.name).join(", ") || "none"}`);
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
skillsConfig.skills.splice(idx, 1);
|
|
2685
|
+
mod.saveSkillsConfig(skillsConfig, root);
|
|
2686
|
+
if (this.skills.has(name)) await this.removeSkill(name);
|
|
2687
|
+
const agents = Array.from(this.agents.values());
|
|
2688
|
+
for (const agent of agents) {
|
|
2689
|
+
const cfg = agent.getConfig();
|
|
2690
|
+
if (cfg.skills) {
|
|
2691
|
+
const skillIdx = cfg.skills.indexOf(name);
|
|
2692
|
+
if (skillIdx >= 0) {
|
|
2693
|
+
cfg.skills.splice(skillIdx, 1);
|
|
2694
|
+
agent.config.skills = cfg.skills;
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
await this.writeAgentFrontmatter(agent.config.name, agent);
|
|
2698
|
+
}
|
|
2699
|
+
await reply(`✅ Skill "${name}" removed from mcp.json.`);
|
|
2700
|
+
} catch (err) {
|
|
2701
|
+
await reply(`Failed to remove skill: ${err.message}`);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
async persistWhitelist() {
|
|
2705
|
+
const whitelist = this.config.trainingMode?.whitelist;
|
|
2706
|
+
if (!whitelist) return;
|
|
2707
|
+
if (this.memory.setSetting) await this.memory.setSetting("training_whitelist", whitelist.join(","));
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Format a date string (YYYY-MM-DD) to a day name (e.g. "Monday")
|
|
2711
|
+
*/
|
|
2712
|
+
formatDayName(dateStr) {
|
|
2713
|
+
try {
|
|
2714
|
+
return (/* @__PURE__ */ new Date(dateStr + "T00:00:00")).toLocaleDateString("en-US", { weekday: "long" });
|
|
2715
|
+
} catch {
|
|
2716
|
+
return dateStr;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
/**
|
|
2720
|
+
* Log helper
|
|
2721
|
+
*/
|
|
2722
|
+
getFirstAgent() {
|
|
2723
|
+
return Array.from(this.agents.values())[0] ?? null;
|
|
2724
|
+
}
|
|
2725
|
+
rebuildAgentSystemPrompt(agent) {
|
|
2726
|
+
agent.config.systemPrompt = buildSystemPrompt({
|
|
2727
|
+
identity: agent.config._rawIdentity || null,
|
|
2728
|
+
soul: agent.config._rawSoul || null,
|
|
2729
|
+
body: agent.config._rawInstructions || "",
|
|
2730
|
+
userContext: null,
|
|
2731
|
+
guardrails: agent.config.guardrails
|
|
2732
|
+
});
|
|
2733
|
+
}
|
|
2734
|
+
async writeAgentFiles(agentName, agent) {
|
|
2735
|
+
const agentsDir = this.config.agentsDir;
|
|
2736
|
+
if (!agentsDir) return;
|
|
2737
|
+
const agentDir = path.join(agentsDir, agentName);
|
|
2738
|
+
await fs.mkdir(agentDir, { recursive: true });
|
|
2739
|
+
const fm = this.extractFrontmatter(agent.config);
|
|
2740
|
+
const yamlFm = yaml.dump(fm, {
|
|
2741
|
+
lineWidth: -1,
|
|
2742
|
+
noRefs: true
|
|
2743
|
+
}).trim();
|
|
2744
|
+
const body = agent.config._rawInstructions || "";
|
|
2745
|
+
await fs.writeFile(path.join(agentDir, "INSTRUCTIONS.md"), `---\n${yamlFm}\n---\n\n${body.trim()}\n`, "utf-8");
|
|
2746
|
+
if (agent.config._rawIdentity) await fs.writeFile(path.join(agentDir, "IDENTITY.md"), agent.config._rawIdentity.trim() + "\n", "utf-8");
|
|
2747
|
+
if (agent.config._rawSoul) await fs.writeFile(path.join(agentDir, "SOUL.md"), agent.config._rawSoul.trim() + "\n", "utf-8");
|
|
2748
|
+
}
|
|
2749
|
+
async writeAgentFrontmatter(agentName, agent) {
|
|
2750
|
+
const agentsDir = this.config.agentsDir;
|
|
2751
|
+
if (!agentsDir) return;
|
|
2752
|
+
const agentDir = path.join(agentsDir, agentName);
|
|
2753
|
+
await fs.mkdir(agentDir, { recursive: true });
|
|
2754
|
+
const fm = this.extractFrontmatter(agent.config);
|
|
2755
|
+
const yamlFm = yaml.dump(fm, {
|
|
2756
|
+
lineWidth: -1,
|
|
2757
|
+
noRefs: true
|
|
2758
|
+
}).trim();
|
|
2759
|
+
const body = agent.config._rawInstructions || "";
|
|
2760
|
+
await fs.writeFile(path.join(agentDir, "INSTRUCTIONS.md"), `---\n${yamlFm}\n---\n\n${body.trim()}\n`, "utf-8");
|
|
2761
|
+
}
|
|
2762
|
+
extractSection(text, sectionName) {
|
|
2763
|
+
return this.parseSections(text).find((s) => s.heading.toLowerCase().includes(sectionName.toLowerCase()))?.content || null;
|
|
2764
|
+
}
|
|
2765
|
+
replaceSection(text, sectionName, newContent) {
|
|
2766
|
+
const sections = this.parseSections(text);
|
|
2767
|
+
const idx = sections.findIndex((s) => s.heading.toLowerCase().includes(sectionName.toLowerCase()));
|
|
2768
|
+
if (idx === -1) return text;
|
|
2769
|
+
sections[idx].content = newContent;
|
|
2770
|
+
return sections.map((s) => `${s.heading}\n\n${s.content}`).join("\n\n");
|
|
2771
|
+
}
|
|
2772
|
+
parseSections(text) {
|
|
2773
|
+
const lines = text.split("\n");
|
|
2774
|
+
const sections = [];
|
|
2775
|
+
let currentHeading = null;
|
|
2776
|
+
let currentContent = [];
|
|
2777
|
+
for (const line of lines) if (line.match(/^##\s+/)) {
|
|
2778
|
+
if (currentHeading) sections.push({
|
|
2779
|
+
heading: currentHeading,
|
|
2780
|
+
content: currentContent.join("\n").trim()
|
|
2781
|
+
});
|
|
2782
|
+
currentHeading = line;
|
|
2783
|
+
currentContent = [];
|
|
2784
|
+
} else if (currentHeading) currentContent.push(line);
|
|
2785
|
+
if (currentHeading) sections.push({
|
|
2786
|
+
heading: currentHeading,
|
|
2787
|
+
content: currentContent.join("\n").trim()
|
|
2788
|
+
});
|
|
2789
|
+
return sections;
|
|
2790
|
+
}
|
|
2791
|
+
extractFrontmatter(config) {
|
|
2792
|
+
const fm = { name: config.name };
|
|
2793
|
+
if (config.purpose) fm.purpose = config.purpose;
|
|
2794
|
+
if (config.triggers?.length) fm.triggers = config.triggers;
|
|
2795
|
+
if (config.channels?.length) fm.channels = config.channels;
|
|
2796
|
+
if (config.skills?.length) fm.skills = config.skills;
|
|
2797
|
+
if (config.knowledgeBase != null) fm.knowledgeBase = config.knowledgeBase;
|
|
2798
|
+
if (config.priority != null) fm.priority = config.priority;
|
|
2799
|
+
if (config.escalateTo) fm.escalateTo = config.escalateTo;
|
|
2800
|
+
if (config.guardrails) fm.guardrails = config.guardrails;
|
|
2801
|
+
return fm;
|
|
2802
|
+
}
|
|
2803
|
+
log(message) {
|
|
2804
|
+
if (this.config.debug) console.log(`[Operor] ${message}`);
|
|
2805
|
+
}
|
|
2806
|
+
};
|
|
2807
|
+
|
|
2808
|
+
//#endregion
|
|
2809
|
+
//#region src/LLMIntentClassifier.ts
|
|
2810
|
+
/**
|
|
2811
|
+
* LLM-based intent classifier.
|
|
2812
|
+
* Uses an LLM to classify user messages based on agent configurations.
|
|
2813
|
+
* Requires an LLM provider (AIProvider from @operor/llm).
|
|
2814
|
+
*/
|
|
2815
|
+
var LLMIntentClassifier = class {
|
|
2816
|
+
constructor(llm) {
|
|
2817
|
+
this.llm = llm;
|
|
2818
|
+
}
|
|
2819
|
+
async classify(message, agents, history) {
|
|
2820
|
+
const systemPrompt = `You are an intent classifier for a customer service system. Classify the user message into one of these agent categories:
|
|
2821
|
+
|
|
2822
|
+
${agents.map((a) => `- ${a.name}: triggers=[${a.triggers?.join(", ")}], purpose="${a.purpose || "N/A"}"`).join("\n")}
|
|
2823
|
+
|
|
2824
|
+
Respond with JSON only (no markdown, no code blocks):
|
|
2825
|
+
{
|
|
2826
|
+
"intent": "trigger_name",
|
|
2827
|
+
"confidence": 0.0-1.0,
|
|
2828
|
+
"entities": {}
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
Choose the most appropriate trigger from the list above. If none match well, use "general".`;
|
|
2832
|
+
try {
|
|
2833
|
+
const jsonMatch = (await this.llm.complete([{
|
|
2834
|
+
role: "system",
|
|
2835
|
+
content: systemPrompt
|
|
2836
|
+
}, {
|
|
2837
|
+
role: "user",
|
|
2838
|
+
content: message
|
|
2839
|
+
}], {
|
|
2840
|
+
maxTokens: 150,
|
|
2841
|
+
temperature: 0
|
|
2842
|
+
})).text.trim().match(/\{[\s\S]*\}/);
|
|
2843
|
+
if (!jsonMatch) throw new Error("No JSON found in LLM response");
|
|
2844
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
2845
|
+
return {
|
|
2846
|
+
intent: parsed.intent || "general",
|
|
2847
|
+
confidence: parsed.confidence || .5,
|
|
2848
|
+
entities: parsed.entities || {}
|
|
2849
|
+
};
|
|
2850
|
+
} catch (error) {
|
|
2851
|
+
console.error("LLM intent classification failed:", error);
|
|
2852
|
+
return {
|
|
2853
|
+
intent: "general",
|
|
2854
|
+
confidence: .3,
|
|
2855
|
+
entities: {}
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
};
|
|
2860
|
+
|
|
2861
|
+
//#endregion
|
|
2862
|
+
export { Agent, AgentLoader, AgentVersionStore, GuardrailEngine, InMemoryStore, KeywordIntentClassifier, LLMIntentClassifier, Operor, buildSystemPrompt, readFileOrNull };
|
|
2863
|
+
//# sourceMappingURL=index.js.map
|