This commit is contained in:
2
apps/web/public/sw.js
Executable file → Normal file
2
apps/web/public/sw.js
Executable file → Normal file
File diff suppressed because one or more lines are too long
@@ -9,6 +9,7 @@ describe("formatContextForPrompt", () => {
|
||||
memoryIds: [],
|
||||
activeDay: { dayMode: "low_energy", isBackfilled: false },
|
||||
recentActivity: "recent",
|
||||
recentLogDetails: "",
|
||||
featureSlice: "",
|
||||
tokenEstimate: 100,
|
||||
layers: { memories: "" },
|
||||
@@ -24,10 +25,26 @@ describe("formatContextForPrompt", () => {
|
||||
memoryIds: ["1"],
|
||||
activeDay: { isBackfilled: true, dayMode: "normal" },
|
||||
recentActivity: "",
|
||||
recentLogDetails: "",
|
||||
featureSlice: "",
|
||||
tokenEstimate: 50,
|
||||
layers: { memories: "[likes] T: C" },
|
||||
});
|
||||
expect(text).toContain("backfilled");
|
||||
});
|
||||
|
||||
it("includes recent log details", () => {
|
||||
const text = formatContextForPrompt({
|
||||
profileSummary: "",
|
||||
memories: [],
|
||||
memoryIds: [],
|
||||
activeDay: { dayMode: "normal" },
|
||||
recentActivity: "",
|
||||
recentLogDetails: "2026-06-27: Items: Prayer: 3/5 checked (partial)",
|
||||
featureSlice: "",
|
||||
tokenEstimate: 50,
|
||||
layers: { memories: "" },
|
||||
});
|
||||
expect(text).toContain("Prayer: 3/5");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { format, subDays } from "date-fns";
|
||||
import type { MemoryCategory, DayMode } from "@adventureos/shared";
|
||||
import type { MemoryCategory } from "@adventureos/shared";
|
||||
import { MEMORY_CATEGORIES } from "@adventureos/shared";
|
||||
import { requireUser } from "./user";
|
||||
import { getDailyAdventure, buildDaySnapshot } from "./adventure";
|
||||
@@ -7,7 +7,7 @@ import { getReflection } from "./reflection";
|
||||
import { getProfileSummary, listMemories, markMemoriesUsed } from "./ai-memory";
|
||||
import { getLogicalToday } from "../dates";
|
||||
import { getDayBoundaryHour } from "./day-boundary";
|
||||
import { db, explorations, weeklyReviews, aiContextLogs } from "../db";
|
||||
import { db, explorations, weeklyReviews, aiContextLogs, readingLogs, books } from "../db";
|
||||
import { and, eq, desc } from "drizzle-orm";
|
||||
import { weekStartString } from "../dates";
|
||||
|
||||
@@ -43,6 +43,7 @@ export type BuiltContext = {
|
||||
memoryIds: string[];
|
||||
activeDay: Record<string, unknown> | null;
|
||||
recentActivity: string;
|
||||
recentLogDetails: string;
|
||||
featureSlice: string;
|
||||
tokenEstimate: number;
|
||||
layers: Record<string, string>;
|
||||
@@ -81,6 +82,162 @@ function categoriesFromMessage(message: string): Set<string> {
|
||||
return cats;
|
||||
}
|
||||
|
||||
function unique<T>(items: T[]): T[] {
|
||||
return [...new Set(items)];
|
||||
}
|
||||
|
||||
function contextDates(activeDate: string, message: string): string[] {
|
||||
const lower = message.toLowerCase();
|
||||
const dates = [activeDate];
|
||||
|
||||
for (let i = 1; i < 7; i++) {
|
||||
dates.push(format(subDays(new Date(activeDate), i), "yyyy-MM-dd"));
|
||||
}
|
||||
|
||||
if (lower.includes("today")) dates.unshift(activeDate);
|
||||
if (lower.includes("yesterday")) {
|
||||
dates.unshift(format(subDays(new Date(activeDate), 1), "yyyy-MM-dd"));
|
||||
}
|
||||
|
||||
const explicitDates = message.match(/\b\d{4}-\d{2}-\d{2}\b/g) ?? [];
|
||||
dates.unshift(...explicitDates);
|
||||
|
||||
return unique(dates).slice(0, 10);
|
||||
}
|
||||
|
||||
function summarizeItemValue(item: {
|
||||
type: string;
|
||||
label: string;
|
||||
state: string;
|
||||
value: Record<string, unknown>;
|
||||
}): string | null {
|
||||
if (item.type === "checklist") {
|
||||
const checks = (item.value?.checks as boolean[]) ?? [];
|
||||
const checked = checks.filter(Boolean).length;
|
||||
return `${checked}/${checks.length} checked (${item.state})`;
|
||||
}
|
||||
|
||||
if (item.type === "duration") {
|
||||
const hours = Number(item.value?.hours ?? 0);
|
||||
return `${hours.toFixed(1)}h (${item.state})`;
|
||||
}
|
||||
|
||||
if (item.type === "reading") {
|
||||
const pages = Number(item.value?.pages ?? 0);
|
||||
return pages > 0 ? `${pages} pages (${item.state})` : `0 pages (${item.state})`;
|
||||
}
|
||||
|
||||
if (item.type === "note") {
|
||||
const note = String(item.value?.note ?? "").trim();
|
||||
return note ? truncate(note, 220) : null;
|
||||
}
|
||||
|
||||
if (item.type === "checkbox" || item.type === "timeblock") {
|
||||
return item.state === "done" ? "done" : "not done";
|
||||
}
|
||||
|
||||
return Object.keys(item.value ?? {}).length > 0 ? JSON.stringify(item.value) : item.state;
|
||||
}
|
||||
|
||||
function shouldIncludeItem(item: {
|
||||
type: string;
|
||||
state: string;
|
||||
value: Record<string, unknown>;
|
||||
}): boolean {
|
||||
if (item.type === "checklist") return true;
|
||||
if (item.type === "note") return Boolean(String(item.value?.note ?? "").trim());
|
||||
return item.state !== "blank" || Object.keys(item.value ?? {}).length > 0;
|
||||
}
|
||||
|
||||
export async function buildRecentLogDetails(
|
||||
userId: string,
|
||||
activeDate: string,
|
||||
message = ""
|
||||
): Promise<string> {
|
||||
const lines: string[] = [];
|
||||
|
||||
for (const date of contextDates(activeDate, message)) {
|
||||
const { adventure, items, todos } = await getDailyAdventure(userId, date);
|
||||
const reflection = await getReflection(userId, date);
|
||||
const dayLines: string[] = [];
|
||||
|
||||
const itemLines = items
|
||||
.filter((i) => i.enabled && shouldIncludeItem({
|
||||
type: i.type,
|
||||
state: i.state,
|
||||
value: i.value as Record<string, unknown>,
|
||||
}))
|
||||
.map((i) => {
|
||||
const summary = summarizeItemValue({
|
||||
type: i.type,
|
||||
label: i.label,
|
||||
state: i.state,
|
||||
value: i.value as Record<string, unknown>,
|
||||
});
|
||||
return summary ? `${i.label}: ${summary}` : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (itemLines.length > 0) dayLines.push(`Items: ${itemLines.join("; ")}`);
|
||||
|
||||
if (todos.length > 0) {
|
||||
const done = todos.filter((t) => t.done).map((t) => t.label);
|
||||
const open = todos.filter((t) => !t.done).map((t) => t.label);
|
||||
dayLines.push(
|
||||
`Todos: ${done.length}/${todos.length} done` +
|
||||
(done.length ? `; done: ${done.join(", ")}` : "") +
|
||||
(open.length ? `; open: ${open.join(", ")}` : "")
|
||||
);
|
||||
}
|
||||
|
||||
if (reflection) {
|
||||
const reflectionParts = [
|
||||
reflection.wentWell ? `went well: ${truncate(reflection.wentWell, 180)}` : "",
|
||||
reflection.learned ? `learned: ${truncate(reflection.learned, 180)}` : "",
|
||||
reflection.improveTomorrow
|
||||
? `improve tomorrow: ${truncate(reflection.improveTomorrow, 180)}`
|
||||
: "",
|
||||
].filter(Boolean);
|
||||
if (reflectionParts.length > 0) dayLines.push(`Reflection: ${reflectionParts.join("; ")}`);
|
||||
}
|
||||
|
||||
const reading = await db
|
||||
.select({
|
||||
title: books.title,
|
||||
author: books.author,
|
||||
pagesRead: readingLogs.pagesRead,
|
||||
note: readingLogs.note,
|
||||
})
|
||||
.from(readingLogs)
|
||||
.innerJoin(books, eq(readingLogs.bookId, books.id))
|
||||
.where(and(eq(books.userId, userId), eq(readingLogs.date, date)));
|
||||
|
||||
if (reading.length > 0) {
|
||||
dayLines.push(
|
||||
`Reading logs: ${reading
|
||||
.map(
|
||||
(r) =>
|
||||
`${r.title}${r.author ? ` by ${r.author}` : ""}: ${r.pagesRead}p` +
|
||||
(r.note ? `, note: ${truncate(r.note, 140)}` : "")
|
||||
)
|
||||
.join("; ")}`
|
||||
);
|
||||
}
|
||||
|
||||
if (dayLines.length > 0 || adventure.isRestDay || adventure.isBackfilled) {
|
||||
const flags = [
|
||||
adventure.dayMode && adventure.dayMode !== "normal" ? `mode=${adventure.dayMode}` : "",
|
||||
adventure.isRestDay ? "rest day" : "",
|
||||
adventure.isBackfilled ? "backfilled" : "",
|
||||
adventure.loggedAt ? `logged=${adventure.loggedAt.toISOString()}` : "",
|
||||
].filter(Boolean);
|
||||
lines.push(`${date}${flags.length ? ` (${flags.join(", ")})` : ""}: ${dayLines.join(" | ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
return truncate(lines.join("\n"), 3500);
|
||||
}
|
||||
|
||||
export async function buildMentorContext(
|
||||
userId: string,
|
||||
opts: MentorContextOptions = {}
|
||||
@@ -112,14 +269,17 @@ export async function buildMentorContext(
|
||||
|
||||
let activeDay: Record<string, unknown> | null = null;
|
||||
try {
|
||||
const { adventure, items } = await getDailyAdventure(userId, activeDate);
|
||||
const { adventure, items, todos } = await getDailyAdventure(userId, activeDate);
|
||||
activeDay = {
|
||||
date: activeDate,
|
||||
dayMode: adventure.dayMode ?? "normal",
|
||||
isRestDay: adventure.isRestDay,
|
||||
isBackfilled: adventure.isBackfilled,
|
||||
loggedAt: adventure.loggedAt?.toISOString() ?? null,
|
||||
itemsLogged: items.filter((i) => i.state !== "blank").length,
|
||||
itemCount: items.filter((i) => i.enabled && i.type !== "note").length,
|
||||
todosDone: todos.filter((t) => t.done).length,
|
||||
todoCount: todos.length,
|
||||
};
|
||||
} catch {
|
||||
activeDay = { date: activeDate };
|
||||
@@ -136,6 +296,7 @@ export async function buildMentorContext(
|
||||
);
|
||||
}
|
||||
const recentActivity = truncate(digestParts.join("\n"), 800);
|
||||
const recentLogDetails = await buildRecentLogDetails(userId, activeDate, message);
|
||||
|
||||
let featureSlice = "";
|
||||
if (opts.feature === "teacher" && opts.topic) {
|
||||
@@ -154,6 +315,7 @@ export async function buildMentorContext(
|
||||
memories: memories.map((m) => `[${m.category}] ${m.title}: ${m.content}`).join("\n"),
|
||||
activeDay: JSON.stringify(activeDay),
|
||||
recentActivity,
|
||||
recentLogDetails,
|
||||
featureSlice,
|
||||
};
|
||||
|
||||
@@ -179,6 +341,7 @@ export async function buildMentorContext(
|
||||
memoryIds,
|
||||
activeDay,
|
||||
recentActivity,
|
||||
recentLogDetails,
|
||||
featureSlice,
|
||||
tokenEstimate,
|
||||
layers,
|
||||
@@ -265,6 +428,7 @@ export function formatContextForPrompt(ctx: BuiltContext): string {
|
||||
`Today: ${ctx.layers.activeDay}`,
|
||||
dayNote,
|
||||
`Recent:\n${ctx.recentActivity}`,
|
||||
ctx.recentLogDetails ? `Recent log details:\n${ctx.recentLogDetails}` : "",
|
||||
ctx.featureSlice,
|
||||
]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -526,6 +526,7 @@ export async function generateChatReply(
|
||||
`User: ${input.userMessage}`,
|
||||
input.memoryAction ? `Application memoryAction result: ${input.memoryAction}` : "",
|
||||
"Memory guardrail: You must not claim that a memory has been saved, added, approved, rejected, deleted, or changed unless the application memoryAction result confirms it. If no memoryAction result confirms persistence, do not imply that memory persistence happened.",
|
||||
"Log access: If the user asks about notes, prayers, todos, reading, reflections, or other logged activity, answer from the Personal context records. If a requested date or log entry is not present in context, say that you do not have that record in the current context instead of guessing.",
|
||||
"If the user asks whether you can learn about them, explain that AdventureOS can suggest memories from goals, preferences, routines, worries, and direct remember requests; the user can review, edit, accept, reject, or ignore suggestions before they become saved memory.",
|
||||
"Respond as the mentor in plain text (no JSON). Keep it concise for a local model.",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user