supaclaw 1.0.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/LICENSE +21 -0
- package/README.md +871 -0
- package/SCHEMA.md +215 -0
- package/dist/clawdbot-integration.d.ts +171 -0
- package/dist/clawdbot-integration.js +339 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1815 -0
- package/dist/context-manager.d.ts +143 -0
- package/dist/context-manager.js +360 -0
- package/dist/error-handling.d.ts +100 -0
- package/dist/error-handling.js +301 -0
- package/dist/index.d.ts +735 -0
- package/dist/index.js +2256 -0
- package/dist/parsers.d.ts +115 -0
- package/dist/parsers.js +406 -0
- package/migrations/001_initial.sql +153 -0
- package/migrations/002_vector_search.sql +219 -0
- package/migrations/003_entity_relationships.sql +143 -0
- package/package.json +66 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.batchWithErrorHandling = exports.gracefulFallback = exports.withTimeout = exports.safeJsonParse = exports.validateInput = exports.wrapEmbeddingOperation = exports.wrapDatabaseOperation = exports.retry = exports.CircuitBreaker = exports.RateLimitError = exports.ValidationError = exports.EmbeddingError = exports.DatabaseError = exports.OpenClawError = exports.createLoggingMiddleware = exports.createClawdbotIntegration = exports.ClawdbotMemoryIntegration = exports.estimateTokensAccurate = exports.estimateTokens = exports.getBudgetForModel = exports.getContextStats = exports.formatContextWindow = exports.buildContextWindow = exports.createAdaptiveBudget = exports.createContextBudget = exports.OpenClawMemory = void 0;
|
|
7
|
+
const supabase_js_1 = require("@supabase/supabase-js");
|
|
8
|
+
const openai_1 = __importDefault(require("openai"));
|
|
9
|
+
const context_manager_1 = require("./context-manager");
|
|
10
|
+
class OpenClawMemory {
|
|
11
|
+
supabase;
|
|
12
|
+
agentId;
|
|
13
|
+
config;
|
|
14
|
+
openai;
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.supabase = (0, supabase_js_1.createClient)(config.supabaseUrl, config.supabaseKey);
|
|
17
|
+
this.agentId = config.agentId;
|
|
18
|
+
this.config = config;
|
|
19
|
+
// Initialize OpenAI if API key provided
|
|
20
|
+
if (config.openaiApiKey) {
|
|
21
|
+
this.openai = new openai_1.default({ apiKey: config.openaiApiKey });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Generate embedding for text using configured provider
|
|
26
|
+
*/
|
|
27
|
+
async generateEmbedding(text) {
|
|
28
|
+
if (!this.config.embeddingProvider || this.config.embeddingProvider === 'none') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (this.config.embeddingProvider === 'openai') {
|
|
32
|
+
if (!this.openai) {
|
|
33
|
+
throw new Error('OpenAI API key not provided');
|
|
34
|
+
}
|
|
35
|
+
const model = this.config.embeddingModel || 'text-embedding-3-small';
|
|
36
|
+
const response = await this.openai.embeddings.create({
|
|
37
|
+
model,
|
|
38
|
+
input: text,
|
|
39
|
+
});
|
|
40
|
+
return response.data[0].embedding;
|
|
41
|
+
}
|
|
42
|
+
// TODO: Add Voyage AI support
|
|
43
|
+
if (this.config.embeddingProvider === 'voyage') {
|
|
44
|
+
throw new Error('Voyage AI embeddings not yet implemented');
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Calculate cosine similarity between two vectors
|
|
50
|
+
*/
|
|
51
|
+
cosineSimilarity(a, b) {
|
|
52
|
+
if (a.length !== b.length) {
|
|
53
|
+
throw new Error('Vectors must have the same length');
|
|
54
|
+
}
|
|
55
|
+
let dotProduct = 0;
|
|
56
|
+
let normA = 0;
|
|
57
|
+
let normB = 0;
|
|
58
|
+
for (let i = 0; i < a.length; i++) {
|
|
59
|
+
dotProduct += a[i] * b[i];
|
|
60
|
+
normA += a[i] * a[i];
|
|
61
|
+
normB += b[i] * b[i];
|
|
62
|
+
}
|
|
63
|
+
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
64
|
+
if (magnitude === 0)
|
|
65
|
+
return 0;
|
|
66
|
+
return dotProduct / magnitude;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Initialize database tables (run once)
|
|
70
|
+
*/
|
|
71
|
+
async initialize() {
|
|
72
|
+
// Tables are created via migration SQL files
|
|
73
|
+
// This checks if tables exist
|
|
74
|
+
const { error } = await this.supabase
|
|
75
|
+
.from('sessions')
|
|
76
|
+
.select('id')
|
|
77
|
+
.limit(1);
|
|
78
|
+
if (error && error.code === '42P01') {
|
|
79
|
+
throw new Error('Tables not found. Run migrations first: npx openclaw-memory migrate');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// ============ SESSIONS ============
|
|
83
|
+
/**
|
|
84
|
+
* Start a new conversation session
|
|
85
|
+
*/
|
|
86
|
+
async startSession(opts = {}) {
|
|
87
|
+
const { data, error } = await this.supabase
|
|
88
|
+
.from('sessions')
|
|
89
|
+
.insert({
|
|
90
|
+
agent_id: this.agentId,
|
|
91
|
+
user_id: opts.userId,
|
|
92
|
+
channel: opts.channel,
|
|
93
|
+
metadata: opts.metadata || {}
|
|
94
|
+
})
|
|
95
|
+
.select()
|
|
96
|
+
.single();
|
|
97
|
+
if (error)
|
|
98
|
+
throw error;
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* End a session with optional summary
|
|
103
|
+
*/
|
|
104
|
+
async endSession(sessionId, opts = {}) {
|
|
105
|
+
let summary = opts.summary;
|
|
106
|
+
// Auto-generate summary if requested
|
|
107
|
+
if (opts.autoSummarize && !summary && this.openai) {
|
|
108
|
+
summary = await this.generateSessionSummary(sessionId);
|
|
109
|
+
}
|
|
110
|
+
const { data, error } = await this.supabase
|
|
111
|
+
.from('sessions')
|
|
112
|
+
.update({
|
|
113
|
+
ended_at: new Date().toISOString(),
|
|
114
|
+
summary
|
|
115
|
+
})
|
|
116
|
+
.eq('id', sessionId)
|
|
117
|
+
.select()
|
|
118
|
+
.single();
|
|
119
|
+
if (error)
|
|
120
|
+
throw error;
|
|
121
|
+
return data;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Generate an AI summary of a session
|
|
125
|
+
*/
|
|
126
|
+
async generateSessionSummary(sessionId) {
|
|
127
|
+
if (!this.openai) {
|
|
128
|
+
throw new Error('OpenAI client required for auto-summarization');
|
|
129
|
+
}
|
|
130
|
+
const messages = await this.getMessages(sessionId);
|
|
131
|
+
if (messages.length === 0) {
|
|
132
|
+
return 'Empty session';
|
|
133
|
+
}
|
|
134
|
+
const conversation = messages
|
|
135
|
+
.map(m => `${m.role}: ${m.content}`)
|
|
136
|
+
.join('\n');
|
|
137
|
+
const response = await this.openai.chat.completions.create({
|
|
138
|
+
model: 'gpt-4o-mini',
|
|
139
|
+
messages: [
|
|
140
|
+
{
|
|
141
|
+
role: 'system',
|
|
142
|
+
content: 'Summarize this conversation in 2-3 sentences. Focus on key topics, decisions, and outcomes.'
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
role: 'user',
|
|
146
|
+
content: conversation
|
|
147
|
+
}
|
|
148
|
+
],
|
|
149
|
+
max_tokens: 200
|
|
150
|
+
});
|
|
151
|
+
return response.choices[0]?.message?.content || 'Summary generation failed';
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Resume a session (useful for continuing interrupted conversations)
|
|
155
|
+
*/
|
|
156
|
+
async resumeSession(sessionId) {
|
|
157
|
+
const session = await this.getSession(sessionId);
|
|
158
|
+
if (!session) {
|
|
159
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
160
|
+
}
|
|
161
|
+
const messages = await this.getMessages(sessionId);
|
|
162
|
+
// Build context summary
|
|
163
|
+
const contextParts = [];
|
|
164
|
+
if (session.summary) {
|
|
165
|
+
contextParts.push(`Previous summary: ${session.summary}`);
|
|
166
|
+
}
|
|
167
|
+
contextParts.push(`Message count: ${messages.length}`);
|
|
168
|
+
const lastMessages = messages.slice(-5);
|
|
169
|
+
if (lastMessages.length > 0) {
|
|
170
|
+
contextParts.push('Recent messages:');
|
|
171
|
+
lastMessages.forEach(m => {
|
|
172
|
+
contextParts.push(` ${m.role}: ${m.content.substring(0, 100)}...`);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
session,
|
|
177
|
+
messages,
|
|
178
|
+
context: contextParts.join('\n')
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Search sessions by date range
|
|
183
|
+
*/
|
|
184
|
+
async searchSessions(opts = {}) {
|
|
185
|
+
let query = this.supabase
|
|
186
|
+
.from('sessions')
|
|
187
|
+
.select()
|
|
188
|
+
.eq('agent_id', this.agentId)
|
|
189
|
+
.order('started_at', { ascending: false });
|
|
190
|
+
if (opts.userId) {
|
|
191
|
+
query = query.eq('user_id', opts.userId);
|
|
192
|
+
}
|
|
193
|
+
if (opts.channel) {
|
|
194
|
+
query = query.eq('channel', opts.channel);
|
|
195
|
+
}
|
|
196
|
+
if (opts.startDate) {
|
|
197
|
+
query = query.gte('started_at', opts.startDate);
|
|
198
|
+
}
|
|
199
|
+
if (opts.endDate) {
|
|
200
|
+
query = query.lte('started_at', opts.endDate);
|
|
201
|
+
}
|
|
202
|
+
query = query.range(opts.offset || 0, (opts.offset || 0) + (opts.limit || 50) - 1);
|
|
203
|
+
const { data, error } = await query;
|
|
204
|
+
if (error)
|
|
205
|
+
throw error;
|
|
206
|
+
return data || [];
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Export a session to markdown
|
|
210
|
+
*/
|
|
211
|
+
async exportSessionToMarkdown(sessionId) {
|
|
212
|
+
const session = await this.getSession(sessionId);
|
|
213
|
+
if (!session) {
|
|
214
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
215
|
+
}
|
|
216
|
+
const messages = await this.getMessages(sessionId);
|
|
217
|
+
const lines = [
|
|
218
|
+
`# Session ${session.id}`,
|
|
219
|
+
'',
|
|
220
|
+
`**Started:** ${new Date(session.started_at).toLocaleString()}`,
|
|
221
|
+
session.ended_at ? `**Ended:** ${new Date(session.ended_at).toLocaleString()}` : '**Status:** Active',
|
|
222
|
+
session.user_id ? `**User:** ${session.user_id}` : '',
|
|
223
|
+
session.channel ? `**Channel:** ${session.channel}` : '',
|
|
224
|
+
''
|
|
225
|
+
];
|
|
226
|
+
if (session.summary) {
|
|
227
|
+
lines.push(`## Summary`, '', session.summary, '');
|
|
228
|
+
}
|
|
229
|
+
lines.push(`## Messages (${messages.length})`, '');
|
|
230
|
+
messages.forEach(msg => {
|
|
231
|
+
const time = new Date(msg.created_at).toLocaleTimeString();
|
|
232
|
+
lines.push(`### ${msg.role} (${time})`);
|
|
233
|
+
lines.push('');
|
|
234
|
+
lines.push(msg.content);
|
|
235
|
+
lines.push('');
|
|
236
|
+
});
|
|
237
|
+
return lines.filter(Boolean).join('\n');
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Import a session from markdown
|
|
241
|
+
*/
|
|
242
|
+
async importSessionFromMarkdown(markdown, opts = {}) {
|
|
243
|
+
// Simple parser - expects format from exportSessionToMarkdown
|
|
244
|
+
const lines = markdown.split('\n');
|
|
245
|
+
// Start new session
|
|
246
|
+
const session = await this.startSession({
|
|
247
|
+
userId: opts.userId,
|
|
248
|
+
channel: opts.channel
|
|
249
|
+
});
|
|
250
|
+
// Parse messages (simple state machine)
|
|
251
|
+
let currentRole = 'user';
|
|
252
|
+
let currentContent = [];
|
|
253
|
+
for (const line of lines) {
|
|
254
|
+
const roleMatch = line.match(/^### (user|assistant|system|tool)/i);
|
|
255
|
+
if (roleMatch) {
|
|
256
|
+
// Save previous message if exists
|
|
257
|
+
if (currentContent.length > 0) {
|
|
258
|
+
await this.addMessage(session.id, {
|
|
259
|
+
role: currentRole,
|
|
260
|
+
content: currentContent.join('\n').trim()
|
|
261
|
+
});
|
|
262
|
+
currentContent = [];
|
|
263
|
+
}
|
|
264
|
+
currentRole = roleMatch[1].toLowerCase();
|
|
265
|
+
}
|
|
266
|
+
else if (line.startsWith('##') || line.startsWith('**')) {
|
|
267
|
+
// Skip headers and metadata
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
currentContent.push(line);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// Save last message
|
|
275
|
+
if (currentContent.length > 0) {
|
|
276
|
+
await this.addMessage(session.id, {
|
|
277
|
+
role: currentRole,
|
|
278
|
+
content: currentContent.join('\n').trim()
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return session;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Extract memories from a session
|
|
285
|
+
*/
|
|
286
|
+
async extractMemoriesFromSession(sessionId, opts = {}) {
|
|
287
|
+
const messages = await this.getMessages(sessionId);
|
|
288
|
+
const memories = [];
|
|
289
|
+
if (opts.autoExtract && this.openai) {
|
|
290
|
+
// Use AI to extract key learnings
|
|
291
|
+
const conversation = messages
|
|
292
|
+
.map(m => `${m.role}: ${m.content}`)
|
|
293
|
+
.join('\n');
|
|
294
|
+
const response = await this.openai.chat.completions.create({
|
|
295
|
+
model: 'gpt-4o-mini',
|
|
296
|
+
messages: [
|
|
297
|
+
{
|
|
298
|
+
role: 'system',
|
|
299
|
+
content: `Extract key facts, decisions, and learnings from this conversation.
|
|
300
|
+
Return as JSON array: [{"content": "...", "category": "fact|decision|preference|learning", "importance": 0-1}]`
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
role: 'user',
|
|
304
|
+
content: conversation
|
|
305
|
+
}
|
|
306
|
+
],
|
|
307
|
+
response_format: { type: 'json_object' }
|
|
308
|
+
});
|
|
309
|
+
const result = JSON.parse(response.choices[0]?.message?.content || '{"items":[]}');
|
|
310
|
+
const items = result.items || result.memories || [];
|
|
311
|
+
for (const item of items) {
|
|
312
|
+
if (item.importance >= (opts.minImportance || 0.5)) {
|
|
313
|
+
const memory = await this.remember({
|
|
314
|
+
content: item.content,
|
|
315
|
+
category: item.category,
|
|
316
|
+
importance: item.importance,
|
|
317
|
+
sessionId
|
|
318
|
+
});
|
|
319
|
+
memories.push(memory);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return memories;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Count tokens in a session
|
|
327
|
+
*/
|
|
328
|
+
async countSessionTokens(sessionId) {
|
|
329
|
+
const messages = await this.getMessages(sessionId);
|
|
330
|
+
let totalTokens = 0;
|
|
331
|
+
for (const msg of messages) {
|
|
332
|
+
if (msg.token_count) {
|
|
333
|
+
totalTokens += msg.token_count;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
// Rough estimation: 1 token ≈ 4 characters
|
|
337
|
+
totalTokens += Math.ceil(msg.content.length / 4);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
totalTokens,
|
|
342
|
+
messageCount: messages.length,
|
|
343
|
+
averageTokensPerMessage: messages.length > 0
|
|
344
|
+
? Math.round(totalTokens / messages.length)
|
|
345
|
+
: 0
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Get a session by ID
|
|
350
|
+
*/
|
|
351
|
+
async getSession(sessionId) {
|
|
352
|
+
const { data, error } = await this.supabase
|
|
353
|
+
.from('sessions')
|
|
354
|
+
.select()
|
|
355
|
+
.eq('id', sessionId)
|
|
356
|
+
.single();
|
|
357
|
+
if (error && error.code !== 'PGRST116')
|
|
358
|
+
throw error;
|
|
359
|
+
return data;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Get recent sessions
|
|
363
|
+
*/
|
|
364
|
+
async getRecentSessions(opts = {}) {
|
|
365
|
+
let query = this.supabase
|
|
366
|
+
.from('sessions')
|
|
367
|
+
.select()
|
|
368
|
+
.eq('agent_id', this.agentId)
|
|
369
|
+
.order('started_at', { ascending: false })
|
|
370
|
+
.limit(opts.limit || 10);
|
|
371
|
+
if (opts.userId) {
|
|
372
|
+
query = query.eq('user_id', opts.userId);
|
|
373
|
+
}
|
|
374
|
+
const { data, error } = await query;
|
|
375
|
+
if (error)
|
|
376
|
+
throw error;
|
|
377
|
+
return data || [];
|
|
378
|
+
}
|
|
379
|
+
// ============ MESSAGES ============
|
|
380
|
+
/**
|
|
381
|
+
* Add a message to a session
|
|
382
|
+
*/
|
|
383
|
+
async addMessage(sessionId, message) {
|
|
384
|
+
const { data, error } = await this.supabase
|
|
385
|
+
.from('messages')
|
|
386
|
+
.insert({
|
|
387
|
+
session_id: sessionId,
|
|
388
|
+
role: message.role,
|
|
389
|
+
content: message.content,
|
|
390
|
+
token_count: message.tokenCount,
|
|
391
|
+
metadata: message.metadata || {}
|
|
392
|
+
})
|
|
393
|
+
.select()
|
|
394
|
+
.single();
|
|
395
|
+
if (error)
|
|
396
|
+
throw error;
|
|
397
|
+
return data;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Get messages from a session
|
|
401
|
+
*/
|
|
402
|
+
async getMessages(sessionId, opts = {}) {
|
|
403
|
+
const { data, error } = await this.supabase
|
|
404
|
+
.from('messages')
|
|
405
|
+
.select()
|
|
406
|
+
.eq('session_id', sessionId)
|
|
407
|
+
.order('created_at', { ascending: true })
|
|
408
|
+
.range(opts.offset || 0, (opts.offset || 0) + (opts.limit || 100) - 1);
|
|
409
|
+
if (error)
|
|
410
|
+
throw error;
|
|
411
|
+
return data || [];
|
|
412
|
+
}
|
|
413
|
+
// ============ MEMORIES ============
|
|
414
|
+
/**
|
|
415
|
+
* Store a long-term memory with semantic embedding
|
|
416
|
+
*/
|
|
417
|
+
async remember(memory) {
|
|
418
|
+
// Generate embedding if provider configured
|
|
419
|
+
const embedding = await this.generateEmbedding(memory.content);
|
|
420
|
+
const { data, error } = await this.supabase
|
|
421
|
+
.from('memories')
|
|
422
|
+
.insert({
|
|
423
|
+
agent_id: this.agentId,
|
|
424
|
+
user_id: memory.userId,
|
|
425
|
+
category: memory.category,
|
|
426
|
+
content: memory.content,
|
|
427
|
+
importance: memory.importance ?? 0.5,
|
|
428
|
+
source_session_id: memory.sessionId,
|
|
429
|
+
expires_at: memory.expiresAt,
|
|
430
|
+
embedding,
|
|
431
|
+
metadata: memory.metadata || {}
|
|
432
|
+
})
|
|
433
|
+
.select()
|
|
434
|
+
.single();
|
|
435
|
+
if (error)
|
|
436
|
+
throw error;
|
|
437
|
+
return data;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Search memories using vector similarity (semantic search)
|
|
441
|
+
*/
|
|
442
|
+
async recall(query, opts = {}) {
|
|
443
|
+
// Generate query embedding for semantic search
|
|
444
|
+
const queryEmbedding = await this.generateEmbedding(query);
|
|
445
|
+
if (queryEmbedding) {
|
|
446
|
+
// Use pgvector for semantic search
|
|
447
|
+
const { data, error } = await this.supabase.rpc('match_memories', {
|
|
448
|
+
query_embedding: queryEmbedding,
|
|
449
|
+
match_threshold: opts.minSimilarity ?? 0.7,
|
|
450
|
+
match_count: opts.limit || 10,
|
|
451
|
+
p_agent_id: this.agentId,
|
|
452
|
+
p_user_id: opts.userId,
|
|
453
|
+
p_category: opts.category,
|
|
454
|
+
p_min_importance: opts.minImportance
|
|
455
|
+
});
|
|
456
|
+
if (error)
|
|
457
|
+
throw error;
|
|
458
|
+
return data || [];
|
|
459
|
+
}
|
|
460
|
+
// Fallback to text search when no embeddings available
|
|
461
|
+
let q = this.supabase
|
|
462
|
+
.from('memories')
|
|
463
|
+
.select()
|
|
464
|
+
.eq('agent_id', this.agentId)
|
|
465
|
+
.order('importance', { ascending: false })
|
|
466
|
+
.order('created_at', { ascending: false })
|
|
467
|
+
.limit(opts.limit || 10);
|
|
468
|
+
if (opts.userId) {
|
|
469
|
+
q = q.or(`user_id.eq.${opts.userId},user_id.is.null`);
|
|
470
|
+
}
|
|
471
|
+
if (opts.category) {
|
|
472
|
+
q = q.eq('category', opts.category);
|
|
473
|
+
}
|
|
474
|
+
if (opts.minImportance) {
|
|
475
|
+
q = q.gte('importance', opts.minImportance);
|
|
476
|
+
}
|
|
477
|
+
// Text search filter
|
|
478
|
+
q = q.ilike('content', `%${query}%`);
|
|
479
|
+
const { data, error } = await q;
|
|
480
|
+
if (error)
|
|
481
|
+
throw error;
|
|
482
|
+
return data || [];
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Hybrid search: combines semantic similarity and keyword matching
|
|
486
|
+
* Returns deduplicated results sorted by relevance score
|
|
487
|
+
*/
|
|
488
|
+
async hybridRecall(query, opts = {}) {
|
|
489
|
+
const vectorWeight = opts.vectorWeight ?? 0.7;
|
|
490
|
+
const keywordWeight = opts.keywordWeight ?? 0.3;
|
|
491
|
+
// Generate query embedding
|
|
492
|
+
const queryEmbedding = await this.generateEmbedding(query);
|
|
493
|
+
if (queryEmbedding) {
|
|
494
|
+
// Use hybrid search RPC function
|
|
495
|
+
const { data, error } = await this.supabase.rpc('hybrid_search_memories', {
|
|
496
|
+
query_embedding: queryEmbedding,
|
|
497
|
+
query_text: query,
|
|
498
|
+
vector_weight: vectorWeight,
|
|
499
|
+
keyword_weight: keywordWeight,
|
|
500
|
+
match_count: opts.limit || 10,
|
|
501
|
+
p_agent_id: this.agentId,
|
|
502
|
+
p_user_id: opts.userId,
|
|
503
|
+
p_category: opts.category,
|
|
504
|
+
p_min_importance: opts.minImportance
|
|
505
|
+
});
|
|
506
|
+
if (error)
|
|
507
|
+
throw error;
|
|
508
|
+
return data || [];
|
|
509
|
+
}
|
|
510
|
+
// Fallback to regular recall if no embeddings
|
|
511
|
+
return this.recall(query, opts);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Delete a memory
|
|
515
|
+
*/
|
|
516
|
+
async forget(memoryId) {
|
|
517
|
+
const { error } = await this.supabase
|
|
518
|
+
.from('memories')
|
|
519
|
+
.delete()
|
|
520
|
+
.eq('id', memoryId);
|
|
521
|
+
if (error)
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Get all memories (paginated)
|
|
526
|
+
*/
|
|
527
|
+
async getMemories(opts = {}) {
|
|
528
|
+
let query = this.supabase
|
|
529
|
+
.from('memories')
|
|
530
|
+
.select()
|
|
531
|
+
.eq('agent_id', this.agentId)
|
|
532
|
+
.order('created_at', { ascending: false })
|
|
533
|
+
.range(opts.offset || 0, (opts.offset || 0) + (opts.limit || 50) - 1);
|
|
534
|
+
if (opts.userId) {
|
|
535
|
+
query = query.eq('user_id', opts.userId);
|
|
536
|
+
}
|
|
537
|
+
if (opts.category) {
|
|
538
|
+
query = query.eq('category', opts.category);
|
|
539
|
+
}
|
|
540
|
+
const { data, error } = await query;
|
|
541
|
+
if (error)
|
|
542
|
+
throw error;
|
|
543
|
+
return data || [];
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Find memories similar to an existing memory
|
|
547
|
+
* Useful for context expansion and deduplication
|
|
548
|
+
*/
|
|
549
|
+
async findSimilarMemories(memoryId, opts = {}) {
|
|
550
|
+
const { data, error } = await this.supabase.rpc('find_similar_memories', {
|
|
551
|
+
memory_id: memoryId,
|
|
552
|
+
match_threshold: opts.minSimilarity ?? 0.8,
|
|
553
|
+
match_count: opts.limit || 5
|
|
554
|
+
});
|
|
555
|
+
if (error)
|
|
556
|
+
throw error;
|
|
557
|
+
return data || [];
|
|
558
|
+
}
|
|
559
|
+
// ============ TASKS ============
|
|
560
|
+
/**
|
|
561
|
+
* Create a task
|
|
562
|
+
*/
|
|
563
|
+
async createTask(task) {
|
|
564
|
+
const { data, error } = await this.supabase
|
|
565
|
+
.from('tasks')
|
|
566
|
+
.insert({
|
|
567
|
+
agent_id: this.agentId,
|
|
568
|
+
user_id: task.userId,
|
|
569
|
+
title: task.title,
|
|
570
|
+
description: task.description,
|
|
571
|
+
priority: task.priority ?? 0,
|
|
572
|
+
due_at: task.dueAt,
|
|
573
|
+
parent_task_id: task.parentTaskId,
|
|
574
|
+
metadata: task.metadata || {}
|
|
575
|
+
})
|
|
576
|
+
.select()
|
|
577
|
+
.single();
|
|
578
|
+
if (error)
|
|
579
|
+
throw error;
|
|
580
|
+
return data;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Update a task
|
|
584
|
+
*/
|
|
585
|
+
async updateTask(taskId, updates) {
|
|
586
|
+
const updateData = {
|
|
587
|
+
updated_at: new Date().toISOString()
|
|
588
|
+
};
|
|
589
|
+
if (updates.title)
|
|
590
|
+
updateData.title = updates.title;
|
|
591
|
+
if (updates.description)
|
|
592
|
+
updateData.description = updates.description;
|
|
593
|
+
if (updates.status) {
|
|
594
|
+
updateData.status = updates.status;
|
|
595
|
+
if (updates.status === 'done') {
|
|
596
|
+
updateData.completed_at = new Date().toISOString();
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (updates.priority !== undefined)
|
|
600
|
+
updateData.priority = updates.priority;
|
|
601
|
+
if (updates.dueAt)
|
|
602
|
+
updateData.due_at = updates.dueAt;
|
|
603
|
+
if (updates.metadata)
|
|
604
|
+
updateData.metadata = updates.metadata;
|
|
605
|
+
const { data, error } = await this.supabase
|
|
606
|
+
.from('tasks')
|
|
607
|
+
.update(updateData)
|
|
608
|
+
.eq('id', taskId)
|
|
609
|
+
.select()
|
|
610
|
+
.single();
|
|
611
|
+
if (error)
|
|
612
|
+
throw error;
|
|
613
|
+
return data;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Get tasks
|
|
617
|
+
*/
|
|
618
|
+
async getTasks(opts = {}) {
|
|
619
|
+
let query = this.supabase
|
|
620
|
+
.from('tasks')
|
|
621
|
+
.select()
|
|
622
|
+
.eq('agent_id', this.agentId)
|
|
623
|
+
.order('priority', { ascending: false })
|
|
624
|
+
.order('created_at', { ascending: false })
|
|
625
|
+
.limit(opts.limit || 50);
|
|
626
|
+
if (opts.status) {
|
|
627
|
+
query = query.eq('status', opts.status);
|
|
628
|
+
}
|
|
629
|
+
if (opts.userId) {
|
|
630
|
+
query = query.eq('user_id', opts.userId);
|
|
631
|
+
}
|
|
632
|
+
const { data, error } = await query;
|
|
633
|
+
if (error)
|
|
634
|
+
throw error;
|
|
635
|
+
return data || [];
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Delete a task
|
|
639
|
+
*/
|
|
640
|
+
async deleteTask(taskId) {
|
|
641
|
+
const { error } = await this.supabase
|
|
642
|
+
.from('tasks')
|
|
643
|
+
.delete()
|
|
644
|
+
.eq('id', taskId);
|
|
645
|
+
if (error)
|
|
646
|
+
throw error;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Get subtasks of a parent task
|
|
650
|
+
*/
|
|
651
|
+
async getSubtasks(parentTaskId) {
|
|
652
|
+
const { data, error } = await this.supabase
|
|
653
|
+
.from('tasks')
|
|
654
|
+
.select()
|
|
655
|
+
.eq('parent_task_id', parentTaskId)
|
|
656
|
+
.order('priority', { ascending: false })
|
|
657
|
+
.order('created_at', { ascending: false });
|
|
658
|
+
if (error)
|
|
659
|
+
throw error;
|
|
660
|
+
return data || [];
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Get task with all its subtasks (hierarchical)
|
|
664
|
+
*/
|
|
665
|
+
async getTaskWithSubtasks(taskId) {
|
|
666
|
+
const task = await this.supabase
|
|
667
|
+
.from('tasks')
|
|
668
|
+
.select()
|
|
669
|
+
.eq('id', taskId)
|
|
670
|
+
.single();
|
|
671
|
+
if (task.error)
|
|
672
|
+
throw task.error;
|
|
673
|
+
const subtasks = await this.getSubtasks(taskId);
|
|
674
|
+
return {
|
|
675
|
+
task: task.data,
|
|
676
|
+
subtasks
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Get upcoming tasks (due soon)
|
|
681
|
+
*/
|
|
682
|
+
async getUpcomingTasks(opts = {}) {
|
|
683
|
+
const now = new Date();
|
|
684
|
+
const future = new Date(now.getTime() + (opts.hoursAhead || 24) * 60 * 60 * 1000);
|
|
685
|
+
let query = this.supabase
|
|
686
|
+
.from('tasks')
|
|
687
|
+
.select()
|
|
688
|
+
.eq('agent_id', this.agentId)
|
|
689
|
+
.neq('status', 'done')
|
|
690
|
+
.not('due_at', 'is', null)
|
|
691
|
+
.gte('due_at', now.toISOString())
|
|
692
|
+
.lte('due_at', future.toISOString())
|
|
693
|
+
.order('due_at', { ascending: true });
|
|
694
|
+
if (opts.userId) {
|
|
695
|
+
query = query.eq('user_id', opts.userId);
|
|
696
|
+
}
|
|
697
|
+
const { data, error } = await query;
|
|
698
|
+
if (error)
|
|
699
|
+
throw error;
|
|
700
|
+
return data || [];
|
|
701
|
+
}
|
|
702
|
+
// ============ LEARNINGS ============
|
|
703
|
+
/**
|
|
704
|
+
* Record a learning
|
|
705
|
+
*/
|
|
706
|
+
async learn(learning) {
|
|
707
|
+
const { data, error } = await this.supabase
|
|
708
|
+
.from('learnings')
|
|
709
|
+
.insert({
|
|
710
|
+
agent_id: this.agentId,
|
|
711
|
+
category: learning.category,
|
|
712
|
+
trigger: learning.trigger,
|
|
713
|
+
lesson: learning.lesson,
|
|
714
|
+
action: learning.action,
|
|
715
|
+
severity: learning.severity ?? 'info',
|
|
716
|
+
source_session_id: learning.sessionId,
|
|
717
|
+
metadata: learning.metadata || {}
|
|
718
|
+
})
|
|
719
|
+
.select()
|
|
720
|
+
.single();
|
|
721
|
+
if (error)
|
|
722
|
+
throw error;
|
|
723
|
+
return data;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Get learnings
|
|
727
|
+
*/
|
|
728
|
+
async getLearnings(opts = {}) {
|
|
729
|
+
let query = this.supabase
|
|
730
|
+
.from('learnings')
|
|
731
|
+
.select()
|
|
732
|
+
.eq('agent_id', this.agentId)
|
|
733
|
+
.order('created_at', { ascending: false })
|
|
734
|
+
.limit(opts.limit || 50);
|
|
735
|
+
if (opts.category) {
|
|
736
|
+
query = query.eq('category', opts.category);
|
|
737
|
+
}
|
|
738
|
+
if (opts.severity) {
|
|
739
|
+
query = query.eq('severity', opts.severity);
|
|
740
|
+
}
|
|
741
|
+
const { data, error } = await query;
|
|
742
|
+
if (error)
|
|
743
|
+
throw error;
|
|
744
|
+
return data || [];
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Search learnings by topic for context
|
|
748
|
+
*/
|
|
749
|
+
async searchLearnings(query, opts = {}) {
|
|
750
|
+
const { data, error } = await this.supabase
|
|
751
|
+
.from('learnings')
|
|
752
|
+
.select()
|
|
753
|
+
.eq('agent_id', this.agentId)
|
|
754
|
+
.or(`trigger.ilike.%${query}%,lesson.ilike.%${query}%,action.ilike.%${query}%`)
|
|
755
|
+
.order('created_at', { ascending: false })
|
|
756
|
+
.limit(opts.limit || 10);
|
|
757
|
+
if (error)
|
|
758
|
+
throw error;
|
|
759
|
+
return data || [];
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Mark a learning as applied (increments applied_count)
|
|
763
|
+
*/
|
|
764
|
+
async applyLearning(learningId) {
|
|
765
|
+
const { data, error } = await this.supabase.rpc('increment_learning_applied', {
|
|
766
|
+
learning_id: learningId
|
|
767
|
+
});
|
|
768
|
+
if (error) {
|
|
769
|
+
// Fallback if RPC doesn't exist
|
|
770
|
+
const learning = await this.supabase
|
|
771
|
+
.from('learnings')
|
|
772
|
+
.select()
|
|
773
|
+
.eq('id', learningId)
|
|
774
|
+
.single();
|
|
775
|
+
if (learning.error)
|
|
776
|
+
throw learning.error;
|
|
777
|
+
const updated = await this.supabase
|
|
778
|
+
.from('learnings')
|
|
779
|
+
.update({ applied_count: (learning.data.applied_count || 0) + 1 })
|
|
780
|
+
.eq('id', learningId)
|
|
781
|
+
.select()
|
|
782
|
+
.single();
|
|
783
|
+
if (updated.error)
|
|
784
|
+
throw updated.error;
|
|
785
|
+
return updated.data;
|
|
786
|
+
}
|
|
787
|
+
return data;
|
|
788
|
+
}
|
|
789
|
+
// ============ TASK DEPENDENCIES ============
|
|
790
|
+
/**
|
|
791
|
+
* Add a task dependency (taskId depends on dependsOnTaskId)
|
|
792
|
+
*/
|
|
793
|
+
async addTaskDependency(taskId, dependsOnTaskId) {
|
|
794
|
+
// Store in metadata
|
|
795
|
+
const task = await this.supabase
|
|
796
|
+
.from('tasks')
|
|
797
|
+
.select()
|
|
798
|
+
.eq('id', taskId)
|
|
799
|
+
.single();
|
|
800
|
+
if (task.error)
|
|
801
|
+
throw task.error;
|
|
802
|
+
const dependencies = task.data.metadata?.dependencies || [];
|
|
803
|
+
if (!dependencies.includes(dependsOnTaskId)) {
|
|
804
|
+
dependencies.push(dependsOnTaskId);
|
|
805
|
+
}
|
|
806
|
+
await this.supabase
|
|
807
|
+
.from('tasks')
|
|
808
|
+
.update({
|
|
809
|
+
metadata: {
|
|
810
|
+
...task.data.metadata,
|
|
811
|
+
dependencies
|
|
812
|
+
}
|
|
813
|
+
})
|
|
814
|
+
.eq('id', taskId);
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Remove a task dependency
|
|
818
|
+
*/
|
|
819
|
+
async removeTaskDependency(taskId, dependsOnTaskId) {
|
|
820
|
+
const task = await this.supabase
|
|
821
|
+
.from('tasks')
|
|
822
|
+
.select()
|
|
823
|
+
.eq('id', taskId)
|
|
824
|
+
.single();
|
|
825
|
+
if (task.error)
|
|
826
|
+
throw task.error;
|
|
827
|
+
const dependencies = task.data.metadata?.dependencies || [];
|
|
828
|
+
const filtered = dependencies.filter(id => id !== dependsOnTaskId);
|
|
829
|
+
await this.supabase
|
|
830
|
+
.from('tasks')
|
|
831
|
+
.update({
|
|
832
|
+
metadata: {
|
|
833
|
+
...task.data.metadata,
|
|
834
|
+
dependencies: filtered
|
|
835
|
+
}
|
|
836
|
+
})
|
|
837
|
+
.eq('id', taskId);
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Get task dependencies
|
|
841
|
+
*/
|
|
842
|
+
async getTaskDependencies(taskId) {
|
|
843
|
+
const task = await this.supabase
|
|
844
|
+
.from('tasks')
|
|
845
|
+
.select()
|
|
846
|
+
.eq('id', taskId)
|
|
847
|
+
.single();
|
|
848
|
+
if (task.error)
|
|
849
|
+
throw task.error;
|
|
850
|
+
const dependencyIds = task.data.metadata?.dependencies || [];
|
|
851
|
+
if (dependencyIds.length === 0)
|
|
852
|
+
return [];
|
|
853
|
+
const { data, error } = await this.supabase
|
|
854
|
+
.from('tasks')
|
|
855
|
+
.select()
|
|
856
|
+
.in('id', dependencyIds);
|
|
857
|
+
if (error)
|
|
858
|
+
throw error;
|
|
859
|
+
return data || [];
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Check if a task is blocked by uncompleted dependencies
|
|
863
|
+
*/
|
|
864
|
+
async isTaskBlocked(taskId) {
|
|
865
|
+
const dependencies = await this.getTaskDependencies(taskId);
|
|
866
|
+
return dependencies.some(dep => dep.status !== 'done');
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Get tasks that are ready to start (no blocking dependencies)
|
|
870
|
+
*/
|
|
871
|
+
async getReadyTasks(opts = {}) {
|
|
872
|
+
const tasks = await this.getTasks({ status: 'pending', userId: opts.userId });
|
|
873
|
+
const ready = [];
|
|
874
|
+
for (const task of tasks) {
|
|
875
|
+
const blocked = await this.isTaskBlocked(task.id);
|
|
876
|
+
if (!blocked) {
|
|
877
|
+
ready.push(task);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return ready;
|
|
881
|
+
}
|
|
882
|
+
// ============ TASK TEMPLATES ============
|
|
883
|
+
/**
|
|
884
|
+
* Create a task template
|
|
885
|
+
*/
|
|
886
|
+
async createTaskTemplate(template) {
|
|
887
|
+
// Store as a special task with metadata flag
|
|
888
|
+
const { data, error } = await this.supabase
|
|
889
|
+
.from('tasks')
|
|
890
|
+
.insert({
|
|
891
|
+
agent_id: this.agentId,
|
|
892
|
+
title: `[TEMPLATE] ${template.name}`,
|
|
893
|
+
description: template.description,
|
|
894
|
+
status: 'pending',
|
|
895
|
+
priority: -1, // Templates have negative priority
|
|
896
|
+
metadata: {
|
|
897
|
+
is_template: true,
|
|
898
|
+
template_data: template,
|
|
899
|
+
...template.metadata
|
|
900
|
+
}
|
|
901
|
+
})
|
|
902
|
+
.select()
|
|
903
|
+
.single();
|
|
904
|
+
if (error)
|
|
905
|
+
throw error;
|
|
906
|
+
return { id: data.id };
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Get all task templates
|
|
910
|
+
*/
|
|
911
|
+
async getTaskTemplates() {
|
|
912
|
+
const { data, error } = await this.supabase
|
|
913
|
+
.from('tasks')
|
|
914
|
+
.select()
|
|
915
|
+
.eq('agent_id', this.agentId)
|
|
916
|
+
.eq('metadata->>is_template', 'true');
|
|
917
|
+
if (error)
|
|
918
|
+
throw error;
|
|
919
|
+
return (data || []).map(task => ({
|
|
920
|
+
id: task.id,
|
|
921
|
+
name: task.title.replace('[TEMPLATE] ', ''),
|
|
922
|
+
description: task.description,
|
|
923
|
+
tasks: task.metadata?.template_data?.tasks || []
|
|
924
|
+
}));
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Apply a task template (create all tasks from template)
|
|
928
|
+
*/
|
|
929
|
+
async applyTaskTemplate(templateId, opts = {}) {
|
|
930
|
+
const template = await this.supabase
|
|
931
|
+
.from('tasks')
|
|
932
|
+
.select()
|
|
933
|
+
.eq('id', templateId)
|
|
934
|
+
.single();
|
|
935
|
+
if (template.error)
|
|
936
|
+
throw template.error;
|
|
937
|
+
const templateData = template.data.metadata?.template_data;
|
|
938
|
+
if (!templateData?.tasks) {
|
|
939
|
+
throw new Error('Invalid template data');
|
|
940
|
+
}
|
|
941
|
+
const createdTasks = [];
|
|
942
|
+
const taskIdMap = new Map(); // template index -> created task id
|
|
943
|
+
// Create all tasks first
|
|
944
|
+
for (let i = 0; i < templateData.tasks.length; i++) {
|
|
945
|
+
const taskDef = templateData.tasks[i];
|
|
946
|
+
let dueAt;
|
|
947
|
+
if (opts.startDate && taskDef.estimatedDuration) {
|
|
948
|
+
// Calculate due date based on start date + duration
|
|
949
|
+
const start = new Date(opts.startDate);
|
|
950
|
+
const durationHours = parseInt(taskDef.estimatedDuration) || 24;
|
|
951
|
+
dueAt = new Date(start.getTime() + durationHours * 60 * 60 * 1000).toISOString();
|
|
952
|
+
}
|
|
953
|
+
const task = await this.createTask({
|
|
954
|
+
title: taskDef.title,
|
|
955
|
+
description: taskDef.description,
|
|
956
|
+
priority: taskDef.priority || 0,
|
|
957
|
+
dueAt,
|
|
958
|
+
userId: opts.userId,
|
|
959
|
+
metadata: {
|
|
960
|
+
from_template: templateId,
|
|
961
|
+
template_index: i,
|
|
962
|
+
...opts.metadata
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
createdTasks.push(task);
|
|
966
|
+
taskIdMap.set(i, task.id);
|
|
967
|
+
}
|
|
968
|
+
// Now add dependencies
|
|
969
|
+
for (let i = 0; i < templateData.tasks.length; i++) {
|
|
970
|
+
const taskDef = templateData.tasks[i];
|
|
971
|
+
if (taskDef.dependencies && taskDef.dependencies.length > 0) {
|
|
972
|
+
const taskId = taskIdMap.get(i);
|
|
973
|
+
if (taskId) {
|
|
974
|
+
for (const depIndex of taskDef.dependencies) {
|
|
975
|
+
const depId = taskIdMap.get(depIndex);
|
|
976
|
+
if (depId) {
|
|
977
|
+
await this.addTaskDependency(taskId, depId);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return createdTasks;
|
|
984
|
+
}
|
|
985
|
+
// ============ TASK REMINDERS ============
|
|
986
|
+
/**
|
|
987
|
+
* Get tasks that need reminders (due soon but not done)
|
|
988
|
+
*/
|
|
989
|
+
async getTasksNeedingReminders(opts = {}) {
|
|
990
|
+
const tasks = await this.getUpcomingTasks({
|
|
991
|
+
userId: opts.userId,
|
|
992
|
+
hoursAhead: opts.hoursAhead || 24
|
|
993
|
+
});
|
|
994
|
+
const now = Date.now();
|
|
995
|
+
return tasks.map(task => ({
|
|
996
|
+
...task,
|
|
997
|
+
timeUntilDue: task.due_at ? new Date(task.due_at).getTime() - now : 0
|
|
998
|
+
})).filter(task => task.timeUntilDue > 0);
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Format task reminder message
|
|
1002
|
+
*/
|
|
1003
|
+
formatTaskReminder(task, timeUntilDue) {
|
|
1004
|
+
const hours = Math.floor(timeUntilDue / (60 * 60 * 1000));
|
|
1005
|
+
const minutes = Math.floor((timeUntilDue % (60 * 60 * 1000)) / (60 * 1000));
|
|
1006
|
+
let timeStr = '';
|
|
1007
|
+
if (hours > 0) {
|
|
1008
|
+
timeStr = `${hours}h ${minutes}m`;
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
timeStr = `${minutes}m`;
|
|
1012
|
+
}
|
|
1013
|
+
return `⏰ Task reminder: "${task.title}" is due in ${timeStr}\n${task.description || ''}`;
|
|
1014
|
+
}
|
|
1015
|
+
// ============ LEARNING PATTERN DETECTION ============
|
|
1016
|
+
/**
|
|
1017
|
+
* Detect patterns in learnings (common categories, triggers, lessons)
|
|
1018
|
+
*/
|
|
1019
|
+
async detectLearningPatterns() {
|
|
1020
|
+
const learnings = await this.getLearnings({ limit: 1000 });
|
|
1021
|
+
// Category distribution
|
|
1022
|
+
const categoryMap = new Map();
|
|
1023
|
+
learnings.forEach(l => {
|
|
1024
|
+
categoryMap.set(l.category, (categoryMap.get(l.category) || 0) + 1);
|
|
1025
|
+
});
|
|
1026
|
+
const commonCategories = Array.from(categoryMap.entries())
|
|
1027
|
+
.map(([category, count]) => ({ category, count }))
|
|
1028
|
+
.sort((a, b) => b.count - a.count);
|
|
1029
|
+
// Trigger patterns (extract common words)
|
|
1030
|
+
const triggerWords = new Map();
|
|
1031
|
+
learnings.forEach(l => {
|
|
1032
|
+
const words = l.trigger.toLowerCase().split(/\s+/)
|
|
1033
|
+
.filter(w => w.length > 4); // Only words longer than 4 chars
|
|
1034
|
+
words.forEach(word => {
|
|
1035
|
+
triggerWords.set(word, (triggerWords.get(word) || 0) + 1);
|
|
1036
|
+
});
|
|
1037
|
+
});
|
|
1038
|
+
const commonTriggers = Array.from(triggerWords.entries())
|
|
1039
|
+
.map(([pattern, count]) => ({ pattern, count }))
|
|
1040
|
+
.sort((a, b) => b.count - a.count)
|
|
1041
|
+
.slice(0, 10);
|
|
1042
|
+
// Recent trends by week
|
|
1043
|
+
const weekMap = new Map();
|
|
1044
|
+
learnings.forEach(l => {
|
|
1045
|
+
const date = new Date(l.created_at);
|
|
1046
|
+
const weekStart = new Date(date);
|
|
1047
|
+
weekStart.setDate(date.getDate() - date.getDay());
|
|
1048
|
+
const weekKey = weekStart.toISOString().split('T')[0];
|
|
1049
|
+
const existing = weekMap.get(weekKey) || { count: 0, severities: [] };
|
|
1050
|
+
existing.count++;
|
|
1051
|
+
existing.severities.push(l.severity);
|
|
1052
|
+
weekMap.set(weekKey, existing);
|
|
1053
|
+
});
|
|
1054
|
+
const recentTrends = Array.from(weekMap.entries())
|
|
1055
|
+
.map(([week, data]) => ({
|
|
1056
|
+
week,
|
|
1057
|
+
count: data.count,
|
|
1058
|
+
severity: data.severities.filter(s => s === 'critical').length > 0
|
|
1059
|
+
? 'critical'
|
|
1060
|
+
: data.severities.filter(s => s === 'warning').length > 0
|
|
1061
|
+
? 'warning'
|
|
1062
|
+
: 'info'
|
|
1063
|
+
}))
|
|
1064
|
+
.sort((a, b) => b.week.localeCompare(a.week))
|
|
1065
|
+
.slice(0, 8);
|
|
1066
|
+
// Top applied lessons
|
|
1067
|
+
const topLessons = learnings
|
|
1068
|
+
.filter(l => l.applied_count > 0)
|
|
1069
|
+
.sort((a, b) => b.applied_count - a.applied_count)
|
|
1070
|
+
.slice(0, 10)
|
|
1071
|
+
.map(l => ({
|
|
1072
|
+
lesson: l.lesson,
|
|
1073
|
+
applied: l.applied_count,
|
|
1074
|
+
id: l.id
|
|
1075
|
+
}));
|
|
1076
|
+
return {
|
|
1077
|
+
commonCategories,
|
|
1078
|
+
commonTriggers,
|
|
1079
|
+
recentTrends,
|
|
1080
|
+
topLessons
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Get learning recommendations based on current context
|
|
1085
|
+
*/
|
|
1086
|
+
async getLearningRecommendations(context, limit = 5) {
|
|
1087
|
+
const learnings = await this.searchLearnings(context, { limit: limit * 2 });
|
|
1088
|
+
// Score and rank by relevance + application count
|
|
1089
|
+
const scored = learnings.map(l => ({
|
|
1090
|
+
learning: l,
|
|
1091
|
+
score: (l.applied_count || 0) * 0.3 + // Boost frequently applied learnings
|
|
1092
|
+
(l.severity === 'critical' ? 1.5 : l.severity === 'warning' ? 1.2 : 1.0)
|
|
1093
|
+
}));
|
|
1094
|
+
return scored
|
|
1095
|
+
.sort((a, b) => b.score - a.score)
|
|
1096
|
+
.slice(0, limit)
|
|
1097
|
+
.map(s => s.learning);
|
|
1098
|
+
}
|
|
1099
|
+
// ============ LEARNING SIMILARITY SEARCH ============
|
|
1100
|
+
/**
|
|
1101
|
+
* Find similar learnings using embeddings
|
|
1102
|
+
*/
|
|
1103
|
+
async findSimilarLearnings(learningId, opts = {}) {
|
|
1104
|
+
const learning = await this.supabase
|
|
1105
|
+
.from('learnings')
|
|
1106
|
+
.select()
|
|
1107
|
+
.eq('id', learningId)
|
|
1108
|
+
.single();
|
|
1109
|
+
if (learning.error)
|
|
1110
|
+
throw learning.error;
|
|
1111
|
+
// Generate embedding for the learning
|
|
1112
|
+
const text = `${learning.data.trigger} ${learning.data.lesson} ${learning.data.action || ''}`;
|
|
1113
|
+
const embedding = await this.generateEmbedding(text);
|
|
1114
|
+
if (!embedding) {
|
|
1115
|
+
throw new Error('Failed to generate embedding for learning');
|
|
1116
|
+
}
|
|
1117
|
+
// Store embedding in metadata for future use
|
|
1118
|
+
await this.supabase
|
|
1119
|
+
.from('learnings')
|
|
1120
|
+
.update({
|
|
1121
|
+
metadata: {
|
|
1122
|
+
...learning.data.metadata,
|
|
1123
|
+
embedding
|
|
1124
|
+
}
|
|
1125
|
+
})
|
|
1126
|
+
.eq('id', learningId);
|
|
1127
|
+
// Search for similar learnings
|
|
1128
|
+
const { data, error } = await this.supabase
|
|
1129
|
+
.from('learnings')
|
|
1130
|
+
.select()
|
|
1131
|
+
.eq('agent_id', this.agentId)
|
|
1132
|
+
.neq('id', learningId);
|
|
1133
|
+
if (error)
|
|
1134
|
+
throw error;
|
|
1135
|
+
// Calculate similarities
|
|
1136
|
+
const similarities = [];
|
|
1137
|
+
for (const l of data || []) {
|
|
1138
|
+
// Get or generate embedding
|
|
1139
|
+
const lText = `${l.trigger} ${l.lesson} ${l.action || ''}`;
|
|
1140
|
+
let lEmbedding;
|
|
1141
|
+
if (l.metadata?.embedding && Array.isArray(l.metadata.embedding)) {
|
|
1142
|
+
lEmbedding = l.metadata.embedding;
|
|
1143
|
+
}
|
|
1144
|
+
else {
|
|
1145
|
+
// Generate embedding on the fly
|
|
1146
|
+
lEmbedding = await this.generateEmbedding(lText);
|
|
1147
|
+
if (lEmbedding) {
|
|
1148
|
+
// Cache it
|
|
1149
|
+
await this.supabase
|
|
1150
|
+
.from('learnings')
|
|
1151
|
+
.update({
|
|
1152
|
+
metadata: {
|
|
1153
|
+
...l.metadata,
|
|
1154
|
+
embedding: lEmbedding
|
|
1155
|
+
}
|
|
1156
|
+
})
|
|
1157
|
+
.eq('id', l.id);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
if (lEmbedding !== null && lEmbedding.length > 0) {
|
|
1161
|
+
const similarity = this.cosineSimilarity(embedding, lEmbedding);
|
|
1162
|
+
similarities.push({ ...l, similarity });
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return similarities
|
|
1166
|
+
.filter(s => s.similarity >= (opts.threshold || 0.7))
|
|
1167
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
1168
|
+
.slice(0, opts.limit || 5);
|
|
1169
|
+
}
|
|
1170
|
+
// ============ LEARNING EXPORT/REPORT ============
|
|
1171
|
+
/**
|
|
1172
|
+
* Export learnings to markdown report
|
|
1173
|
+
*/
|
|
1174
|
+
async exportLearningsReport(opts = {}) {
|
|
1175
|
+
let learnings = await this.getLearnings({
|
|
1176
|
+
category: opts.category,
|
|
1177
|
+
severity: opts.severity,
|
|
1178
|
+
limit: 1000
|
|
1179
|
+
});
|
|
1180
|
+
if (opts.since) {
|
|
1181
|
+
const sinceDate = new Date(opts.since);
|
|
1182
|
+
learnings = learnings.filter(l => new Date(l.created_at) >= sinceDate);
|
|
1183
|
+
}
|
|
1184
|
+
const patterns = await this.detectLearningPatterns();
|
|
1185
|
+
let report = `# Learning Report\n\n`;
|
|
1186
|
+
report += `**Generated:** ${new Date().toISOString()}\n`;
|
|
1187
|
+
report += `**Total Learnings:** ${learnings.length}\n\n`;
|
|
1188
|
+
// Patterns section
|
|
1189
|
+
report += `## Patterns\n\n`;
|
|
1190
|
+
report += `### Categories\n`;
|
|
1191
|
+
patterns.commonCategories.forEach(c => {
|
|
1192
|
+
report += `- ${c.category}: ${c.count}\n`;
|
|
1193
|
+
});
|
|
1194
|
+
report += `\n### Common Triggers\n`;
|
|
1195
|
+
patterns.commonTriggers.forEach(t => {
|
|
1196
|
+
report += `- "${t.pattern}": ${t.count} occurrences\n`;
|
|
1197
|
+
});
|
|
1198
|
+
report += `\n### Top Applied Lessons\n`;
|
|
1199
|
+
patterns.topLessons.forEach(l => {
|
|
1200
|
+
report += `- "${l.lesson}" (applied ${l.applied} times)\n`;
|
|
1201
|
+
});
|
|
1202
|
+
// Individual learnings by category
|
|
1203
|
+
report += `\n## All Learnings\n\n`;
|
|
1204
|
+
const byCategory = new Map();
|
|
1205
|
+
learnings.forEach(l => {
|
|
1206
|
+
const cat = byCategory.get(l.category) || [];
|
|
1207
|
+
cat.push(l);
|
|
1208
|
+
byCategory.set(l.category, cat);
|
|
1209
|
+
});
|
|
1210
|
+
byCategory.forEach((items, category) => {
|
|
1211
|
+
report += `### ${category.toUpperCase()}\n\n`;
|
|
1212
|
+
items.forEach(l => {
|
|
1213
|
+
report += `**[${l.severity.toUpperCase()}]** ${l.trigger}\n`;
|
|
1214
|
+
report += `- Lesson: ${l.lesson}\n`;
|
|
1215
|
+
if (l.action) {
|
|
1216
|
+
report += `- Action: ${l.action}\n`;
|
|
1217
|
+
}
|
|
1218
|
+
report += `- Applied: ${l.applied_count} times\n`;
|
|
1219
|
+
report += `- Created: ${new Date(l.created_at).toLocaleDateString()}\n\n`;
|
|
1220
|
+
});
|
|
1221
|
+
});
|
|
1222
|
+
return report;
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Export learnings to JSON
|
|
1226
|
+
*/
|
|
1227
|
+
async exportLearningsJSON(opts = {}) {
|
|
1228
|
+
let learnings = await this.getLearnings({
|
|
1229
|
+
category: opts.category,
|
|
1230
|
+
severity: opts.severity,
|
|
1231
|
+
limit: 1000
|
|
1232
|
+
});
|
|
1233
|
+
if (opts.since) {
|
|
1234
|
+
const sinceDate = new Date(opts.since);
|
|
1235
|
+
learnings = learnings.filter(l => new Date(l.created_at) >= sinceDate);
|
|
1236
|
+
}
|
|
1237
|
+
const patterns = await this.detectLearningPatterns();
|
|
1238
|
+
return {
|
|
1239
|
+
generated: new Date().toISOString(),
|
|
1240
|
+
total: learnings.length,
|
|
1241
|
+
patterns,
|
|
1242
|
+
learnings
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
// ============ ENTITIES ============
|
|
1246
|
+
/**
|
|
1247
|
+
* Extract entities from text using AI
|
|
1248
|
+
*/
|
|
1249
|
+
async extractEntities(text, opts = {}) {
|
|
1250
|
+
if (!this.openai) {
|
|
1251
|
+
throw new Error('OpenAI client required for entity extraction');
|
|
1252
|
+
}
|
|
1253
|
+
const response = await this.openai.chat.completions.create({
|
|
1254
|
+
model: 'gpt-4o-mini',
|
|
1255
|
+
messages: [
|
|
1256
|
+
{
|
|
1257
|
+
role: 'system',
|
|
1258
|
+
content: `Extract named entities from the text. Return JSON array of entities.
|
|
1259
|
+
Each entity should have: type (person|place|organization|product|concept), name, description.
|
|
1260
|
+
Focus on important entities that should be remembered.
|
|
1261
|
+
Format: {"entities": [{"type": "...", "name": "...", "description": "..."}]}`
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
role: 'user',
|
|
1265
|
+
content: text
|
|
1266
|
+
}
|
|
1267
|
+
],
|
|
1268
|
+
response_format: { type: 'json_object' }
|
|
1269
|
+
});
|
|
1270
|
+
const result = JSON.parse(response.choices[0]?.message?.content || '{"entities":[]}');
|
|
1271
|
+
const extractedEntities = result.entities || [];
|
|
1272
|
+
const entities = [];
|
|
1273
|
+
for (const e of extractedEntities) {
|
|
1274
|
+
// Check if entity already exists (by name, case-insensitive)
|
|
1275
|
+
const existing = await this.findEntity(e.name);
|
|
1276
|
+
if (existing) {
|
|
1277
|
+
// Update existing entity
|
|
1278
|
+
const updated = await this.updateEntity(existing.id, {
|
|
1279
|
+
description: e.description,
|
|
1280
|
+
lastSeenAt: new Date().toISOString()
|
|
1281
|
+
});
|
|
1282
|
+
entities.push(updated);
|
|
1283
|
+
}
|
|
1284
|
+
else {
|
|
1285
|
+
// Create new entity
|
|
1286
|
+
const entity = await this.createEntity({
|
|
1287
|
+
entityType: e.type,
|
|
1288
|
+
name: e.name,
|
|
1289
|
+
description: e.description
|
|
1290
|
+
});
|
|
1291
|
+
entities.push(entity);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return entities;
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Create an entity
|
|
1298
|
+
*/
|
|
1299
|
+
async createEntity(entity) {
|
|
1300
|
+
const { data, error } = await this.supabase
|
|
1301
|
+
.from('entities')
|
|
1302
|
+
.insert({
|
|
1303
|
+
agent_id: this.agentId,
|
|
1304
|
+
entity_type: entity.entityType,
|
|
1305
|
+
name: entity.name,
|
|
1306
|
+
aliases: entity.aliases || [],
|
|
1307
|
+
description: entity.description,
|
|
1308
|
+
properties: entity.properties || {},
|
|
1309
|
+
first_seen_at: new Date().toISOString(),
|
|
1310
|
+
last_seen_at: new Date().toISOString(),
|
|
1311
|
+
mention_count: 1
|
|
1312
|
+
})
|
|
1313
|
+
.select()
|
|
1314
|
+
.single();
|
|
1315
|
+
if (error)
|
|
1316
|
+
throw error;
|
|
1317
|
+
return data;
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Update an entity
|
|
1321
|
+
*/
|
|
1322
|
+
async updateEntity(entityId, updates) {
|
|
1323
|
+
const updateData = {};
|
|
1324
|
+
if (updates.name)
|
|
1325
|
+
updateData.name = updates.name;
|
|
1326
|
+
if (updates.aliases)
|
|
1327
|
+
updateData.aliases = updates.aliases;
|
|
1328
|
+
if (updates.description)
|
|
1329
|
+
updateData.description = updates.description;
|
|
1330
|
+
if (updates.properties)
|
|
1331
|
+
updateData.properties = updates.properties;
|
|
1332
|
+
if (updates.lastSeenAt)
|
|
1333
|
+
updateData.last_seen_at = updates.lastSeenAt;
|
|
1334
|
+
// Increment mention count
|
|
1335
|
+
const entity = await this.supabase
|
|
1336
|
+
.from('entities')
|
|
1337
|
+
.select()
|
|
1338
|
+
.eq('id', entityId)
|
|
1339
|
+
.single();
|
|
1340
|
+
if (entity.error)
|
|
1341
|
+
throw entity.error;
|
|
1342
|
+
updateData.mention_count = (entity.data.mention_count || 0) + 1;
|
|
1343
|
+
const { data, error } = await this.supabase
|
|
1344
|
+
.from('entities')
|
|
1345
|
+
.update(updateData)
|
|
1346
|
+
.eq('id', entityId)
|
|
1347
|
+
.select()
|
|
1348
|
+
.single();
|
|
1349
|
+
if (error)
|
|
1350
|
+
throw error;
|
|
1351
|
+
return data;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Find an entity by name or alias
|
|
1355
|
+
*/
|
|
1356
|
+
async findEntity(nameOrAlias) {
|
|
1357
|
+
const { data, error } = await this.supabase
|
|
1358
|
+
.from('entities')
|
|
1359
|
+
.select()
|
|
1360
|
+
.eq('agent_id', this.agentId)
|
|
1361
|
+
.or(`name.ilike.${nameOrAlias},aliases.cs.{${nameOrAlias}}`)
|
|
1362
|
+
.limit(1)
|
|
1363
|
+
.single();
|
|
1364
|
+
if (error && error.code !== 'PGRST116')
|
|
1365
|
+
throw error;
|
|
1366
|
+
return data;
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Search entities
|
|
1370
|
+
*/
|
|
1371
|
+
async searchEntities(opts = {}) {
|
|
1372
|
+
let query = this.supabase
|
|
1373
|
+
.from('entities')
|
|
1374
|
+
.select()
|
|
1375
|
+
.eq('agent_id', this.agentId)
|
|
1376
|
+
.order('mention_count', { ascending: false })
|
|
1377
|
+
.order('last_seen_at', { ascending: false })
|
|
1378
|
+
.limit(opts.limit || 20);
|
|
1379
|
+
if (opts.entityType) {
|
|
1380
|
+
query = query.eq('entity_type', opts.entityType);
|
|
1381
|
+
}
|
|
1382
|
+
if (opts.query) {
|
|
1383
|
+
query = query.or(`name.ilike.%${opts.query}%,description.ilike.%${opts.query}%`);
|
|
1384
|
+
}
|
|
1385
|
+
const { data, error } = await query;
|
|
1386
|
+
if (error)
|
|
1387
|
+
throw error;
|
|
1388
|
+
return data || [];
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Merge two entities (deduplication)
|
|
1392
|
+
*/
|
|
1393
|
+
async mergeEntities(primaryId, duplicateId) {
|
|
1394
|
+
// Get both entities
|
|
1395
|
+
const [primary, duplicate] = await Promise.all([
|
|
1396
|
+
this.supabase.from('entities').select().eq('id', primaryId).single(),
|
|
1397
|
+
this.supabase.from('entities').select().eq('id', duplicateId).single()
|
|
1398
|
+
]);
|
|
1399
|
+
if (primary.error)
|
|
1400
|
+
throw primary.error;
|
|
1401
|
+
if (duplicate.error)
|
|
1402
|
+
throw duplicate.error;
|
|
1403
|
+
// Merge aliases
|
|
1404
|
+
const mergedAliases = [
|
|
1405
|
+
...(primary.data.aliases || []),
|
|
1406
|
+
duplicate.data.name,
|
|
1407
|
+
...(duplicate.data.aliases || [])
|
|
1408
|
+
].filter((v, i, a) => a.indexOf(v) === i); // Deduplicate
|
|
1409
|
+
// Merge properties
|
|
1410
|
+
const mergedProperties = {
|
|
1411
|
+
...duplicate.data.properties,
|
|
1412
|
+
...primary.data.properties
|
|
1413
|
+
};
|
|
1414
|
+
// Update primary entity
|
|
1415
|
+
const { data, error } = await this.supabase
|
|
1416
|
+
.from('entities')
|
|
1417
|
+
.update({
|
|
1418
|
+
aliases: mergedAliases,
|
|
1419
|
+
properties: mergedProperties,
|
|
1420
|
+
mention_count: primary.data.mention_count + duplicate.data.mention_count,
|
|
1421
|
+
first_seen_at: new Date(Math.min(new Date(primary.data.first_seen_at).getTime(), new Date(duplicate.data.first_seen_at).getTime())).toISOString(),
|
|
1422
|
+
last_seen_at: new Date(Math.max(new Date(primary.data.last_seen_at).getTime(), new Date(duplicate.data.last_seen_at).getTime())).toISOString()
|
|
1423
|
+
})
|
|
1424
|
+
.eq('id', primaryId)
|
|
1425
|
+
.select()
|
|
1426
|
+
.single();
|
|
1427
|
+
if (error)
|
|
1428
|
+
throw error;
|
|
1429
|
+
// Delete duplicate
|
|
1430
|
+
await this.supabase.from('entities').delete().eq('id', duplicateId);
|
|
1431
|
+
return data;
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Create or update a relationship between entities
|
|
1435
|
+
*/
|
|
1436
|
+
async createEntityRelationship(rel) {
|
|
1437
|
+
// Check if relationship already exists
|
|
1438
|
+
const { data: existing, error: checkError } = await this.supabase
|
|
1439
|
+
.from('entity_relationships')
|
|
1440
|
+
.select()
|
|
1441
|
+
.eq('agent_id', this.agentId)
|
|
1442
|
+
.eq('source_entity_id', rel.sourceEntityId)
|
|
1443
|
+
.eq('target_entity_id', rel.targetEntityId)
|
|
1444
|
+
.eq('relationship_type', rel.relationshipType)
|
|
1445
|
+
.single();
|
|
1446
|
+
if (existing && !checkError) {
|
|
1447
|
+
// Update existing relationship
|
|
1448
|
+
const { data, error } = await this.supabase.rpc('increment_relationship_mentions', {
|
|
1449
|
+
rel_id: existing.id
|
|
1450
|
+
});
|
|
1451
|
+
if (error) {
|
|
1452
|
+
// Fallback if RPC doesn't exist
|
|
1453
|
+
const updated = await this.supabase
|
|
1454
|
+
.from('entity_relationships')
|
|
1455
|
+
.update({
|
|
1456
|
+
mention_count: existing.mention_count + 1,
|
|
1457
|
+
last_seen_at: new Date().toISOString(),
|
|
1458
|
+
confidence: Math.min(1.0, existing.confidence + 0.1), // Increase confidence with mentions
|
|
1459
|
+
properties: { ...existing.properties, ...rel.properties }
|
|
1460
|
+
})
|
|
1461
|
+
.eq('id', existing.id)
|
|
1462
|
+
.select()
|
|
1463
|
+
.single();
|
|
1464
|
+
if (updated.error)
|
|
1465
|
+
throw updated.error;
|
|
1466
|
+
return updated.data;
|
|
1467
|
+
}
|
|
1468
|
+
return data;
|
|
1469
|
+
}
|
|
1470
|
+
// Create new relationship
|
|
1471
|
+
const { data, error } = await this.supabase
|
|
1472
|
+
.from('entity_relationships')
|
|
1473
|
+
.insert({
|
|
1474
|
+
agent_id: this.agentId,
|
|
1475
|
+
source_entity_id: rel.sourceEntityId,
|
|
1476
|
+
target_entity_id: rel.targetEntityId,
|
|
1477
|
+
relationship_type: rel.relationshipType,
|
|
1478
|
+
properties: rel.properties || {},
|
|
1479
|
+
confidence: rel.confidence ?? 0.5,
|
|
1480
|
+
source_session_id: rel.sessionId,
|
|
1481
|
+
metadata: rel.metadata || {}
|
|
1482
|
+
})
|
|
1483
|
+
.select()
|
|
1484
|
+
.single();
|
|
1485
|
+
if (error)
|
|
1486
|
+
throw error;
|
|
1487
|
+
return data;
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Get relationships for an entity
|
|
1491
|
+
*/
|
|
1492
|
+
async getEntityRelationships(entityId, opts = {}) {
|
|
1493
|
+
const direction = opts.direction || 'both';
|
|
1494
|
+
const minConfidence = opts.minConfidence ?? 0.3;
|
|
1495
|
+
const limit = opts.limit || 50;
|
|
1496
|
+
const results = [];
|
|
1497
|
+
// Get outgoing relationships
|
|
1498
|
+
if (direction === 'outgoing' || direction === 'both') {
|
|
1499
|
+
let query = this.supabase
|
|
1500
|
+
.from('entity_relationships')
|
|
1501
|
+
.select('*, target:entities!target_entity_id(*)')
|
|
1502
|
+
.eq('source_entity_id', entityId)
|
|
1503
|
+
.gte('confidence', minConfidence)
|
|
1504
|
+
.order('mention_count', { ascending: false })
|
|
1505
|
+
.limit(limit);
|
|
1506
|
+
if (opts.relationshipType) {
|
|
1507
|
+
query = query.eq('relationship_type', opts.relationshipType);
|
|
1508
|
+
}
|
|
1509
|
+
const { data, error } = await query;
|
|
1510
|
+
if (error)
|
|
1511
|
+
throw error;
|
|
1512
|
+
if (data) {
|
|
1513
|
+
for (const row of data) {
|
|
1514
|
+
const { target, ...relationship } = row;
|
|
1515
|
+
results.push({
|
|
1516
|
+
relationship: relationship,
|
|
1517
|
+
relatedEntity: target,
|
|
1518
|
+
direction: 'outgoing'
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
// Get incoming relationships
|
|
1524
|
+
if (direction === 'incoming' || direction === 'both') {
|
|
1525
|
+
let query = this.supabase
|
|
1526
|
+
.from('entity_relationships')
|
|
1527
|
+
.select('*, source:entities!source_entity_id(*)')
|
|
1528
|
+
.eq('target_entity_id', entityId)
|
|
1529
|
+
.gte('confidence', minConfidence)
|
|
1530
|
+
.order('mention_count', { ascending: false })
|
|
1531
|
+
.limit(limit);
|
|
1532
|
+
if (opts.relationshipType) {
|
|
1533
|
+
query = query.eq('relationship_type', opts.relationshipType);
|
|
1534
|
+
}
|
|
1535
|
+
const { data, error } = await query;
|
|
1536
|
+
if (error)
|
|
1537
|
+
throw error;
|
|
1538
|
+
if (data) {
|
|
1539
|
+
for (const row of data) {
|
|
1540
|
+
const { source, ...relationship } = row;
|
|
1541
|
+
results.push({
|
|
1542
|
+
relationship: relationship,
|
|
1543
|
+
relatedEntity: source,
|
|
1544
|
+
direction: 'incoming'
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return results;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Find related entities through graph traversal
|
|
1553
|
+
*/
|
|
1554
|
+
async findRelatedEntities(entityId, opts = {}) {
|
|
1555
|
+
const { data, error } = await this.supabase.rpc('find_related_entities', {
|
|
1556
|
+
entity_id: entityId,
|
|
1557
|
+
max_depth: opts.maxDepth || 2,
|
|
1558
|
+
min_confidence: opts.minConfidence ?? 0.5
|
|
1559
|
+
});
|
|
1560
|
+
if (error)
|
|
1561
|
+
throw error;
|
|
1562
|
+
return data || [];
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Get entity network statistics
|
|
1566
|
+
*/
|
|
1567
|
+
async getEntityNetworkStats() {
|
|
1568
|
+
const { data, error } = await this.supabase.rpc('get_entity_network_stats', {
|
|
1569
|
+
agent: this.agentId
|
|
1570
|
+
});
|
|
1571
|
+
if (error)
|
|
1572
|
+
throw error;
|
|
1573
|
+
const stats = data?.[0];
|
|
1574
|
+
if (!stats) {
|
|
1575
|
+
return {
|
|
1576
|
+
totalEntities: 0,
|
|
1577
|
+
totalRelationships: 0,
|
|
1578
|
+
avgConnectionsPerEntity: 0
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
return {
|
|
1582
|
+
totalEntities: Number(stats.total_entities),
|
|
1583
|
+
totalRelationships: Number(stats.total_relationships),
|
|
1584
|
+
avgConnectionsPerEntity: Number(stats.avg_connections_per_entity) || 0,
|
|
1585
|
+
mostConnectedEntity: stats.most_connected_entity_id ? {
|
|
1586
|
+
id: stats.most_connected_entity_id,
|
|
1587
|
+
name: stats.most_connected_entity_name,
|
|
1588
|
+
connectionCount: Number(stats.connection_count)
|
|
1589
|
+
} : undefined
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Extract entities and relationships from text using AI
|
|
1594
|
+
*/
|
|
1595
|
+
async extractEntitiesWithRelationships(text, opts = {}) {
|
|
1596
|
+
if (!this.openai) {
|
|
1597
|
+
throw new Error('OpenAI client required for entity extraction');
|
|
1598
|
+
}
|
|
1599
|
+
const response = await this.openai.chat.completions.create({
|
|
1600
|
+
model: 'gpt-4o-mini',
|
|
1601
|
+
messages: [
|
|
1602
|
+
{
|
|
1603
|
+
role: 'system',
|
|
1604
|
+
content: `Extract named entities and their relationships from the text.
|
|
1605
|
+
|
|
1606
|
+
Return JSON with this structure:
|
|
1607
|
+
{
|
|
1608
|
+
"entities": [
|
|
1609
|
+
{"type": "person|place|organization|product|concept", "name": "...", "description": "..."}
|
|
1610
|
+
],
|
|
1611
|
+
"relationships": [
|
|
1612
|
+
{"source": "entity name", "target": "entity name", "type": "works_at|knows|created|located_in|etc", "confidence": 0.0-1.0}
|
|
1613
|
+
]
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
Focus on important entities and clear relationships. Use standard relationship types when possible.`
|
|
1617
|
+
},
|
|
1618
|
+
{
|
|
1619
|
+
role: 'user',
|
|
1620
|
+
content: text
|
|
1621
|
+
}
|
|
1622
|
+
],
|
|
1623
|
+
response_format: { type: 'json_object' }
|
|
1624
|
+
});
|
|
1625
|
+
const result = JSON.parse(response.choices[0]?.message?.content || '{"entities":[],"relationships":[]}');
|
|
1626
|
+
const extractedEntities = result.entities || [];
|
|
1627
|
+
const extractedRelationships = result.relationships || [];
|
|
1628
|
+
// First, create/update all entities
|
|
1629
|
+
const entityMap = new Map();
|
|
1630
|
+
for (const e of extractedEntities) {
|
|
1631
|
+
const existing = await this.findEntity(e.name);
|
|
1632
|
+
let entity;
|
|
1633
|
+
if (existing) {
|
|
1634
|
+
entity = await this.updateEntity(existing.id, {
|
|
1635
|
+
description: e.description,
|
|
1636
|
+
lastSeenAt: new Date().toISOString()
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
entity = await this.createEntity({
|
|
1641
|
+
entityType: e.type,
|
|
1642
|
+
name: e.name,
|
|
1643
|
+
description: e.description
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
entityMap.set(e.name.toLowerCase(), entity);
|
|
1647
|
+
}
|
|
1648
|
+
// Then, create relationships
|
|
1649
|
+
const relationships = [];
|
|
1650
|
+
for (const r of extractedRelationships) {
|
|
1651
|
+
const sourceEntity = entityMap.get(r.source.toLowerCase());
|
|
1652
|
+
const targetEntity = entityMap.get(r.target.toLowerCase());
|
|
1653
|
+
if (sourceEntity && targetEntity) {
|
|
1654
|
+
const relationship = await this.createEntityRelationship({
|
|
1655
|
+
sourceEntityId: sourceEntity.id,
|
|
1656
|
+
targetEntityId: targetEntity.id,
|
|
1657
|
+
relationshipType: r.type,
|
|
1658
|
+
confidence: r.confidence || 0.7,
|
|
1659
|
+
sessionId: opts.sessionId
|
|
1660
|
+
});
|
|
1661
|
+
relationships.push(relationship);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
return {
|
|
1665
|
+
entities: Array.from(entityMap.values()),
|
|
1666
|
+
relationships
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Delete a relationship
|
|
1671
|
+
*/
|
|
1672
|
+
async deleteEntityRelationship(relationshipId) {
|
|
1673
|
+
const { error } = await this.supabase
|
|
1674
|
+
.from('entity_relationships')
|
|
1675
|
+
.delete()
|
|
1676
|
+
.eq('id', relationshipId);
|
|
1677
|
+
if (error)
|
|
1678
|
+
throw error;
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Search relationships
|
|
1682
|
+
*/
|
|
1683
|
+
async searchRelationships(opts = {}) {
|
|
1684
|
+
let query = this.supabase
|
|
1685
|
+
.from('entity_relationships')
|
|
1686
|
+
.select()
|
|
1687
|
+
.eq('agent_id', this.agentId)
|
|
1688
|
+
.order('mention_count', { ascending: false })
|
|
1689
|
+
.order('confidence', { ascending: false })
|
|
1690
|
+
.limit(opts.limit || 50);
|
|
1691
|
+
if (opts.relationshipType) {
|
|
1692
|
+
query = query.eq('relationship_type', opts.relationshipType);
|
|
1693
|
+
}
|
|
1694
|
+
if (opts.minConfidence) {
|
|
1695
|
+
query = query.gte('confidence', opts.minConfidence);
|
|
1696
|
+
}
|
|
1697
|
+
const { data, error } = await query;
|
|
1698
|
+
if (error)
|
|
1699
|
+
throw error;
|
|
1700
|
+
return data || [];
|
|
1701
|
+
}
|
|
1702
|
+
// ============ CONTEXT ============
|
|
1703
|
+
/**
|
|
1704
|
+
* Get relevant context for a query
|
|
1705
|
+
* Combines memories, recent messages, and entities
|
|
1706
|
+
*/
|
|
1707
|
+
async getContext(query, opts = {}) {
|
|
1708
|
+
// Get relevant memories
|
|
1709
|
+
const memories = await this.recall(query, {
|
|
1710
|
+
userId: opts.userId,
|
|
1711
|
+
limit: opts.maxMemories || 5
|
|
1712
|
+
});
|
|
1713
|
+
// Get recent messages from current session
|
|
1714
|
+
let recentMessages = [];
|
|
1715
|
+
if (opts.sessionId) {
|
|
1716
|
+
recentMessages = await this.getMessages(opts.sessionId, {
|
|
1717
|
+
limit: opts.maxMessages || 20
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
// Build context summary
|
|
1721
|
+
const memoryText = memories
|
|
1722
|
+
.map(m => `- ${m.content}`)
|
|
1723
|
+
.join('\n');
|
|
1724
|
+
const summary = memories.length > 0
|
|
1725
|
+
? `Relevant memories:\n${memoryText}`
|
|
1726
|
+
: 'No relevant memories found.';
|
|
1727
|
+
return { memories, recentMessages, summary };
|
|
1728
|
+
}
|
|
1729
|
+
// ============ CONTEXT WINDOW MANAGEMENT ============
|
|
1730
|
+
/**
|
|
1731
|
+
* Build an optimized context window with token budgeting
|
|
1732
|
+
* Implements smart context selection and lost-in-middle mitigation
|
|
1733
|
+
*/
|
|
1734
|
+
async buildOptimizedContext(opts) {
|
|
1735
|
+
const { query, sessionId, userId, modelContextSize, model, useLostInMiddleFix = true, recencyWeight, importanceWeight, customBudget } = opts;
|
|
1736
|
+
// Fetch relevant data
|
|
1737
|
+
const [messages, memories, learnings, entities] = await Promise.all([
|
|
1738
|
+
sessionId ? this.getMessages(sessionId) : Promise.resolve([]),
|
|
1739
|
+
this.recall(query, { userId, limit: 50 }),
|
|
1740
|
+
this.searchLearnings(query, { limit: 20 }),
|
|
1741
|
+
this.searchEntities({ query, limit: 15 })
|
|
1742
|
+
]);
|
|
1743
|
+
// Determine budget
|
|
1744
|
+
let budget;
|
|
1745
|
+
if (customBudget) {
|
|
1746
|
+
budget = customBudget;
|
|
1747
|
+
}
|
|
1748
|
+
else if (model) {
|
|
1749
|
+
budget = (0, context_manager_1.getBudgetForModel)(model);
|
|
1750
|
+
}
|
|
1751
|
+
else if (modelContextSize) {
|
|
1752
|
+
budget = (0, context_manager_1.createContextBudget)({ modelContextSize });
|
|
1753
|
+
}
|
|
1754
|
+
else {
|
|
1755
|
+
// Adaptive budget based on available content
|
|
1756
|
+
budget = (0, context_manager_1.createAdaptiveBudget)({
|
|
1757
|
+
messageCount: messages.length,
|
|
1758
|
+
memoryCount: memories.length,
|
|
1759
|
+
learningCount: learnings.length,
|
|
1760
|
+
entityCount: entities.length
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
// Build context window
|
|
1764
|
+
const window = (0, context_manager_1.buildContextWindow)({
|
|
1765
|
+
messages,
|
|
1766
|
+
memories,
|
|
1767
|
+
learnings,
|
|
1768
|
+
entities,
|
|
1769
|
+
budget,
|
|
1770
|
+
useLostInMiddleFix,
|
|
1771
|
+
recencyWeight,
|
|
1772
|
+
importanceWeight
|
|
1773
|
+
});
|
|
1774
|
+
// Format for prompt
|
|
1775
|
+
const formatted = (0, context_manager_1.formatContextWindow)(window, {
|
|
1776
|
+
groupByType: true,
|
|
1777
|
+
includeMetadata: false
|
|
1778
|
+
});
|
|
1779
|
+
// Get stats
|
|
1780
|
+
const stats = (0, context_manager_1.getContextStats)(window);
|
|
1781
|
+
return { window, formatted, stats };
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Get smart context with automatic budget management
|
|
1785
|
+
* Simplified version of buildOptimizedContext for common use cases
|
|
1786
|
+
*/
|
|
1787
|
+
async getSmartContext(query, opts = {}) {
|
|
1788
|
+
const result = await this.buildOptimizedContext({
|
|
1789
|
+
query,
|
|
1790
|
+
sessionId: opts.sessionId,
|
|
1791
|
+
userId: opts.userId,
|
|
1792
|
+
model: opts.model || 'default'
|
|
1793
|
+
});
|
|
1794
|
+
return result.formatted;
|
|
1795
|
+
}
|
|
1796
|
+
/**
|
|
1797
|
+
* Estimate token usage for a session
|
|
1798
|
+
*/
|
|
1799
|
+
async estimateSessionTokenUsage(sessionId) {
|
|
1800
|
+
const stats = await this.countSessionTokens(sessionId);
|
|
1801
|
+
// Get memories from this session
|
|
1802
|
+
const { data, error } = await this.supabase
|
|
1803
|
+
.from('memories')
|
|
1804
|
+
.select()
|
|
1805
|
+
.eq('source_session_id', sessionId);
|
|
1806
|
+
const memoryTokens = (data || []).reduce((sum, mem) => {
|
|
1807
|
+
return sum + (mem.content.length / 4); // Rough estimate
|
|
1808
|
+
}, 0);
|
|
1809
|
+
const total = stats.totalTokens + memoryTokens;
|
|
1810
|
+
// Determine context size needed
|
|
1811
|
+
let contextSize = '4k';
|
|
1812
|
+
if (total > 4000)
|
|
1813
|
+
contextSize = '8k';
|
|
1814
|
+
if (total > 8000)
|
|
1815
|
+
contextSize = '16k';
|
|
1816
|
+
if (total > 16000)
|
|
1817
|
+
contextSize = '32k';
|
|
1818
|
+
if (total > 32000)
|
|
1819
|
+
contextSize = '64k';
|
|
1820
|
+
if (total > 64000)
|
|
1821
|
+
contextSize = '128k';
|
|
1822
|
+
if (total > 128000)
|
|
1823
|
+
contextSize = '200k';
|
|
1824
|
+
return {
|
|
1825
|
+
messages: stats.totalTokens,
|
|
1826
|
+
memories: Math.round(memoryTokens),
|
|
1827
|
+
total: Math.round(total),
|
|
1828
|
+
contextSize
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Test context window with different budgets
|
|
1833
|
+
* Useful for optimization and debugging
|
|
1834
|
+
*/
|
|
1835
|
+
async testContextBudgets(query, opts = {}) {
|
|
1836
|
+
const models = opts.models || ['gpt-3.5-turbo', 'gpt-4-turbo', 'claude-3.5-sonnet'];
|
|
1837
|
+
const results = [];
|
|
1838
|
+
for (const model of models) {
|
|
1839
|
+
const { window, stats } = await this.buildOptimizedContext({
|
|
1840
|
+
query,
|
|
1841
|
+
sessionId: opts.sessionId,
|
|
1842
|
+
userId: opts.userId,
|
|
1843
|
+
model
|
|
1844
|
+
});
|
|
1845
|
+
results.push({
|
|
1846
|
+
model,
|
|
1847
|
+
budget: window.budget,
|
|
1848
|
+
stats
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
return results;
|
|
1852
|
+
}
|
|
1853
|
+
// ============ MEMORY LIFECYCLE MANAGEMENT ============
|
|
1854
|
+
/**
|
|
1855
|
+
* Apply importance decay to memories
|
|
1856
|
+
* Reduces importance over time to prevent old memories from dominating
|
|
1857
|
+
*/
|
|
1858
|
+
async decayMemoryImportance(opts = {}) {
|
|
1859
|
+
const decayRate = opts.decayRate ?? 0.1;
|
|
1860
|
+
const minImportance = opts.minImportance ?? 0.1;
|
|
1861
|
+
const olderThanDays = opts.olderThanDays ?? 7;
|
|
1862
|
+
// Get memories to decay
|
|
1863
|
+
const cutoffDate = new Date();
|
|
1864
|
+
cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
|
|
1865
|
+
let query = this.supabase
|
|
1866
|
+
.from('memories')
|
|
1867
|
+
.select()
|
|
1868
|
+
.eq('agent_id', this.agentId)
|
|
1869
|
+
.lt('updated_at', cutoffDate.toISOString())
|
|
1870
|
+
.gt('importance', minImportance);
|
|
1871
|
+
if (opts.userId) {
|
|
1872
|
+
query = query.eq('user_id', opts.userId);
|
|
1873
|
+
}
|
|
1874
|
+
const { data: memories, error } = await query;
|
|
1875
|
+
if (error)
|
|
1876
|
+
throw error;
|
|
1877
|
+
if (!memories || memories.length === 0) {
|
|
1878
|
+
return { updated: 0, avgDecay: 0 };
|
|
1879
|
+
}
|
|
1880
|
+
// Apply decay
|
|
1881
|
+
let totalDecay = 0;
|
|
1882
|
+
const updates = memories.map(mem => {
|
|
1883
|
+
const newImportance = Math.max(minImportance, mem.importance * (1 - decayRate));
|
|
1884
|
+
totalDecay += (mem.importance - newImportance);
|
|
1885
|
+
return {
|
|
1886
|
+
id: mem.id,
|
|
1887
|
+
importance: newImportance,
|
|
1888
|
+
metadata: {
|
|
1889
|
+
...mem.metadata,
|
|
1890
|
+
last_decay: new Date().toISOString(),
|
|
1891
|
+
decay_count: (mem.metadata?.decay_count || 0) + 1
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
});
|
|
1895
|
+
// Batch update
|
|
1896
|
+
for (const update of updates) {
|
|
1897
|
+
await this.supabase
|
|
1898
|
+
.from('memories')
|
|
1899
|
+
.update({
|
|
1900
|
+
importance: update.importance,
|
|
1901
|
+
metadata: update.metadata,
|
|
1902
|
+
updated_at: new Date().toISOString()
|
|
1903
|
+
})
|
|
1904
|
+
.eq('id', update.id);
|
|
1905
|
+
}
|
|
1906
|
+
return {
|
|
1907
|
+
updated: memories.length,
|
|
1908
|
+
avgDecay: totalDecay / memories.length
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Consolidate similar memories
|
|
1913
|
+
* Merge duplicate/similar memories to reduce clutter
|
|
1914
|
+
*/
|
|
1915
|
+
async consolidateMemories(opts = {}) {
|
|
1916
|
+
const threshold = opts.similarityThreshold ?? 0.9;
|
|
1917
|
+
const limit = opts.limit ?? 100;
|
|
1918
|
+
// Get memories with embeddings
|
|
1919
|
+
let query = this.supabase
|
|
1920
|
+
.from('memories')
|
|
1921
|
+
.select()
|
|
1922
|
+
.eq('agent_id', this.agentId)
|
|
1923
|
+
.not('embedding', 'is', null)
|
|
1924
|
+
.order('created_at', { ascending: false })
|
|
1925
|
+
.limit(limit);
|
|
1926
|
+
if (opts.userId) {
|
|
1927
|
+
query = query.eq('user_id', opts.userId);
|
|
1928
|
+
}
|
|
1929
|
+
if (opts.category) {
|
|
1930
|
+
query = query.eq('category', opts.category);
|
|
1931
|
+
}
|
|
1932
|
+
const { data: memories, error } = await query;
|
|
1933
|
+
if (error)
|
|
1934
|
+
throw error;
|
|
1935
|
+
if (!memories || memories.length < 2) {
|
|
1936
|
+
return { merged: 0, kept: memories?.length || 0 };
|
|
1937
|
+
}
|
|
1938
|
+
// Find similar pairs
|
|
1939
|
+
const toMerge = [];
|
|
1940
|
+
for (let i = 0; i < memories.length; i++) {
|
|
1941
|
+
for (let j = i + 1; j < memories.length; j++) {
|
|
1942
|
+
const similarity = this.cosineSimilarity(memories[i].embedding, memories[j].embedding);
|
|
1943
|
+
if (similarity >= threshold) {
|
|
1944
|
+
// Keep the more important/recent one
|
|
1945
|
+
const [keep, merge] = memories[i].importance >= memories[j].importance
|
|
1946
|
+
? [memories[i], memories[j]]
|
|
1947
|
+
: [memories[j], memories[i]];
|
|
1948
|
+
toMerge.push({ keep, merge, similarity });
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
// Merge memories
|
|
1953
|
+
let mergedCount = 0;
|
|
1954
|
+
for (const { keep, merge, similarity } of toMerge) {
|
|
1955
|
+
// Combine content
|
|
1956
|
+
const combinedContent = `${keep.content}\n\n[Merged similar memory (similarity: ${similarity.toFixed(2)})]:\n${merge.content}`;
|
|
1957
|
+
// Update importance (weighted average)
|
|
1958
|
+
const combinedImportance = (keep.importance + merge.importance) / 2;
|
|
1959
|
+
// Update metadata
|
|
1960
|
+
const combinedMetadata = {
|
|
1961
|
+
...keep.metadata,
|
|
1962
|
+
merged_from: [
|
|
1963
|
+
...(keep.metadata?.merged_from || []),
|
|
1964
|
+
merge.id
|
|
1965
|
+
],
|
|
1966
|
+
merge_count: (keep.metadata?.merge_count || 0) + 1,
|
|
1967
|
+
last_merged: new Date().toISOString()
|
|
1968
|
+
};
|
|
1969
|
+
// Update keep
|
|
1970
|
+
await this.supabase
|
|
1971
|
+
.from('memories')
|
|
1972
|
+
.update({
|
|
1973
|
+
content: combinedContent,
|
|
1974
|
+
importance: combinedImportance,
|
|
1975
|
+
metadata: combinedMetadata,
|
|
1976
|
+
updated_at: new Date().toISOString()
|
|
1977
|
+
})
|
|
1978
|
+
.eq('id', keep.id);
|
|
1979
|
+
// Delete merge
|
|
1980
|
+
await this.supabase
|
|
1981
|
+
.from('memories')
|
|
1982
|
+
.delete()
|
|
1983
|
+
.eq('id', merge.id);
|
|
1984
|
+
mergedCount++;
|
|
1985
|
+
}
|
|
1986
|
+
return {
|
|
1987
|
+
merged: mergedCount,
|
|
1988
|
+
kept: memories.length - mergedCount
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Version a memory (create historical snapshot)
|
|
1993
|
+
*/
|
|
1994
|
+
async versionMemory(memoryId) {
|
|
1995
|
+
// Get current memory
|
|
1996
|
+
const { data: memory, error } = await this.supabase
|
|
1997
|
+
.from('memories')
|
|
1998
|
+
.select()
|
|
1999
|
+
.eq('id', memoryId)
|
|
2000
|
+
.single();
|
|
2001
|
+
if (error)
|
|
2002
|
+
throw error;
|
|
2003
|
+
// Store version in metadata
|
|
2004
|
+
const versions = memory.metadata?.versions || [];
|
|
2005
|
+
versions.push({
|
|
2006
|
+
timestamp: new Date().toISOString(),
|
|
2007
|
+
content: memory.content,
|
|
2008
|
+
importance: memory.importance
|
|
2009
|
+
});
|
|
2010
|
+
const versionId = `v${versions.length}`;
|
|
2011
|
+
// Update metadata
|
|
2012
|
+
await this.supabase
|
|
2013
|
+
.from('memories')
|
|
2014
|
+
.update({
|
|
2015
|
+
metadata: {
|
|
2016
|
+
...memory.metadata,
|
|
2017
|
+
versions,
|
|
2018
|
+
current_version: versionId
|
|
2019
|
+
}
|
|
2020
|
+
})
|
|
2021
|
+
.eq('id', memoryId);
|
|
2022
|
+
return { memory, versionId };
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Get memory version history
|
|
2026
|
+
*/
|
|
2027
|
+
async getMemoryVersions(memoryId) {
|
|
2028
|
+
const { data: memory, error } = await this.supabase
|
|
2029
|
+
.from('memories')
|
|
2030
|
+
.select()
|
|
2031
|
+
.eq('id', memoryId)
|
|
2032
|
+
.single();
|
|
2033
|
+
if (error)
|
|
2034
|
+
throw error;
|
|
2035
|
+
const versions = memory.metadata?.versions || [];
|
|
2036
|
+
return versions.map((v, i) => ({
|
|
2037
|
+
version: `v${i + 1}`,
|
|
2038
|
+
...v
|
|
2039
|
+
}));
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Tag memories for organization
|
|
2043
|
+
*/
|
|
2044
|
+
async tagMemory(memoryId, tags) {
|
|
2045
|
+
const { data: memory, error: fetchError } = await this.supabase
|
|
2046
|
+
.from('memories')
|
|
2047
|
+
.select()
|
|
2048
|
+
.eq('id', memoryId)
|
|
2049
|
+
.single();
|
|
2050
|
+
if (fetchError)
|
|
2051
|
+
throw fetchError;
|
|
2052
|
+
const existingTags = memory.metadata?.tags || [];
|
|
2053
|
+
const newTags = Array.from(new Set([...existingTags, ...tags]));
|
|
2054
|
+
const { data, error } = await this.supabase
|
|
2055
|
+
.from('memories')
|
|
2056
|
+
.update({
|
|
2057
|
+
metadata: {
|
|
2058
|
+
...memory.metadata,
|
|
2059
|
+
tags: newTags
|
|
2060
|
+
},
|
|
2061
|
+
updated_at: new Date().toISOString()
|
|
2062
|
+
})
|
|
2063
|
+
.eq('id', memoryId)
|
|
2064
|
+
.select()
|
|
2065
|
+
.single();
|
|
2066
|
+
if (error)
|
|
2067
|
+
throw error;
|
|
2068
|
+
return data;
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Remove tags from memory
|
|
2072
|
+
*/
|
|
2073
|
+
async untagMemory(memoryId, tags) {
|
|
2074
|
+
const { data: memory, error: fetchError } = await this.supabase
|
|
2075
|
+
.from('memories')
|
|
2076
|
+
.select()
|
|
2077
|
+
.eq('id', memoryId)
|
|
2078
|
+
.single();
|
|
2079
|
+
if (fetchError)
|
|
2080
|
+
throw fetchError;
|
|
2081
|
+
const existingTags = memory.metadata?.tags || [];
|
|
2082
|
+
const newTags = existingTags.filter(t => !tags.includes(t));
|
|
2083
|
+
const { data, error } = await this.supabase
|
|
2084
|
+
.from('memories')
|
|
2085
|
+
.update({
|
|
2086
|
+
metadata: {
|
|
2087
|
+
...memory.metadata,
|
|
2088
|
+
tags: newTags
|
|
2089
|
+
},
|
|
2090
|
+
updated_at: new Date().toISOString()
|
|
2091
|
+
})
|
|
2092
|
+
.eq('id', memoryId)
|
|
2093
|
+
.select()
|
|
2094
|
+
.single();
|
|
2095
|
+
if (error)
|
|
2096
|
+
throw error;
|
|
2097
|
+
return data;
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Search memories by tags
|
|
2101
|
+
*/
|
|
2102
|
+
async searchMemoriesByTags(tags, opts = {}) {
|
|
2103
|
+
let query = this.supabase
|
|
2104
|
+
.from('memories')
|
|
2105
|
+
.select()
|
|
2106
|
+
.eq('agent_id', this.agentId);
|
|
2107
|
+
if (opts.userId) {
|
|
2108
|
+
query = query.eq('user_id', opts.userId);
|
|
2109
|
+
}
|
|
2110
|
+
const { data, error } = await query;
|
|
2111
|
+
if (error)
|
|
2112
|
+
throw error;
|
|
2113
|
+
// Filter by tags
|
|
2114
|
+
const filtered = (data || []).filter(mem => {
|
|
2115
|
+
const memTags = mem.metadata?.tags || [];
|
|
2116
|
+
if (opts.matchAll) {
|
|
2117
|
+
return tags.every(tag => memTags.includes(tag));
|
|
2118
|
+
}
|
|
2119
|
+
else {
|
|
2120
|
+
return tags.some(tag => memTags.includes(tag));
|
|
2121
|
+
}
|
|
2122
|
+
});
|
|
2123
|
+
return filtered.slice(0, opts.limit || 50);
|
|
2124
|
+
}
|
|
2125
|
+
/**
|
|
2126
|
+
* Auto-cleanup old sessions
|
|
2127
|
+
* Archive or delete sessions older than a threshold
|
|
2128
|
+
*/
|
|
2129
|
+
async cleanupOldSessions(opts = {}) {
|
|
2130
|
+
const olderThanDays = opts.olderThanDays ?? 90;
|
|
2131
|
+
const action = opts.action ?? 'archive';
|
|
2132
|
+
const keepSummaries = opts.keepSummaries ?? true;
|
|
2133
|
+
const cutoffDate = new Date();
|
|
2134
|
+
cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
|
|
2135
|
+
let query = this.supabase
|
|
2136
|
+
.from('sessions')
|
|
2137
|
+
.select()
|
|
2138
|
+
.eq('agent_id', this.agentId)
|
|
2139
|
+
.lt('started_at', cutoffDate.toISOString());
|
|
2140
|
+
if (opts.userId) {
|
|
2141
|
+
query = query.eq('user_id', opts.userId);
|
|
2142
|
+
}
|
|
2143
|
+
if (keepSummaries) {
|
|
2144
|
+
query = query.is('summary', null);
|
|
2145
|
+
}
|
|
2146
|
+
const { data: sessions, error } = await query;
|
|
2147
|
+
if (error)
|
|
2148
|
+
throw error;
|
|
2149
|
+
if (!sessions || sessions.length === 0) {
|
|
2150
|
+
return action === 'delete' ? { deleted: 0 } : { archived: 0 };
|
|
2151
|
+
}
|
|
2152
|
+
if (action === 'delete') {
|
|
2153
|
+
// Delete sessions and their messages
|
|
2154
|
+
for (const session of sessions) {
|
|
2155
|
+
// Delete messages first
|
|
2156
|
+
await this.supabase
|
|
2157
|
+
.from('messages')
|
|
2158
|
+
.delete()
|
|
2159
|
+
.eq('session_id', session.id);
|
|
2160
|
+
// Delete session
|
|
2161
|
+
await this.supabase
|
|
2162
|
+
.from('sessions')
|
|
2163
|
+
.delete()
|
|
2164
|
+
.eq('id', session.id);
|
|
2165
|
+
}
|
|
2166
|
+
return { deleted: sessions.length };
|
|
2167
|
+
}
|
|
2168
|
+
else {
|
|
2169
|
+
// Archive by marking in metadata
|
|
2170
|
+
for (const session of sessions) {
|
|
2171
|
+
await this.supabase
|
|
2172
|
+
.from('sessions')
|
|
2173
|
+
.update({
|
|
2174
|
+
metadata: {
|
|
2175
|
+
...session.metadata,
|
|
2176
|
+
archived: true,
|
|
2177
|
+
archived_at: new Date().toISOString()
|
|
2178
|
+
}
|
|
2179
|
+
})
|
|
2180
|
+
.eq('id', session.id);
|
|
2181
|
+
}
|
|
2182
|
+
return { archived: sessions.length };
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
2186
|
+
* Get cleanup statistics
|
|
2187
|
+
*/
|
|
2188
|
+
async getCleanupStats() {
|
|
2189
|
+
const { data: sessions, error: sessError } = await this.supabase
|
|
2190
|
+
.from('sessions')
|
|
2191
|
+
.select()
|
|
2192
|
+
.eq('agent_id', this.agentId);
|
|
2193
|
+
if (sessError)
|
|
2194
|
+
throw sessError;
|
|
2195
|
+
const totalSessions = sessions?.length || 0;
|
|
2196
|
+
const archivedSessions = sessions?.filter(s => s.metadata?.archived).length || 0;
|
|
2197
|
+
const cutoffDate = new Date();
|
|
2198
|
+
cutoffDate.setDate(cutoffDate.getDate() - 90);
|
|
2199
|
+
const oldSessions = sessions?.filter(s => new Date(s.started_at) < cutoffDate).length || 0;
|
|
2200
|
+
const { data: messages, error: msgError } = await this.supabase
|
|
2201
|
+
.from('messages')
|
|
2202
|
+
.select()
|
|
2203
|
+
.in('session_id', sessions?.map(s => s.id) || []);
|
|
2204
|
+
if (msgError)
|
|
2205
|
+
throw msgError;
|
|
2206
|
+
const totalMessages = messages?.length || 0;
|
|
2207
|
+
// Find orphaned messages (messages without sessions)
|
|
2208
|
+
const { data: allMessages, error: allMsgError } = await this.supabase
|
|
2209
|
+
.from('messages')
|
|
2210
|
+
.select('id, session_id');
|
|
2211
|
+
if (allMsgError)
|
|
2212
|
+
throw allMsgError;
|
|
2213
|
+
const sessionIds = new Set(sessions?.map(s => s.id) || []);
|
|
2214
|
+
const orphanedMessages = allMessages?.filter(m => !sessionIds.has(m.session_id)).length || 0;
|
|
2215
|
+
return {
|
|
2216
|
+
totalSessions,
|
|
2217
|
+
archivedSessions,
|
|
2218
|
+
oldSessions,
|
|
2219
|
+
totalMessages,
|
|
2220
|
+
orphanedMessages
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
exports.OpenClawMemory = OpenClawMemory;
|
|
2225
|
+
// Re-export context manager utilities
|
|
2226
|
+
var context_manager_2 = require("./context-manager");
|
|
2227
|
+
Object.defineProperty(exports, "createContextBudget", { enumerable: true, get: function () { return context_manager_2.createContextBudget; } });
|
|
2228
|
+
Object.defineProperty(exports, "createAdaptiveBudget", { enumerable: true, get: function () { return context_manager_2.createAdaptiveBudget; } });
|
|
2229
|
+
Object.defineProperty(exports, "buildContextWindow", { enumerable: true, get: function () { return context_manager_2.buildContextWindow; } });
|
|
2230
|
+
Object.defineProperty(exports, "formatContextWindow", { enumerable: true, get: function () { return context_manager_2.formatContextWindow; } });
|
|
2231
|
+
Object.defineProperty(exports, "getContextStats", { enumerable: true, get: function () { return context_manager_2.getContextStats; } });
|
|
2232
|
+
Object.defineProperty(exports, "getBudgetForModel", { enumerable: true, get: function () { return context_manager_2.getBudgetForModel; } });
|
|
2233
|
+
Object.defineProperty(exports, "estimateTokens", { enumerable: true, get: function () { return context_manager_2.estimateTokens; } });
|
|
2234
|
+
Object.defineProperty(exports, "estimateTokensAccurate", { enumerable: true, get: function () { return context_manager_2.estimateTokensAccurate; } });
|
|
2235
|
+
// Export Clawdbot integration
|
|
2236
|
+
var clawdbot_integration_1 = require("./clawdbot-integration");
|
|
2237
|
+
Object.defineProperty(exports, "ClawdbotMemoryIntegration", { enumerable: true, get: function () { return clawdbot_integration_1.ClawdbotMemoryIntegration; } });
|
|
2238
|
+
Object.defineProperty(exports, "createClawdbotIntegration", { enumerable: true, get: function () { return clawdbot_integration_1.createClawdbotIntegration; } });
|
|
2239
|
+
Object.defineProperty(exports, "createLoggingMiddleware", { enumerable: true, get: function () { return clawdbot_integration_1.createLoggingMiddleware; } });
|
|
2240
|
+
// Export error handling utilities
|
|
2241
|
+
var error_handling_1 = require("./error-handling");
|
|
2242
|
+
Object.defineProperty(exports, "OpenClawError", { enumerable: true, get: function () { return error_handling_1.OpenClawError; } });
|
|
2243
|
+
Object.defineProperty(exports, "DatabaseError", { enumerable: true, get: function () { return error_handling_1.DatabaseError; } });
|
|
2244
|
+
Object.defineProperty(exports, "EmbeddingError", { enumerable: true, get: function () { return error_handling_1.EmbeddingError; } });
|
|
2245
|
+
Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return error_handling_1.ValidationError; } });
|
|
2246
|
+
Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return error_handling_1.RateLimitError; } });
|
|
2247
|
+
Object.defineProperty(exports, "CircuitBreaker", { enumerable: true, get: function () { return error_handling_1.CircuitBreaker; } });
|
|
2248
|
+
Object.defineProperty(exports, "retry", { enumerable: true, get: function () { return error_handling_1.retry; } });
|
|
2249
|
+
Object.defineProperty(exports, "wrapDatabaseOperation", { enumerable: true, get: function () { return error_handling_1.wrapDatabaseOperation; } });
|
|
2250
|
+
Object.defineProperty(exports, "wrapEmbeddingOperation", { enumerable: true, get: function () { return error_handling_1.wrapEmbeddingOperation; } });
|
|
2251
|
+
Object.defineProperty(exports, "validateInput", { enumerable: true, get: function () { return error_handling_1.validateInput; } });
|
|
2252
|
+
Object.defineProperty(exports, "safeJsonParse", { enumerable: true, get: function () { return error_handling_1.safeJsonParse; } });
|
|
2253
|
+
Object.defineProperty(exports, "withTimeout", { enumerable: true, get: function () { return error_handling_1.withTimeout; } });
|
|
2254
|
+
Object.defineProperty(exports, "gracefulFallback", { enumerable: true, get: function () { return error_handling_1.gracefulFallback; } });
|
|
2255
|
+
Object.defineProperty(exports, "batchWithErrorHandling", { enumerable: true, get: function () { return error_handling_1.batchWithErrorHandling; } });
|
|
2256
|
+
exports.default = OpenClawMemory;
|