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: [],
|
memoryIds: [],
|
||||||
activeDay: { dayMode: "low_energy", isBackfilled: false },
|
activeDay: { dayMode: "low_energy", isBackfilled: false },
|
||||||
recentActivity: "recent",
|
recentActivity: "recent",
|
||||||
|
recentLogDetails: "",
|
||||||
featureSlice: "",
|
featureSlice: "",
|
||||||
tokenEstimate: 100,
|
tokenEstimate: 100,
|
||||||
layers: { memories: "" },
|
layers: { memories: "" },
|
||||||
@@ -24,10 +25,26 @@ describe("formatContextForPrompt", () => {
|
|||||||
memoryIds: ["1"],
|
memoryIds: ["1"],
|
||||||
activeDay: { isBackfilled: true, dayMode: "normal" },
|
activeDay: { isBackfilled: true, dayMode: "normal" },
|
||||||
recentActivity: "",
|
recentActivity: "",
|
||||||
|
recentLogDetails: "",
|
||||||
featureSlice: "",
|
featureSlice: "",
|
||||||
tokenEstimate: 50,
|
tokenEstimate: 50,
|
||||||
layers: { memories: "[likes] T: C" },
|
layers: { memories: "[likes] T: C" },
|
||||||
});
|
});
|
||||||
expect(text).toContain("backfilled");
|
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 { 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 { MEMORY_CATEGORIES } from "@adventureos/shared";
|
||||||
import { requireUser } from "./user";
|
import { requireUser } from "./user";
|
||||||
import { getDailyAdventure, buildDaySnapshot } from "./adventure";
|
import { getDailyAdventure, buildDaySnapshot } from "./adventure";
|
||||||
@@ -7,7 +7,7 @@ import { getReflection } from "./reflection";
|
|||||||
import { getProfileSummary, listMemories, markMemoriesUsed } from "./ai-memory";
|
import { getProfileSummary, listMemories, markMemoriesUsed } from "./ai-memory";
|
||||||
import { getLogicalToday } from "../dates";
|
import { getLogicalToday } from "../dates";
|
||||||
import { getDayBoundaryHour } from "./day-boundary";
|
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 { and, eq, desc } from "drizzle-orm";
|
||||||
import { weekStartString } from "../dates";
|
import { weekStartString } from "../dates";
|
||||||
|
|
||||||
@@ -43,6 +43,7 @@ export type BuiltContext = {
|
|||||||
memoryIds: string[];
|
memoryIds: string[];
|
||||||
activeDay: Record<string, unknown> | null;
|
activeDay: Record<string, unknown> | null;
|
||||||
recentActivity: string;
|
recentActivity: string;
|
||||||
|
recentLogDetails: string;
|
||||||
featureSlice: string;
|
featureSlice: string;
|
||||||
tokenEstimate: number;
|
tokenEstimate: number;
|
||||||
layers: Record<string, string>;
|
layers: Record<string, string>;
|
||||||
@@ -81,6 +82,162 @@ function categoriesFromMessage(message: string): Set<string> {
|
|||||||
return cats;
|
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(
|
export async function buildMentorContext(
|
||||||
userId: string,
|
userId: string,
|
||||||
opts: MentorContextOptions = {}
|
opts: MentorContextOptions = {}
|
||||||
@@ -112,14 +269,17 @@ export async function buildMentorContext(
|
|||||||
|
|
||||||
let activeDay: Record<string, unknown> | null = null;
|
let activeDay: Record<string, unknown> | null = null;
|
||||||
try {
|
try {
|
||||||
const { adventure, items } = await getDailyAdventure(userId, activeDate);
|
const { adventure, items, todos } = await getDailyAdventure(userId, activeDate);
|
||||||
activeDay = {
|
activeDay = {
|
||||||
date: activeDate,
|
date: activeDate,
|
||||||
dayMode: adventure.dayMode ?? "normal",
|
dayMode: adventure.dayMode ?? "normal",
|
||||||
isRestDay: adventure.isRestDay,
|
isRestDay: adventure.isRestDay,
|
||||||
isBackfilled: adventure.isBackfilled,
|
isBackfilled: adventure.isBackfilled,
|
||||||
|
loggedAt: adventure.loggedAt?.toISOString() ?? null,
|
||||||
itemsLogged: items.filter((i) => i.state !== "blank").length,
|
itemsLogged: items.filter((i) => i.state !== "blank").length,
|
||||||
itemCount: items.filter((i) => i.enabled && i.type !== "note").length,
|
itemCount: items.filter((i) => i.enabled && i.type !== "note").length,
|
||||||
|
todosDone: todos.filter((t) => t.done).length,
|
||||||
|
todoCount: todos.length,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
activeDay = { date: activeDate };
|
activeDay = { date: activeDate };
|
||||||
@@ -136,6 +296,7 @@ export async function buildMentorContext(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const recentActivity = truncate(digestParts.join("\n"), 800);
|
const recentActivity = truncate(digestParts.join("\n"), 800);
|
||||||
|
const recentLogDetails = await buildRecentLogDetails(userId, activeDate, message);
|
||||||
|
|
||||||
let featureSlice = "";
|
let featureSlice = "";
|
||||||
if (opts.feature === "teacher" && opts.topic) {
|
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"),
|
memories: memories.map((m) => `[${m.category}] ${m.title}: ${m.content}`).join("\n"),
|
||||||
activeDay: JSON.stringify(activeDay),
|
activeDay: JSON.stringify(activeDay),
|
||||||
recentActivity,
|
recentActivity,
|
||||||
|
recentLogDetails,
|
||||||
featureSlice,
|
featureSlice,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -179,6 +341,7 @@ export async function buildMentorContext(
|
|||||||
memoryIds,
|
memoryIds,
|
||||||
activeDay,
|
activeDay,
|
||||||
recentActivity,
|
recentActivity,
|
||||||
|
recentLogDetails,
|
||||||
featureSlice,
|
featureSlice,
|
||||||
tokenEstimate,
|
tokenEstimate,
|
||||||
layers,
|
layers,
|
||||||
@@ -265,6 +428,7 @@ export function formatContextForPrompt(ctx: BuiltContext): string {
|
|||||||
`Today: ${ctx.layers.activeDay}`,
|
`Today: ${ctx.layers.activeDay}`,
|
||||||
dayNote,
|
dayNote,
|
||||||
`Recent:\n${ctx.recentActivity}`,
|
`Recent:\n${ctx.recentActivity}`,
|
||||||
|
ctx.recentLogDetails ? `Recent log details:\n${ctx.recentLogDetails}` : "",
|
||||||
ctx.featureSlice,
|
ctx.featureSlice,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|||||||
@@ -526,6 +526,7 @@ export async function generateChatReply(
|
|||||||
`User: ${input.userMessage}`,
|
`User: ${input.userMessage}`,
|
||||||
input.memoryAction ? `Application memoryAction result: ${input.memoryAction}` : "",
|
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.",
|
"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.",
|
"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.",
|
"Respond as the mentor in plain text (no JSON). Keep it concise for a local model.",
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user