import OpenAI from "openai"; import { prisma } from "@/lib/prisma"; import { keywordCheck, llmSafetyCheck, type SafetyResult } from "@/lib/safety"; import type { Lang } from "@/lib/i18n/types"; import { createServerT } from "@/lib/i18n/utils"; import zh from "../../../dictionaries/zh"; import en from "../../../dictionaries/en"; const DICTS = { zh, en } as const; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- interface WeekData { sessions: { taskType: string; taskDescription: string; focusScore: number | null; emotionScore: number | null; notes: string | null; durationSeconds: number; createdAt: Date; }[]; state: { attentionLevel: number; emotionStability: number; stressLevel: number; practiceStage: number; consistencyScore: number; streakDays: number; totalSessionsCompleted: number; }; } interface GeneratedSummary { summary: string; safety: SafetyResult; } // --------------------------------------------------------------------------- // LLM summary generation (LLM #3) // --------------------------------------------------------------------------- const SUMMARY_PROMPT = `You are a wise, warm inner development coach writing a weekly practice summary. Your tone is grounded, encouraging, and specific — never generic or saccharine. You will receive data about the user's practice this week: sessions completed, task types, focus/emotion scores, reflections, and their current state metrics (attention, emotional stability, stress, consistency, streak, stage). Write a 3-4 paragraph summary that: 1. Opens by acknowledging their commitment this week — be specific about what they practiced and how many sessions 2. Highlights patterns you notice: which types of practice led to better focus? Did their emotional awareness deepen? Were there difficult days they pushed through? 3. Notes changes in their state metrics from what you can infer — are they trending in a good direction? 4. Closes with gentle encouragement for the week ahead, grounded in what they've actually demonstrated (not empty motivation) Voice rules: - Be specific. Reference actual task types and patterns. "Your two attention training sessions showed stronger focus than your stress regulation work" is good. "Great job this week!" is not. - Be honest. If it was a light week with only 1-2 sessions, acknowledge that without judgment. - Never diagnose, never claim medical benefits, never use self-help clichés. - Write in second person ("you"). - Keep it to 3-4 paragraphs, 200-300 words total.`; export async function generateWeeklySummary( userId: string, openai: OpenAI, locale: Lang = "zh" ): Promise { const t = createServerT(DICTS[locale]); // Gather week data const weekAgo = new Date(); weekAgo.setDate(weekAgo.getDate() - 7); const [sessions, userState] = await Promise.all([ prisma.session.findMany({ where: { userId, createdAt: { gte: weekAgo }, completed: true, }, orderBy: { createdAt: "asc" }, select: { taskType: true, taskDescription: true, focusScore: true, emotionScore: true, notes: true, durationSeconds: true, createdAt: true, }, }), prisma.userState.findUnique({ where: { userId } }), ]); if (!userState || sessions.length === 0) { return { summary: t("engine.summary.no_sessions"), safety: { safe: true }, }; } const weekData: WeekData = { sessions, state: { attentionLevel: userState.attentionLevel, emotionStability: userState.emotionStability, stressLevel: userState.stressLevel, practiceStage: userState.practiceStage, consistencyScore: userState.consistencyScore, streakDays: userState.streakDays, totalSessionsCompleted: userState.totalSessionsCompleted, }, }; try { const response = await openai.chat.completions.create({ model: "deepseek-v4-pro", messages: [ { role: "system", content: SUMMARY_PROMPT }, { role: "user", content: JSON.stringify(weekData), }, ], temperature: 0.7, max_tokens: 500, }); const summary = response.choices[0]?.message?.content?.trim(); if (!summary || summary.length < 50) throw new Error("Summary too short"); // Safety check const keywordResult = keywordCheck(summary); if (!keywordResult.safe) { return { summary: t("engine.summary.safety_fallback"), safety: keywordResult }; } const llmSafety = await llmSafetyCheck(summary, openai as any); // Persist the summary (upsert — one per user per week) const weekStart = new Date(weekAgo); weekStart.setHours(0, 0, 0, 0); const safetyFallback = t("engine.summary.safety_fallback"); await prisma.weeklySummary.upsert({ where: { userId }, create: { userId, summary: llmSafety.safe ? summary : safetyFallback, weekStart, }, update: { summary: llmSafety.safe ? summary : safetyFallback, weekStart, }, }); return { summary: llmSafety.safe ? summary : safetyFallback, safety: llmSafety, }; } catch { const count = sessions.length; const streak = userState.streakDays; const fallback = t("engine.summary.fallback_template", { count, plural: count === 1 ? "" : "s", streak, streak_plural: streak === 1 ? "" : "s", }); return { summary: fallback, safety: { safe: true } }; } } // --------------------------------------------------------------------------- // Cron handler — generates summaries for all active users // --------------------------------------------------------------------------- export async function generateAllWeeklySummaries( openai: OpenAI, locale: Lang = "zh" ): Promise<{ userId: string; success: boolean }[]> { // Find users who had sessions in the last 7 days const weekAgo = new Date(); weekAgo.setDate(weekAgo.getDate() - 7); const activeUsers = await prisma.session.findMany({ where: { createdAt: { gte: weekAgo }, completed: true, }, select: { userId: true }, distinct: ["userId"], }); const results: { userId: string; success: boolean }[] = []; for (const { userId } of activeUsers) { try { await generateWeeklySummary(userId, openai, locale); results.push({ userId, success: true }); } catch { results.push({ userId, success: false }); } } return results; }