learning_ai_notes/backend/src/modules/palace/wakeup.ts

176 lines
5.0 KiB
TypeScript

/**
* Wake-up context builder for NoteLett palace.
*
* Builds L0/L1/L2 layered context within a configurable token budget.
* Uses @bytelyst/palace shared wakeup builder for assembly + truncation.
*/
import { buildWakeUpLayers, WAKEUP_PRESETS, estimateTokens, truncateToTokenBudget } from '@bytelyst/palace';
import type { WakeUpContext } from '@bytelyst/palace';
import { config } from '../../lib/config.js';
import { embedText, stripHtmlForEmbedding } from '../../lib/embeddings.js';
import { getWing, listRooms, listMemories, searchSemantic, getWingSummary } from './repository.js';
import type { PalaceMemoryDoc, PalaceWingDoc } from './types.js';
export type { WakeUpContext };
export interface NoteLettWakeUpContext extends WakeUpContext {
wingId: string;
wingName: string;
}
const NOTELETT_PRESET = WAKEUP_PRESETS.notelett ?? {
totalBudget: 600, l0Budget: 50, l1Budget: 150, l2Budget: 400,
};
/**
* Build wake-up context for a workspace/wing.
*
* L0 — Identity: workspace name + room topics (~50 tokens)
* L1 — Critical Facts: recent decisions/preferences/insights (~150 tokens)
* L2 — Task Context: semantically relevant memories (~400 tokens)
*/
export async function buildNoteLettWakeUp(
userId: string,
productId: string,
wingId: string,
taskDescription?: string,
): Promise<NoteLettWakeUpContext> {
const wing = await getWing(userId, productId, wingId);
if (!wing) {
return {
text: '',
layers: [],
totalChars: 0,
truncated: false,
wingId,
wingName: 'Unknown',
};
}
const budget = {
...NOTELETT_PRESET,
totalBudget: config.PALACE_WAKE_UP_BUDGET,
};
const l0 = await buildL0Identity(userId, productId, wing);
const l1 = await buildL1CriticalFacts(userId, productId, wingId, wing);
const l2 = await buildL2TaskContext(userId, productId, wingId, taskDescription);
const ctx = buildWakeUpLayers(l0, l1, l2, budget);
return {
...ctx,
wingId,
wingName: wing.name,
};
}
/**
* L0 — Identity: workspace name, description, room topics.
*/
async function buildL0Identity(
userId: string,
productId: string,
wing: PalaceWingDoc,
): Promise<string> {
const rooms = await listRooms(userId, productId, wing.id);
const roomNames = rooms.map(r => r.name).slice(0, 10).join(', ');
const parts = [`Workspace: ${wing.name}`];
if (wing.description) parts.push(wing.description);
if (roomNames) parts.push(`Topics: ${roomNames}`);
return parts.join('\n');
}
/**
* L1 — Critical Facts: recent decisions, preferences, insights.
* Uses cached L1 if available, otherwise regenerates.
*/
async function buildL1CriticalFacts(
userId: string,
productId: string,
wingId: string,
wing: PalaceWingDoc,
): Promise<string> {
// Try cached L1
if (wing.l1Cache && wing.l1CacheUpdatedAt) {
const cacheAge = Date.now() - new Date(wing.l1CacheUpdatedAt).getTime();
if (cacheAge < 24 * 3600_000) return wing.l1Cache;
}
// Rebuild from recent decisions + preferences + insights
const factHalls = ['decisions', 'preferences', 'insights'] as const;
const recentFacts: PalaceMemoryDoc[] = [];
for (const hall of factHalls) {
const mems = await listMemories(userId, productId, {
wingId,
hall,
limit: 5,
});
recentFacts.push(...mems);
}
if (recentFacts.length === 0) return '';
// Sort by recency
recentFacts.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
const lines = recentFacts.slice(0, 8).map(m => `- [${m.hall}] ${m.content}`);
return lines.join('\n');
}
/**
* L2 — Task Context: semantically relevant memories (or recent if no task).
*/
async function buildL2TaskContext(
userId: string,
productId: string,
wingId: string,
taskDescription?: string,
): Promise<string> {
let memories: PalaceMemoryDoc[];
if (taskDescription && taskDescription.trim().length > 0) {
const embedding = await embedText(taskDescription);
if (embedding) {
memories = await searchSemantic(userId, productId, taskDescription, embedding, wingId, 8);
} else {
memories = await listMemories(userId, productId, { wingId, limit: 8 });
}
} else {
memories = await listMemories(userId, productId, { wingId, limit: 8 });
}
if (memories.length === 0) return '';
const lines = memories.map(m => `- [${m.hall}] ${m.content}`);
return lines.join('\n');
}
/**
* Regenerate L1 cache for a wing (called after memory extraction).
*/
export async function regenerateCriticalFacts(
userId: string,
productId: string,
wingId: string,
): Promise<string> {
const wing = await getWing(userId, productId, wingId);
if (!wing) return '';
const l1 = await buildL1CriticalFacts(userId, productId, wingId, wing);
// Update wing doc with cached L1
const { getCollection } = await import('../../lib/datastore.js');
const col = getCollection<PalaceWingDoc>('palace_wings', '/userId');
await col.update(wingId, userId, {
l1Cache: l1,
l1CacheUpdatedAt: new Date().toISOString(),
} as Partial<PalaceWingDoc>);
return l1;
}