89
apps/web/src/lib/services/memory-extraction.ts
Normal file
89
apps/web/src/lib/services/memory-extraction.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { z } from "zod";
|
||||
import type { MemoryCategory, MemorySourceType } from "@adventureos/shared";
|
||||
import { SENSITIVE_MEMORY_CATEGORIES } from "@adventureos/shared";
|
||||
import { parseAiJson } from "@/lib/ai/parse-json";
|
||||
import { getProvider } from "@/lib/ai/provider-registry";
|
||||
import { getAiBehaviorConfig, getAiProviderConfig, buildSystemPrompt } from "./ai-config";
|
||||
import { createSuggestion, getMemoryLearningSettings } from "./ai-memory";
|
||||
|
||||
const candidateSchema = z.object({
|
||||
candidates: z
|
||||
.array(
|
||||
z.object({
|
||||
category: z.string(),
|
||||
title: z.string(),
|
||||
content: z.string(),
|
||||
confidence: z.coerce.number(),
|
||||
sensitivity: z.enum(["normal", "private", "sensitive"]).optional(),
|
||||
})
|
||||
)
|
||||
.max(2),
|
||||
});
|
||||
|
||||
export async function extractMemoryCandidates(
|
||||
userId: string,
|
||||
sourceType: MemorySourceType,
|
||||
sourceText: string,
|
||||
sourceRef?: { type: string; id?: string; date?: string }
|
||||
) {
|
||||
const settings = await getMemoryLearningSettings(userId);
|
||||
if (!settings.learningEnabled || !settings.autoSuggestEnabled) return [];
|
||||
|
||||
const behavior = await getAiBehaviorConfig(userId);
|
||||
const providerConfig = await getAiProviderConfig(userId);
|
||||
if (!behavior.enabled || !providerConfig.enabled) return [];
|
||||
|
||||
const prompt = `From this user text, suggest 0-2 personal memory facts the app could remember (only clear patterns, not guesses).
|
||||
User text:
|
||||
"""
|
||||
${sourceText.slice(0, 1500)}
|
||||
"""
|
||||
Return JSON: { "candidates": [{ "category": "likes|motivators|current_goals|reading_preferences|learning_interests|worries|...", "title": "short label", "content": "one sentence fact", "confidence": 0.0-1.0, "sensitivity": "normal|private|sensitive" }] }
|
||||
If nothing clear, return { "candidates": [] }.`;
|
||||
|
||||
try {
|
||||
const coreSystem = await getTemplateBody(userId);
|
||||
const provider = getProvider(providerConfig.type);
|
||||
const res = await provider.generateText(providerConfig, {
|
||||
model: providerConfig.model,
|
||||
prompt,
|
||||
system: buildSystemPrompt(behavior, coreSystem),
|
||||
format: "json",
|
||||
temperature: 0.3,
|
||||
maxTokens: 400,
|
||||
timeoutMs: providerConfig.timeoutMs,
|
||||
});
|
||||
|
||||
const parsed = candidateSchema.safeParse(parseAiJson(res.text));
|
||||
if (!parsed.success) return [];
|
||||
|
||||
const results = [];
|
||||
for (const c of parsed.data.candidates) {
|
||||
if (c.confidence < 0.6) continue;
|
||||
if (!settings.allowedCategories.includes(c.category as MemoryCategory)) continue;
|
||||
if (
|
||||
SENSITIVE_MEMORY_CATEGORIES.includes(c.category as MemoryCategory) &&
|
||||
!settings.allowSensitiveCategories
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const row = await createSuggestion(userId, {
|
||||
category: c.category as MemoryCategory,
|
||||
title: c.title,
|
||||
content: c.content,
|
||||
sourceType,
|
||||
sourceRef,
|
||||
confidence: c.confidence,
|
||||
});
|
||||
if (row) results.push(row);
|
||||
}
|
||||
return results;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function getTemplateBody(userId: string) {
|
||||
const { getTemplateBody: getTpl } = await import("./ai-templates");
|
||||
return getTpl(userId, "system_core");
|
||||
}
|
||||
Reference in New Issue
Block a user