@cccarv82/freya 3.5.2 → 3.6.1
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/cli/init.js +2 -1
- package/cli/web.js +242 -65
- package/package.json +2 -1
- package/scripts/lib/DataLayer.js +6 -0
- package/scripts/lib/DataManager.js +89 -0
- package/scripts/retroactive-ingest.js +386 -0
- package/templates/base/scripts/lib/DataLayer.js +6 -0
- package/templates/base/scripts/lib/DataManager.js +89 -0
- package/templates/base/scripts/retroactive-ingest.js +386 -0
package/cli/init.js
CHANGED
|
@@ -88,7 +88,8 @@ function ensurePackageJson(targetDir, force, summary) {
|
|
|
88
88
|
'sm-weekly': 'node scripts/generate-sm-weekly-report.js',
|
|
89
89
|
daily: 'node scripts/generate-daily-summary.js',
|
|
90
90
|
status: 'node scripts/generate-executive-report.js',
|
|
91
|
-
blockers: 'node scripts/generate-blockers-report.js'
|
|
91
|
+
blockers: 'node scripts/generate-blockers-report.js',
|
|
92
|
+
'retroactive-ingest': 'node scripts/retroactive-ingest.js'
|
|
92
93
|
};
|
|
93
94
|
|
|
94
95
|
const depsToEnsure = {
|
package/cli/web.js
CHANGED
|
@@ -104,9 +104,9 @@ function newestFile(dir, prefix) {
|
|
|
104
104
|
function syncDailyLogs(workspaceDir) {
|
|
105
105
|
try {
|
|
106
106
|
const logsDir = path.join(workspaceDir, 'logs', 'daily');
|
|
107
|
-
if (!exists(logsDir)) return 0;
|
|
107
|
+
if (!exists(logsDir)) return { synced: 0, toEmbed: [] };
|
|
108
108
|
const files = fs.readdirSync(logsDir).filter(f => /^\d{4}-\d{2}-\d{2}\.md$/.test(f));
|
|
109
|
-
if (!files.length) return 0;
|
|
109
|
+
if (!files.length) return { synced: 0, toEmbed: [] };
|
|
110
110
|
|
|
111
111
|
const upsert = dl.db.prepare(`
|
|
112
112
|
INSERT INTO daily_logs (date, raw_markdown) VALUES (?, ?)
|
|
@@ -114,88 +114,87 @@ function syncDailyLogs(workspaceDir) {
|
|
|
114
114
|
`);
|
|
115
115
|
|
|
116
116
|
let synced = 0;
|
|
117
|
+
const toEmbed = []; // collect logs that need embedding
|
|
117
118
|
const tx = dl.db.transaction((fileList) => {
|
|
118
119
|
for (const file of fileList) {
|
|
119
120
|
const date = file.replace('.md', '');
|
|
120
121
|
const content = fs.readFileSync(path.join(logsDir, file), 'utf8');
|
|
121
122
|
upsert.run(date, content);
|
|
123
|
+
toEmbed.push({ date, content });
|
|
122
124
|
synced++;
|
|
123
125
|
}
|
|
124
126
|
});
|
|
125
127
|
tx(files);
|
|
126
|
-
return synced;
|
|
128
|
+
return { synced, toEmbed };
|
|
127
129
|
} catch (e) {
|
|
128
130
|
console.error('[sync] Daily-logs sync failed:', e.message);
|
|
129
|
-
return 0;
|
|
131
|
+
return { synced: 0, toEmbed: [] };
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
// ---------------------------------------------------------------------------
|
|
134
|
-
//
|
|
135
|
-
// as plain-text so the LLM has actual data to synthesize answers from
|
|
136
|
+
// Background embedding generation — runs async, never blocks
|
|
136
137
|
// ---------------------------------------------------------------------------
|
|
137
|
-
function
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// 1. Recent daily logs (from filesystem — most up-to-date source)
|
|
138
|
+
async function generateEmbeddingsBackground(workspaceDir, items) {
|
|
139
|
+
// items = [{ type: 'daily_log'|'task'|'blocker', id: string, text: string }]
|
|
140
|
+
if (!items || !items.length) return;
|
|
142
141
|
try {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
// Truncate very large logs to avoid token overflow
|
|
155
|
-
const trimmed = content.length > 8000 ? content.slice(0, 8000) + '\n...(truncado)' : content;
|
|
156
|
-
parts.push(`\n--- LOG ${date} ---\n${trimmed}`);
|
|
157
|
-
}
|
|
142
|
+
const dm = new DataManager(workspaceDir, path.join(workspaceDir, 'logs'));
|
|
143
|
+
let generated = 0;
|
|
144
|
+
for (const item of items) {
|
|
145
|
+
try {
|
|
146
|
+
// Skip if embeddings already exist AND item is not a daily_log
|
|
147
|
+
// (daily logs get updated frequently, so always regenerate)
|
|
148
|
+
if (item.type !== 'daily_log' && dm.hasEmbeddings(item.type, item.id)) continue;
|
|
149
|
+
const count = await dm.generateEmbeddings(item.type, item.id, item.text);
|
|
150
|
+
generated += count;
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error(`[embeddings] Failed for ${item.type}/${item.id}:`, err.message);
|
|
158
153
|
}
|
|
159
154
|
}
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
if (generated > 0) console.log(`[embeddings] Generated ${generated} embedding chunks`);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.error('[embeddings] Background generation failed:', err.message);
|
|
162
158
|
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Build structured data context (tasks, blockers, projects) — always compact
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
function buildStructuredContext() {
|
|
165
|
+
const parts = [];
|
|
163
166
|
|
|
164
|
-
//
|
|
167
|
+
// Pending tasks
|
|
165
168
|
try {
|
|
166
169
|
const tasks = dl.db.prepare("SELECT id, description, category, status, project_slug, created_at, due_date FROM tasks WHERE status = 'PENDING' ORDER BY created_at DESC LIMIT 50").all();
|
|
167
170
|
if (tasks.length) {
|
|
168
|
-
parts.push('\n
|
|
171
|
+
parts.push('\n[TASKS PENDENTES (' + tasks.length + ')]');
|
|
169
172
|
for (const t of tasks) {
|
|
170
|
-
parts.push(`• [${t.category}] ${t.description} (projeto: ${t.project_slug || 'N/A'}
|
|
173
|
+
parts.push(`• [${t.category}] ${t.description} (projeto: ${t.project_slug || 'N/A'}${t.due_date ? ', prazo: ' + t.due_date : ''})`);
|
|
171
174
|
}
|
|
172
175
|
} else {
|
|
173
|
-
parts.push('\n
|
|
176
|
+
parts.push('\n[TASKS: nenhuma task pendente registrada no sistema]');
|
|
174
177
|
}
|
|
175
|
-
} catch
|
|
176
|
-
parts.push('\n\n[TASKS: erro ao consultar SQLite — ' + e.message + ']');
|
|
177
|
-
}
|
|
178
|
+
} catch { /* ignore */ }
|
|
178
179
|
|
|
179
|
-
//
|
|
180
|
+
// Open blockers
|
|
180
181
|
try {
|
|
181
182
|
const blockers = dl.db.prepare("SELECT id, title, severity, status, project_slug, owner, next_action, created_at FROM blockers WHERE status IN ('OPEN','MITIGATING') ORDER BY created_at DESC LIMIT 30").all();
|
|
182
183
|
if (blockers.length) {
|
|
183
|
-
parts.push('\n
|
|
184
|
+
parts.push('\n[BLOCKERS ABERTOS (' + blockers.length + ')]');
|
|
184
185
|
for (const b of blockers) {
|
|
185
|
-
parts.push(`• [${b.severity}] ${b.title} (projeto: ${b.project_slug || 'N/A'},
|
|
186
|
+
parts.push(`• [${b.severity}] ${b.title} (projeto: ${b.project_slug || 'N/A'}, owner: ${b.owner || '?'})`);
|
|
186
187
|
}
|
|
187
188
|
} else {
|
|
188
|
-
parts.push('\n
|
|
189
|
+
parts.push('\n[BLOCKERS: nenhum blocker aberto registrado no sistema]');
|
|
189
190
|
}
|
|
190
|
-
} catch
|
|
191
|
-
parts.push('\n\n[BLOCKERS: erro ao consultar SQLite — ' + e.message + ']');
|
|
192
|
-
}
|
|
191
|
+
} catch { /* ignore */ }
|
|
193
192
|
|
|
194
|
-
//
|
|
193
|
+
// Active projects
|
|
195
194
|
try {
|
|
196
195
|
const projects = dl.db.prepare("SELECT slug, client, name FROM projects WHERE is_active = 1 ORDER BY slug").all();
|
|
197
196
|
if (projects.length) {
|
|
198
|
-
parts.push('\n
|
|
197
|
+
parts.push('\n[PROJETOS ATIVOS (' + projects.length + ')]');
|
|
199
198
|
for (const p of projects) {
|
|
200
199
|
parts.push(`• ${p.slug} — ${p.name || p.client || 'sem nome'}`);
|
|
201
200
|
}
|
|
@@ -205,6 +204,173 @@ function buildDataContext(workspaceDir, maxDays) {
|
|
|
205
204
|
return parts.join('\n');
|
|
206
205
|
}
|
|
207
206
|
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Smart context builder: uses RAG when available, falls back to raw logs
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
async function buildSmartContext(workspaceDir, query) {
|
|
211
|
+
const parts = [];
|
|
212
|
+
const dm = new DataManager(workspaceDir, path.join(workspaceDir, 'logs'));
|
|
213
|
+
const TOKEN_BUDGET = 12000; // chars budget for log/RAG context
|
|
214
|
+
let usedBudget = 0;
|
|
215
|
+
|
|
216
|
+
// 1. Try semantic search first (if embeddings exist)
|
|
217
|
+
let ragUsed = false;
|
|
218
|
+
try {
|
|
219
|
+
const embCount = dm.getEmbeddingCount();
|
|
220
|
+
if (embCount > 0) {
|
|
221
|
+
const ragResults = await dm.semanticSearch(query, 15);
|
|
222
|
+
const relevant = ragResults.filter(r => r.score > 0.25);
|
|
223
|
+
if (relevant.length > 0) {
|
|
224
|
+
ragUsed = true;
|
|
225
|
+
parts.push('\n[CONTEXTO RELEVANTE — Busca Semântica]');
|
|
226
|
+
for (const r of relevant) {
|
|
227
|
+
const chunk = `\n--- ${r.reference_type} (${r.reference_id}) [relevância: ${Math.round(r.score * 100)}%] ---\n${r.text_chunk}`;
|
|
228
|
+
if (usedBudget + chunk.length > TOKEN_BUDGET) break;
|
|
229
|
+
parts.push(chunk);
|
|
230
|
+
usedBudget += chunk.length;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch (ragErr) {
|
|
235
|
+
console.error('[context] RAG search failed:', ragErr.message);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 2. Fallback: if RAG not available or returned few results, include recent daily logs from SQLite
|
|
239
|
+
if (!ragUsed || usedBudget < TOKEN_BUDGET / 3) {
|
|
240
|
+
try {
|
|
241
|
+
const maxDays = ragUsed ? 3 : 5;
|
|
242
|
+
const recentLogs = dl.db.prepare(
|
|
243
|
+
`SELECT date, raw_markdown FROM daily_logs ORDER BY date DESC LIMIT ?`
|
|
244
|
+
).all(maxDays);
|
|
245
|
+
if (recentLogs.length) {
|
|
246
|
+
parts.push('\n[DAILY LOGS — ÚLTIMOS ' + recentLogs.length + ' DIAS]');
|
|
247
|
+
// Reverse to show chronologically (oldest first)
|
|
248
|
+
for (const log of recentLogs.reverse()) {
|
|
249
|
+
const maxPerLog = Math.floor((TOKEN_BUDGET - usedBudget) / recentLogs.length);
|
|
250
|
+
const content = log.raw_markdown || '';
|
|
251
|
+
const trimmed = content.length > maxPerLog ? content.slice(0, maxPerLog) + '\n...(truncado)' : content;
|
|
252
|
+
parts.push(`\n--- LOG ${log.date} ---\n${trimmed}`);
|
|
253
|
+
usedBudget += trimmed.length;
|
|
254
|
+
if (usedBudget >= TOKEN_BUDGET) break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch (e) {
|
|
258
|
+
console.error('[context] Failed to read daily logs from SQLite:', e.message);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 3. Always include structured data (compact, always useful)
|
|
263
|
+
parts.push(buildStructuredContext());
|
|
264
|
+
|
|
265
|
+
return parts.join('\n');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// Background auto-ingest from chat: extracts tasks/blockers from conversation
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
const INGEST_SIGNALS = /\b(criar|crie|registr|task|tarefa|blocker|impediment|problem|urgente|preciso|agendar|schedule|delegat|prioriz|adicionar?|anotar?|lembr|reminder|todo|pendente|pendência)\b/i;
|
|
272
|
+
const QUERY_ONLY = /^(o que|como|quando|qual|quais|quem|onde|por que|porque|quantos?|existe|tem |show|list|status|resumo|report|relatório|buscar?|search|find)/i;
|
|
273
|
+
|
|
274
|
+
async function backgroundIngestFromChat(workspaceDir, userQuery) {
|
|
275
|
+
// Skip pure queries — only ingest actionable messages
|
|
276
|
+
if (!userQuery || userQuery.length < 25) return;
|
|
277
|
+
if (QUERY_ONLY.test(userQuery.trim()) && !INGEST_SIGNALS.test(userQuery)) return;
|
|
278
|
+
if (!INGEST_SIGNALS.test(userQuery)) return;
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const cmd = process.env.COPILOT_CMD || 'copilot';
|
|
282
|
+
|
|
283
|
+
// Build a minimal planner prompt
|
|
284
|
+
const schema = {
|
|
285
|
+
actions: [
|
|
286
|
+
{ type: 'create_task', description: '<string>', priority: 'HIGH|MEDIUM|LOW', category: 'DO_NOW|SCHEDULE|DELEGATE|IGNORE', projectSlug: '<string optional>' },
|
|
287
|
+
{ type: 'create_blocker', title: '<string>', severity: 'CRITICAL|HIGH|MEDIUM|LOW', notes: '<string>', projectSlug: '<string optional>' }
|
|
288
|
+
]
|
|
289
|
+
};
|
|
290
|
+
const prompt = `Você é o planner do sistema F.R.E.Y.A.\n\nAnalise o texto abaixo e extraia APENAS tarefas e blockers explícitos.\nSe NÃO houver tarefas ou blockers claros, retorne: {"actions":[]}\nRetorne APENAS JSON válido no formato: ${JSON.stringify(schema)}\nNÃO use code fences. NÃO inclua texto extra.\n\nTEXTO:\n${userQuery}\n`;
|
|
291
|
+
|
|
292
|
+
const agentEnv = { FREYA_WORKSPACE_DIR: workspaceDir };
|
|
293
|
+
const baseArgs = ['-s', '--no-color', '--stream', 'off', '-p', prompt];
|
|
294
|
+
|
|
295
|
+
const r = await run(cmd, baseArgs, workspaceDir, agentEnv);
|
|
296
|
+
const out = (r.stdout + r.stderr).trim();
|
|
297
|
+
if (r.code !== 0 || !out) return;
|
|
298
|
+
|
|
299
|
+
// Try to parse JSON plan
|
|
300
|
+
const jsonText = extractFirstJsonObject(out) || out;
|
|
301
|
+
let plan;
|
|
302
|
+
try {
|
|
303
|
+
plan = JSON.parse(jsonText);
|
|
304
|
+
} catch {
|
|
305
|
+
try { plan = JSON.parse(escapeJsonControlChars(jsonText)); } catch { return; }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const actions = Array.isArray(plan.actions) ? plan.actions : [];
|
|
309
|
+
const taskActions = actions.filter(a => a && a.type === 'create_task' && a.description);
|
|
310
|
+
const blockerActions = actions.filter(a => a && a.type === 'create_blocker' && a.title);
|
|
311
|
+
|
|
312
|
+
if (!taskActions.length && !blockerActions.length) return;
|
|
313
|
+
|
|
314
|
+
// Apply actions directly to SQLite
|
|
315
|
+
const slugMap = readProjectSlugMap(workspaceDir);
|
|
316
|
+
const validTaskCats = new Set(['DO_NOW', 'SCHEDULE', 'DELEGATE', 'IGNORE']);
|
|
317
|
+
const insertTask = dl.db.prepare(`INSERT INTO tasks (id, project_slug, description, category, status, metadata) VALUES (?, ?, ?, ?, ?, ?)`);
|
|
318
|
+
const insertBlocker = dl.db.prepare(`INSERT INTO blockers (id, project_slug, title, severity, status, owner, next_action, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
319
|
+
|
|
320
|
+
let tasksCreated = 0, blockersCreated = 0;
|
|
321
|
+
const embeddingQueue = [];
|
|
322
|
+
|
|
323
|
+
const ingestTx = dl.db.transaction(() => {
|
|
324
|
+
// Dedup check
|
|
325
|
+
const recentTasks = dl.db.prepare("SELECT description FROM tasks WHERE created_at >= datetime('now', '-1 day')").all();
|
|
326
|
+
const existingKeys = new Set(recentTasks.map(t => sha1(normalizeTextForKey(t.description))));
|
|
327
|
+
const recentBlockers = dl.db.prepare("SELECT title FROM blockers WHERE created_at >= datetime('now', '-1 day')").all();
|
|
328
|
+
const existingBKeys = new Set(recentBlockers.map(b => sha1(normalizeTextForKey(b.title))));
|
|
329
|
+
|
|
330
|
+
for (const a of taskActions) {
|
|
331
|
+
const desc = normalizeWhitespace(a.description);
|
|
332
|
+
if (!desc) continue;
|
|
333
|
+
const projectSlug = String(a.projectSlug || '').trim() || inferProjectSlug(desc, slugMap);
|
|
334
|
+
const key = sha1(normalizeTextForKey((projectSlug ? projectSlug + ' ' : '') + desc));
|
|
335
|
+
if (existingKeys.has(key)) continue;
|
|
336
|
+
|
|
337
|
+
const id = `t-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
|
338
|
+
const category = validTaskCats.has(String(a.category || '').trim()) ? String(a.category).trim() : 'DO_NOW';
|
|
339
|
+
const metadata = JSON.stringify({ priority: a.priority || 'medium' });
|
|
340
|
+
insertTask.run(id, projectSlug || null, desc, category, 'PENDING', metadata);
|
|
341
|
+
existingKeys.add(key);
|
|
342
|
+
tasksCreated++;
|
|
343
|
+
embeddingQueue.push({ type: 'task', id, text: desc });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for (const a of blockerActions) {
|
|
347
|
+
const title = normalizeWhitespace(a.title);
|
|
348
|
+
if (!title) continue;
|
|
349
|
+
const projectSlug = String(a.projectSlug || '').trim() || inferProjectSlug(title, slugMap);
|
|
350
|
+
const key = sha1(normalizeTextForKey((projectSlug ? projectSlug + ' ' : '') + title));
|
|
351
|
+
if (existingBKeys.has(key)) continue;
|
|
352
|
+
|
|
353
|
+
const id = `b-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
|
354
|
+
const severity = String(a.severity || 'MEDIUM').toUpperCase();
|
|
355
|
+
const metadata = JSON.stringify({ description: a.notes || title });
|
|
356
|
+
insertBlocker.run(id, projectSlug || null, title, severity, 'OPEN', null, null, metadata);
|
|
357
|
+
existingBKeys.add(key);
|
|
358
|
+
blockersCreated++;
|
|
359
|
+
embeddingQueue.push({ type: 'blocker', id, text: title + ' ' + (a.notes || '') });
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
ingestTx();
|
|
363
|
+
|
|
364
|
+
if (tasksCreated || blockersCreated) {
|
|
365
|
+
console.log(`[chat-ingest] Auto-ingested ${tasksCreated} tasks, ${blockersCreated} blockers from chat`);
|
|
366
|
+
// Generate embeddings in background
|
|
367
|
+
generateEmbeddingsBackground(workspaceDir, embeddingQueue).catch(() => {});
|
|
368
|
+
}
|
|
369
|
+
} catch (err) {
|
|
370
|
+
console.error('[chat-ingest] Background ingestion failed:', err.message);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
208
374
|
function settingsPath(workspaceDir) {
|
|
209
375
|
return path.join(workspaceDir, 'data', 'settings', 'settings.json');
|
|
210
376
|
}
|
|
@@ -2864,8 +3030,15 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
2864
3030
|
|
|
2865
3031
|
// Sync daily log .md files → SQLite daily_logs table on startup
|
|
2866
3032
|
try {
|
|
2867
|
-
const synced = syncDailyLogs(wsDir);
|
|
3033
|
+
const { synced, toEmbed } = syncDailyLogs(wsDir);
|
|
2868
3034
|
if (synced > 0) console.log(`[FREYA] Synced ${synced} daily logs to SQLite`);
|
|
3035
|
+
// Generate embeddings in background (non-blocking, last 30 days max)
|
|
3036
|
+
if (toEmbed.length > 0) {
|
|
3037
|
+
const recentLogs = toEmbed.slice(-30).map(l => ({ type: 'daily_log', id: l.date, text: l.content }));
|
|
3038
|
+
generateEmbeddingsBackground(wsDir, recentLogs).catch(err => {
|
|
3039
|
+
console.error('[FREYA] Embedding generation failed (non-fatal):', err.message);
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
2869
3042
|
} catch (e) {
|
|
2870
3043
|
console.error('[FREYA] Warning: daily-logs sync failed:', e.message || String(e));
|
|
2871
3044
|
}
|
|
@@ -3853,8 +4026,11 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
3853
4026
|
|
|
3854
4027
|
// Sync this daily log file to SQLite so chat/RAG can find it
|
|
3855
4028
|
try {
|
|
4029
|
+
const logContent = fs.readFileSync(file, 'utf8');
|
|
3856
4030
|
const upsert = dl.db.prepare(`INSERT INTO daily_logs (date, raw_markdown) VALUES (?, ?) ON CONFLICT(date) DO UPDATE SET raw_markdown = excluded.raw_markdown`);
|
|
3857
|
-
upsert.run(d,
|
|
4031
|
+
upsert.run(d, logContent);
|
|
4032
|
+
// Regenerate embeddings for this log in background
|
|
4033
|
+
generateEmbeddingsBackground(workspaceDir, [{ type: 'daily_log', id: d, text: logContent }]).catch(() => {});
|
|
3858
4034
|
} catch (syncErr) {
|
|
3859
4035
|
console.error('[inbox] Failed to sync daily log to SQLite:', syncErr.message);
|
|
3860
4036
|
}
|
|
@@ -4220,6 +4396,8 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
4220
4396
|
insertTask.run(id, projectSlug || null, description, category, 'PENDING', metadata);
|
|
4221
4397
|
|
|
4222
4398
|
applied.tasks++;
|
|
4399
|
+
if (!applied._embedQueue) applied._embedQueue = [];
|
|
4400
|
+
applied._embedQueue.push({ type: 'task', id, text: description });
|
|
4223
4401
|
existingTaskKeys24h.add(key); // prevent duplicates within same batch
|
|
4224
4402
|
continue;
|
|
4225
4403
|
}
|
|
@@ -4240,6 +4418,8 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
4240
4418
|
insertBlocker.run(id, projectSlug || null, title, severity, 'OPEN', null, null, metadata);
|
|
4241
4419
|
|
|
4242
4420
|
applied.blockers++;
|
|
4421
|
+
if (!applied._embedQueue) applied._embedQueue = [];
|
|
4422
|
+
applied._embedQueue.push({ type: 'blocker', id, text: title + ' ' + (notes || '') });
|
|
4243
4423
|
existingBlockerKeys24h.add(key); // prevent duplicates within same batch
|
|
4244
4424
|
continue;
|
|
4245
4425
|
}
|
|
@@ -4260,6 +4440,12 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
4260
4440
|
|
|
4261
4441
|
applyTx(actions);
|
|
4262
4442
|
|
|
4443
|
+
// Generate embeddings for newly created tasks/blockers (background, non-blocking)
|
|
4444
|
+
if (applied._embedQueue && applied._embedQueue.length > 0) {
|
|
4445
|
+
generateEmbeddingsBackground(workspaceDir, applied._embedQueue).catch(() => {});
|
|
4446
|
+
delete applied._embedQueue; // don't send internal queue in response
|
|
4447
|
+
}
|
|
4448
|
+
|
|
4263
4449
|
// Auto-suggest reports when planner didn't include any
|
|
4264
4450
|
if (!applied.reportsSuggested.length) {
|
|
4265
4451
|
const sug = [];
|
|
@@ -4403,23 +4589,8 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
4403
4589
|
// Ensure daily logs are synced to SQLite before querying
|
|
4404
4590
|
try { syncDailyLogs(workspaceDir); } catch { /* non-fatal */ }
|
|
4405
4591
|
|
|
4406
|
-
// Build
|
|
4407
|
-
const dataContext =
|
|
4408
|
-
|
|
4409
|
-
// V2 RAG Context (graceful fallback if embedder/sharp not available)
|
|
4410
|
-
const dm = new DataManager(workspaceDir, path.join(workspaceDir, 'logs'));
|
|
4411
|
-
let ragContext = '';
|
|
4412
|
-
try {
|
|
4413
|
-
const ragResults = await dm.semanticSearch(query, 12);
|
|
4414
|
-
if (ragResults.length > 0) {
|
|
4415
|
-
ragContext = '\n\n[MEMÓRIA DE LONGO PRAZO RECUPERADA (RAG VIA SQLITE)]\n';
|
|
4416
|
-
for (const r of ragResults) {
|
|
4417
|
-
ragContext += `\n---\nFONTE: ${r.reference_type} -> ID: ${r.reference_id} (Score: ${r.score.toFixed(3)})\nCONTEÚDO:\n${r.text_chunk}\n`;
|
|
4418
|
-
}
|
|
4419
|
-
}
|
|
4420
|
-
} catch (ragErr) {
|
|
4421
|
-
console.error('[oracle] RAG search failed (embedder/sharp unavailable), continuing without context:', ragErr.message);
|
|
4422
|
-
}
|
|
4592
|
+
// Build smart context: RAG (if embeddings exist) + fallback to raw logs + structured data
|
|
4593
|
+
const dataContext = await buildSmartContext(workspaceDir, query);
|
|
4423
4594
|
|
|
4424
4595
|
// Build image context for the prompt (Copilot reads files via --allow-all-tools)
|
|
4425
4596
|
let imageContext = '';
|
|
@@ -4446,7 +4617,7 @@ REGRAS ABSOLUTAS:
|
|
|
4446
4617
|
|
|
4447
4618
|
DADOS REAIS DO WORKSPACE (use estes dados para responder):
|
|
4448
4619
|
${dataContext}
|
|
4449
|
-
${
|
|
4620
|
+
${imageContext}`;
|
|
4450
4621
|
|
|
4451
4622
|
const cmd = process.env.COPILOT_CMD || 'copilot';
|
|
4452
4623
|
|
|
@@ -4483,7 +4654,13 @@ ${ragContext}${imageContext}`;
|
|
|
4483
4654
|
if (r.code !== 0) {
|
|
4484
4655
|
return safeJson(res, 200, { ok: false, answer: 'Falha no processamento do agente FREYA:\n' + (out || 'Exit code != 0'), sessionId });
|
|
4485
4656
|
}
|
|
4486
|
-
|
|
4657
|
+
// Send response immediately
|
|
4658
|
+
safeJson(res, 200, { ok: true, answer: out, sessionId });
|
|
4659
|
+
// Fire-and-forget: auto-ingest tasks/blockers from user message
|
|
4660
|
+
backgroundIngestFromChat(workspaceDir, query).catch(err => {
|
|
4661
|
+
console.error('[chat-ingest] Background failed:', err.message);
|
|
4662
|
+
});
|
|
4663
|
+
return;
|
|
4487
4664
|
} catch (e) {
|
|
4488
4665
|
return safeJson(res, 200, {
|
|
4489
4666
|
ok: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cccarv82/freya",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.1",
|
|
4
4
|
"description": "Personal AI Assistant with local-first persistence",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"health": "node scripts/validate-data.js && node scripts/validate-structure.js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"status": "node scripts/generate-executive-report.js",
|
|
11
11
|
"blockers": "node scripts/generate-blockers-report.js",
|
|
12
12
|
"export-obsidian": "node scripts/export-obsidian.js",
|
|
13
|
+
"retroactive-ingest": "node scripts/retroactive-ingest.js",
|
|
13
14
|
"build-index": "node scripts/index/build-index.js",
|
|
14
15
|
"update-index": "node scripts/index/update-index.js",
|
|
15
16
|
"test": "node tests/unit/test-package-config.js && node tests/unit/test-cli-init.js && node tests/unit/test-cli-web-help.js && node tests/unit/test-web-static-assets.js && node tests/unit/test-fs-utils.js && node tests/unit/test-search-utils.js && node tests/unit/test-index-utils.js && node tests/unit/test-daily-generation.js && node tests/unit/test-report-generation.js && node tests/unit/test-executive-report-logs.js && node tests/unit/test-oracle-retrieval.js && node tests/unit/test-task-completion.js && node tests/unit/test-migrate-data.js && node tests/unit/test-blockers-report.js && node tests/unit/test-sm-weekly-report.js && node tests/integration/test-ingestor-task.js && node tests/unit/test-structure-validation.js"
|
package/scripts/lib/DataLayer.js
CHANGED
|
@@ -325,6 +325,12 @@ class DataLayer {
|
|
|
325
325
|
embedding BLOB NOT NULL, /* Stored as Buffer of Float32Array */
|
|
326
326
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
327
327
|
);
|
|
328
|
+
|
|
329
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_doc_emb_ref
|
|
330
|
+
ON document_embeddings(reference_type, reference_id, chunk_index);
|
|
331
|
+
|
|
332
|
+
CREATE INDEX IF NOT EXISTS idx_doc_emb_type
|
|
333
|
+
ON document_embeddings(reference_type);
|
|
328
334
|
`);
|
|
329
335
|
|
|
330
336
|
// --- Migrations for existing databases ---
|
|
@@ -229,6 +229,95 @@ class DataManager {
|
|
|
229
229
|
return NaN;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
// --- Embedding Generation ---
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Split text into chunks suitable for embedding (~400-600 chars each).
|
|
236
|
+
* Splits on markdown headings, then paragraphs, then sentences.
|
|
237
|
+
*/
|
|
238
|
+
chunkText(text, maxChunkSize = 500) {
|
|
239
|
+
if (!text || text.length <= maxChunkSize) return [text].filter(Boolean);
|
|
240
|
+
|
|
241
|
+
const chunks = [];
|
|
242
|
+
// First split on markdown ## headings
|
|
243
|
+
const sections = text.split(/(?=^## )/m).filter(s => s.trim());
|
|
244
|
+
|
|
245
|
+
for (const section of sections) {
|
|
246
|
+
if (section.length <= maxChunkSize) {
|
|
247
|
+
chunks.push(section.trim());
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
// Split long sections on double newlines (paragraphs)
|
|
251
|
+
const paragraphs = section.split(/\n\n+/).filter(p => p.trim());
|
|
252
|
+
let buffer = '';
|
|
253
|
+
for (const para of paragraphs) {
|
|
254
|
+
if (buffer.length + para.length + 2 > maxChunkSize && buffer) {
|
|
255
|
+
chunks.push(buffer.trim());
|
|
256
|
+
buffer = '';
|
|
257
|
+
}
|
|
258
|
+
buffer += (buffer ? '\n\n' : '') + para;
|
|
259
|
+
}
|
|
260
|
+
if (buffer.trim()) chunks.push(buffer.trim());
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return chunks.filter(c => c.length > 10); // skip tiny fragments
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Generate embeddings for a piece of content and store in document_embeddings.
|
|
268
|
+
* Deletes existing embeddings for (referenceType, referenceId) first to avoid stale data.
|
|
269
|
+
* @param {string} referenceType - 'daily_log', 'task', or 'blocker'
|
|
270
|
+
* @param {string} referenceId - unique ID (date for logs, task/blocker id)
|
|
271
|
+
* @param {string} text - content to embed
|
|
272
|
+
*/
|
|
273
|
+
async generateEmbeddings(referenceType, referenceId, text) {
|
|
274
|
+
if (!text || !text.trim()) return 0;
|
|
275
|
+
|
|
276
|
+
const chunks = this.chunkText(text);
|
|
277
|
+
if (!chunks.length) return 0;
|
|
278
|
+
|
|
279
|
+
// Delete existing embeddings for this reference
|
|
280
|
+
dl.db.prepare('DELETE FROM document_embeddings WHERE reference_type = ? AND reference_id = ?')
|
|
281
|
+
.run(referenceType, referenceId);
|
|
282
|
+
|
|
283
|
+
const insert = dl.db.prepare(`
|
|
284
|
+
INSERT INTO document_embeddings (reference_type, reference_id, chunk_index, text_chunk, embedding)
|
|
285
|
+
VALUES (?, ?, ?, ?, ?)
|
|
286
|
+
`);
|
|
287
|
+
|
|
288
|
+
let count = 0;
|
|
289
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
290
|
+
try {
|
|
291
|
+
const vector = await defaultEmbedder.embedText(chunks[i]);
|
|
292
|
+
const buffer = defaultEmbedder.vectorToBuffer(vector);
|
|
293
|
+
insert.run(referenceType, referenceId, i, chunks[i], buffer);
|
|
294
|
+
count++;
|
|
295
|
+
} catch (err) {
|
|
296
|
+
console.error(`[embeddings] Failed to embed chunk ${i} of ${referenceType}/${referenceId}:`, err.message);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return count;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Check if embeddings exist and are up-to-date for a reference.
|
|
304
|
+
* @returns {boolean} true if embeddings exist
|
|
305
|
+
*/
|
|
306
|
+
hasEmbeddings(referenceType, referenceId) {
|
|
307
|
+
const row = dl.db.prepare(
|
|
308
|
+
'SELECT COUNT(*) as c FROM document_embeddings WHERE reference_type = ? AND reference_id = ?'
|
|
309
|
+
).get(referenceType, referenceId);
|
|
310
|
+
return row && row.c > 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get total embedding count (for checking if RAG is available).
|
|
315
|
+
*/
|
|
316
|
+
getEmbeddingCount() {
|
|
317
|
+
const row = dl.db.prepare('SELECT COUNT(*) as c FROM document_embeddings').get();
|
|
318
|
+
return row ? row.c : 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
232
321
|
// --- RAG (Vector Search) ---
|
|
233
322
|
async semanticSearch(query, topK = 10) {
|
|
234
323
|
const queryVector = await defaultEmbedder.embedText(query);
|