@projectservan8n/cnapse 0.8.2 → 0.10.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.
@@ -0,0 +1,489 @@
1
+ /**
2
+ * Agent Learner - Self-learning system that consults multiple AI sources
3
+ * and remembers successful solutions for future use
4
+ */
5
+
6
+ import { EventEmitter } from 'events';
7
+ import { homedir } from 'os';
8
+ import { join } from 'path';
9
+ import { readFile, writeFile, mkdir } from 'fs/promises';
10
+ import { existsSync } from 'fs';
11
+ import { describeScreen, captureScreenshot } from '../lib/vision.js';
12
+ import { chat } from '../lib/api.js';
13
+ import * as computer from '../tools/computer.js';
14
+
15
+ const MEMORY_FILE = join(homedir(), '.cnapse', 'agent-memory.json');
16
+ const MAX_MEMORY_SIZE = 500; // Max learned actions to keep
17
+
18
+ export interface LearnedAction {
19
+ id: string;
20
+ situation: string; // What the screen looked like
21
+ goal: string; // What we were trying to do
22
+ solution: string; // What worked (action description)
23
+ actionType: string; // Action type (click, type, navigate, etc.)
24
+ actionValue: string; // Action value/params
25
+ source: string; // Where we learned it (perplexity, claude, chatgpt, web, self)
26
+ successCount: number; // How many times this worked
27
+ failCount: number; // How many times this failed
28
+ lastUsed: string; // ISO timestamp
29
+ created: string; // ISO timestamp
30
+ }
31
+
32
+ export interface Suggestion {
33
+ action: string;
34
+ value: string;
35
+ reasoning: string;
36
+ source: string;
37
+ confidence: number;
38
+ }
39
+
40
+ export interface AgentMemory {
41
+ version: number;
42
+ learned: LearnedAction[];
43
+ stats: {
44
+ totalAttempts: number;
45
+ totalSuccesses: number;
46
+ totalLearned: number;
47
+ sourceCounts: Record<string, number>;
48
+ };
49
+ }
50
+
51
+ export class AgentLearner extends EventEmitter {
52
+ private memory: AgentMemory;
53
+ private loaded: boolean = false;
54
+
55
+ constructor() {
56
+ super();
57
+ this.memory = this.createEmptyMemory();
58
+ }
59
+
60
+ private createEmptyMemory(): AgentMemory {
61
+ return {
62
+ version: 1,
63
+ learned: [],
64
+ stats: {
65
+ totalAttempts: 0,
66
+ totalSuccesses: 0,
67
+ totalLearned: 0,
68
+ sourceCounts: {},
69
+ },
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Load memory from disk
75
+ */
76
+ async load(): Promise<void> {
77
+ if (this.loaded) return;
78
+
79
+ try {
80
+ const dir = join(homedir(), '.cnapse');
81
+ if (!existsSync(dir)) {
82
+ await mkdir(dir, { recursive: true });
83
+ }
84
+
85
+ if (existsSync(MEMORY_FILE)) {
86
+ const data = await readFile(MEMORY_FILE, 'utf-8');
87
+ this.memory = JSON.parse(data);
88
+ }
89
+ this.loaded = true;
90
+ this.emit('loaded', this.memory.learned.length);
91
+ } catch (error) {
92
+ console.error('Failed to load agent memory:', error);
93
+ this.memory = this.createEmptyMemory();
94
+ this.loaded = true;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Save memory to disk
100
+ */
101
+ async save(): Promise<void> {
102
+ try {
103
+ const dir = join(homedir(), '.cnapse');
104
+ if (!existsSync(dir)) {
105
+ await mkdir(dir, { recursive: true });
106
+ }
107
+
108
+ // Prune if too large (keep best performers)
109
+ if (this.memory.learned.length > MAX_MEMORY_SIZE) {
110
+ this.memory.learned.sort((a, b) => {
111
+ const aScore = a.successCount - a.failCount;
112
+ const bScore = b.successCount - b.failCount;
113
+ return bScore - aScore;
114
+ });
115
+ this.memory.learned = this.memory.learned.slice(0, MAX_MEMORY_SIZE);
116
+ }
117
+
118
+ await writeFile(MEMORY_FILE, JSON.stringify(this.memory, null, 2));
119
+ this.emit('saved', this.memory.learned.length);
120
+ } catch (error) {
121
+ console.error('Failed to save agent memory:', error);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Check if we've solved something similar before
127
+ */
128
+ async recall(goal: string, currentScreen: string): Promise<LearnedAction | null> {
129
+ await this.load();
130
+
131
+ const goalLower = goal.toLowerCase();
132
+ const screenLower = currentScreen.toLowerCase();
133
+
134
+ // Find candidates with similar goals and situations
135
+ const candidates = this.memory.learned.filter(m => {
136
+ const goalSimilarity = this.calculateSimilarity(goalLower, m.goal.toLowerCase());
137
+ const screenSimilarity = this.calculateSimilarity(screenLower, m.situation.toLowerCase());
138
+
139
+ // Need reasonable match on goal and some match on screen
140
+ return goalSimilarity > 0.5 && screenSimilarity > 0.3;
141
+ });
142
+
143
+ if (candidates.length === 0) return null;
144
+
145
+ // Sort by success rate and recency
146
+ candidates.sort((a, b) => {
147
+ const aScore = (a.successCount - a.failCount) + (a.successCount * 0.1);
148
+ const bScore = (b.successCount - b.failCount) + (b.successCount * 0.1);
149
+ return bScore - aScore;
150
+ });
151
+
152
+ const best = candidates[0];
153
+
154
+ // Only return if it has a positive track record
155
+ if (best.successCount > best.failCount) {
156
+ this.emit('recalled', best);
157
+ return best;
158
+ }
159
+
160
+ return null;
161
+ }
162
+
163
+ /**
164
+ * Simple word-based similarity (Jaccard-like)
165
+ */
166
+ private calculateSimilarity(a: string, b: string): number {
167
+ const wordsA = new Set(a.split(/\s+/).filter(w => w.length > 2));
168
+ const wordsB = new Set(b.split(/\s+/).filter(w => w.length > 2));
169
+
170
+ if (wordsA.size === 0 || wordsB.size === 0) return 0;
171
+
172
+ let intersection = 0;
173
+ for (const word of wordsA) {
174
+ if (wordsB.has(word)) intersection++;
175
+ }
176
+
177
+ const union = wordsA.size + wordsB.size - intersection;
178
+ return intersection / union;
179
+ }
180
+
181
+ /**
182
+ * Learn from a successful action
183
+ */
184
+ async learn(
185
+ situation: string,
186
+ goal: string,
187
+ actionType: string,
188
+ actionValue: string,
189
+ source: string
190
+ ): Promise<void> {
191
+ await this.load();
192
+
193
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
194
+ const now = new Date().toISOString();
195
+
196
+ // Check if we already have a similar entry
197
+ const existing = this.memory.learned.find(m =>
198
+ m.goal.toLowerCase() === goal.toLowerCase() &&
199
+ m.actionType === actionType &&
200
+ m.actionValue === actionValue
201
+ );
202
+
203
+ if (existing) {
204
+ existing.successCount++;
205
+ existing.lastUsed = now;
206
+ this.emit('reinforced', existing);
207
+ } else {
208
+ const learned: LearnedAction = {
209
+ id,
210
+ situation: situation.slice(0, 500), // Truncate long descriptions
211
+ goal,
212
+ solution: `${actionType}: ${actionValue}`,
213
+ actionType,
214
+ actionValue,
215
+ source,
216
+ successCount: 1,
217
+ failCount: 0,
218
+ lastUsed: now,
219
+ created: now,
220
+ };
221
+
222
+ this.memory.learned.push(learned);
223
+ this.memory.stats.totalLearned++;
224
+ this.memory.stats.sourceCounts[source] = (this.memory.stats.sourceCounts[source] || 0) + 1;
225
+ this.emit('learned', learned);
226
+ }
227
+
228
+ await this.save();
229
+ }
230
+
231
+ /**
232
+ * Record a failed attempt
233
+ */
234
+ async recordFailure(goal: string, actionType: string, actionValue: string): Promise<void> {
235
+ await this.load();
236
+
237
+ const existing = this.memory.learned.find(m =>
238
+ m.goal.toLowerCase() === goal.toLowerCase() &&
239
+ m.actionType === actionType &&
240
+ m.actionValue === actionValue
241
+ );
242
+
243
+ if (existing) {
244
+ existing.failCount++;
245
+ await this.save();
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Get help from multiple AI sources when stuck
251
+ */
252
+ async getHelp(
253
+ goal: string,
254
+ currentScreen: string,
255
+ triedActions: string[]
256
+ ): Promise<Suggestion[]> {
257
+ this.emit('seeking_help', { goal, triedActions: triedActions.length });
258
+
259
+ const query = this.buildHelpQuery(goal, currentScreen, triedActions);
260
+ const suggestions: Suggestion[] = [];
261
+
262
+ // Try multiple sources in parallel
263
+ const sources = await Promise.allSettled([
264
+ this.askOwnAI(query),
265
+ this.askPerplexity(goal, currentScreen),
266
+ this.askWebSearch(goal),
267
+ ]);
268
+
269
+ for (const result of sources) {
270
+ if (result.status === 'fulfilled' && result.value) {
271
+ suggestions.push(result.value);
272
+ }
273
+ }
274
+
275
+ // Sort by confidence
276
+ suggestions.sort((a, b) => b.confidence - a.confidence);
277
+
278
+ this.emit('got_help', suggestions.length);
279
+ return suggestions;
280
+ }
281
+
282
+ private buildHelpQuery(goal: string, screen: string, tried: string[]): string {
283
+ return `I'm trying to: ${goal}
284
+
285
+ Current screen shows: ${screen.slice(0, 500)}
286
+
287
+ Actions I've already tried:
288
+ ${tried.length > 0 ? tried.slice(-5).join('\n') : 'None yet'}
289
+
290
+ What's the SINGLE next action I should take?
291
+ Be very specific - tell me exactly what to click, what to type, or what key to press.
292
+
293
+ Respond in this format:
294
+ ACTION: <click|type|press|navigate|scroll|wait>
295
+ VALUE: <what to click on / text to type / key to press / URL to navigate to>
296
+ REASONING: <brief explanation>`;
297
+ }
298
+
299
+ /**
300
+ * Ask our configured AI provider for help
301
+ */
302
+ private async askOwnAI(query: string): Promise<Suggestion | null> {
303
+ try {
304
+ const response = await chat([{ role: 'user', content: query }]);
305
+ return this.parseSuggestion(response.content, 'own_ai', 0.8);
306
+ } catch (error) {
307
+ return null;
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Ask Perplexity by opening browser (uses existing browser.ts)
313
+ */
314
+ private async askPerplexity(goal: string, screen: string): Promise<Suggestion | null> {
315
+ try {
316
+ // Import browser module dynamically to avoid circular deps
317
+ const browser = await import('../services/browser.js');
318
+
319
+ const question = `How do I ${goal}? I can see ${screen.slice(0, 200)}. Give me the exact next step.`;
320
+ const result = await browser.askAI('perplexity', question);
321
+
322
+ if (result.response) {
323
+ return {
324
+ action: 'suggested',
325
+ value: result.response.slice(0, 500),
326
+ reasoning: 'From Perplexity web search',
327
+ source: 'perplexity',
328
+ confidence: 0.7,
329
+ };
330
+ }
331
+ } catch (error) {
332
+ // Perplexity might not be available
333
+ }
334
+ return null;
335
+ }
336
+
337
+ /**
338
+ * Search Google for help
339
+ */
340
+ private async askWebSearch(goal: string): Promise<Suggestion | null> {
341
+ try {
342
+ const browser = await import('../services/browser.js');
343
+ const searchQuery = `how to ${goal} step by step`;
344
+ const result = await browser.webSearch(searchQuery);
345
+
346
+ if (result.description) {
347
+ return {
348
+ action: 'suggested',
349
+ value: result.description.slice(0, 500),
350
+ reasoning: 'From web search results',
351
+ source: 'google',
352
+ confidence: 0.5,
353
+ };
354
+ }
355
+ } catch (error) {
356
+ // Web search might fail
357
+ }
358
+ return null;
359
+ }
360
+
361
+ /**
362
+ * Parse AI response into structured suggestion
363
+ */
364
+ private parseSuggestion(content: string, source: string, baseConfidence: number): Suggestion | null {
365
+ const actionMatch = content.match(/ACTION:\s*(\w+)/i);
366
+ const valueMatch = content.match(/VALUE:\s*(.+?)(?:\n|$)/i);
367
+ const reasoningMatch = content.match(/REASONING:\s*(.+?)(?:\n|$)/i);
368
+
369
+ if (!actionMatch) return null;
370
+
371
+ return {
372
+ action: actionMatch[1].toLowerCase(),
373
+ value: valueMatch?.[1]?.trim() || '',
374
+ reasoning: reasoningMatch?.[1]?.trim() || 'No reasoning provided',
375
+ source,
376
+ confidence: baseConfidence,
377
+ };
378
+ }
379
+
380
+ /**
381
+ * Ask Claude.ai via browser automation
382
+ */
383
+ async askClaude(query: string): Promise<Suggestion | null> {
384
+ try {
385
+ const browser = await import('../services/browser.js');
386
+
387
+ // Open Claude.ai
388
+ await browser.openUrl('https://claude.ai');
389
+ await this.sleep(3000);
390
+
391
+ // Type the query
392
+ await computer.typeText(query);
393
+ await this.sleep(500);
394
+ await computer.pressKey('Return');
395
+
396
+ // Wait for response
397
+ await this.sleep(8000);
398
+
399
+ // Capture and analyze the response
400
+ const screen = await describeScreen();
401
+
402
+ return {
403
+ action: 'suggested',
404
+ value: screen.description.slice(0, 500),
405
+ reasoning: 'From Claude.ai',
406
+ source: 'claude_web',
407
+ confidence: 0.75,
408
+ };
409
+ } catch (error) {
410
+ return null;
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Ask ChatGPT via browser automation
416
+ */
417
+ async askChatGPT(query: string): Promise<Suggestion | null> {
418
+ try {
419
+ const browser = await import('../services/browser.js');
420
+
421
+ // Open ChatGPT
422
+ await browser.openUrl('https://chat.openai.com');
423
+ await this.sleep(3000);
424
+
425
+ // Type the query
426
+ await computer.typeText(query);
427
+ await this.sleep(500);
428
+ await computer.pressKey('Return');
429
+
430
+ // Wait for response
431
+ await this.sleep(8000);
432
+
433
+ // Capture and analyze the response
434
+ const screen = await describeScreen();
435
+
436
+ return {
437
+ action: 'suggested',
438
+ value: screen.description.slice(0, 500),
439
+ reasoning: 'From ChatGPT',
440
+ source: 'chatgpt_web',
441
+ confidence: 0.75,
442
+ };
443
+ } catch (error) {
444
+ return null;
445
+ }
446
+ }
447
+
448
+ private sleep(ms: number): Promise<void> {
449
+ return new Promise(resolve => setTimeout(resolve, ms));
450
+ }
451
+
452
+ /**
453
+ * Get memory stats
454
+ */
455
+ getStats(): AgentMemory['stats'] & { memorySize: number } {
456
+ return {
457
+ ...this.memory.stats,
458
+ memorySize: this.memory.learned.length,
459
+ };
460
+ }
461
+
462
+ /**
463
+ * Get all learned actions (for debugging/display)
464
+ */
465
+ getAllLearned(): LearnedAction[] {
466
+ return [...this.memory.learned];
467
+ }
468
+
469
+ /**
470
+ * Clear all memory (for testing)
471
+ */
472
+ async clearMemory(): Promise<void> {
473
+ this.memory = this.createEmptyMemory();
474
+ await this.save();
475
+ this.emit('cleared');
476
+ }
477
+ }
478
+
479
+ // Singleton instance
480
+ let learnerInstance: AgentLearner | null = null;
481
+
482
+ export function getLearner(): AgentLearner {
483
+ if (!learnerInstance) {
484
+ learnerInstance = new AgentLearner();
485
+ }
486
+ return learnerInstance;
487
+ }
488
+
489
+ export default AgentLearner;