Initial commit: Inner OS v1
Full-stack inner development operating system built on Next.js 16 + Prisma 7 + PostgreSQL + OpenAI + Clerk. Generates personalized daily mental training tasks, collects reflection, updates user state, and provides internal reward feedback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
import OpenAI from "openai";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { keywordCheck, llmSafetyCheck, type SafetyResult } from "@/lib/safety";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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
|
||||
): Promise<GeneratedSummary> {
|
||||
// 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:
|
||||
"No completed practice sessions this week. Every week is a fresh start — your practice is here when you're ready.",
|
||||
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: "gpt-4o-mini",
|
||||
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: "Your practice summary for this week.", 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);
|
||||
|
||||
await prisma.weeklySummary.upsert({
|
||||
where: { userId },
|
||||
create: {
|
||||
userId,
|
||||
summary: llmSafety.safe ? summary : "Your practice summary for this week.",
|
||||
weekStart,
|
||||
},
|
||||
update: {
|
||||
summary: llmSafety.safe ? summary : "Your practice summary for this week.",
|
||||
weekStart,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
summary: llmSafety.safe ? summary : "Your practice summary for this week.",
|
||||
safety: llmSafety,
|
||||
};
|
||||
} catch {
|
||||
const fallback = `You completed ${sessions.length} practice session${sessions.length === 1 ? "" : "s"} this week. Showing up consistently is the foundation everything else is built on. Your current streak is ${userState.streakDays} day${userState.streakDays === 1 ? "" : "s"}. See you next week.`;
|
||||
return { summary: fallback, safety: { safe: true } };
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cron handler — generates summaries for all active users
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function generateAllWeeklySummaries(
|
||||
openai: OpenAI
|
||||
): 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);
|
||||
results.push({ userId, success: true });
|
||||
} catch {
|
||||
results.push({ userId, success: false });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
Reference in New Issue
Block a user