Make curator chats live generated

This commit is contained in:
Sai-karthik
2026-06-14 20:10:41 +00:00
parent 60b1df6892
commit 4b23dd0905
4 changed files with 82 additions and 20 deletions

View File

@@ -58,6 +58,19 @@ export const curatorActor = {
throw new Error("curator_task_has_no_handoff");
},
async completeTask(input: { userId: string; taskId: string; date?: string; reason?: string }) {
const date = input.date ?? todayIsoDate();
const task = (await buildCuratorTasks(input.userId, date)).find((item) => item.id === input.taskId);
if (!task) throw new Error("curator_task_not_found");
const event = await emitCuratorEvent({
userId: input.userId,
type: "curator.task.completed",
mission: { missionId: task.missionId, missionInstanceId: task.missionInstanceId, stageId: task.stageId },
payload: { taskId: task.id, date, reason: input.reason ?? "subtasks_completed" },
});
return { task: { ...task, status: "completed" as const }, eventId: event.id };
},
async recordServiceImpact(input: { userId: string; eventId: string }) {
const streak = await buildCuratorStreak(input.userId);
return { matched: true, completedTasks: await buildCuratorTasks(input.userId, todayIsoDate()), streak };

View File

@@ -1,4 +1,4 @@
import { generateObject } from "ai";
import { generateText } from "ai";
import { z } from "zod";
import { addMessagePg, createConversationPg, ensureCuratorTaskConversationPg, getConversationMetadataPg, listMessagesPg } from "../../grow/persistence.js";
import { generateConversationResponse, getConversationModel } from "../../actors/conversation/agent.js";
@@ -13,6 +13,17 @@ const chatExtractSchema = z.object({
shouldPrepareHandoff: z.boolean().default(false),
});
function parseJsonObject(text: string) {
const trimmed = text.trim();
try {
return JSON.parse(trimmed);
} catch {
const match = trimmed.match(/\{[\s\S]*\}/);
if (!match) throw new Error("model_did_not_return_json");
return JSON.parse(match[0]);
}
}
function buildId(prefix: string) {
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
@@ -209,6 +220,17 @@ function curatorTaskKey(taskId?: string, subtaskIndex?: number) {
return `${taskId}:${subtaskIndex ?? "task"}`;
}
function firstTurnPrompt(input: {
subtask?: string;
task?: Awaited<ReturnType<typeof buildCuratorTasks>>[number];
}) {
return [
`The user opened this focused subtask: ${input.subtask ?? input.task?.title ?? "curator task"}.`,
"Generate the first live conversational question for this exact subtask.",
"Ask only one question. Do not use fallback wording. Do not prepare any service handoff yet.",
].join("\n");
}
async function ensureCuratorConversation(input: { userId: string; taskId?: string; date: string; subtaskIndex?: number; subtask?: string }) {
if (!input.taskId) return createConversationPg(input.userId, "V1 Curator chat");
const task = (await buildCuratorTasks(input.userId, input.date)).find((item) => item.id === input.taskId);
@@ -267,23 +289,40 @@ export async function runCuratorChat(input: {
let reply = "";
try {
const extract = await generateObject({
model: getConversationModel(),
schema: chatExtractSchema,
system: "Extract compact curator memory from the user's latest message. Use ASCII punctuation only.",
prompt: `Task: ${task?.title ?? "General curator chat"}\nSubtask: ${input.subtask ?? "none"}\nService: ${task?.serviceName ?? "none"}\nMessage: ${latest}`,
});
await emitCuratorEvent({
userId: input.userId,
type: "curator.chat.context_extracted",
mission: task ? { missionId: task.missionId, missionInstanceId: task.missionInstanceId, stageId: task.stageId } : undefined,
payload: { taskId: input.taskId, extract: extract.object },
});
try {
const extract = await generateText({
model: getConversationModel(),
system: [
"Extract compact curator memory from the user's latest message.",
"Return JSON only: {\"summary\": string, \"userGoal\"?: string, \"serviceIntent\"?: string, \"shouldPrepareHandoff\": boolean}.",
"Use ASCII punctuation only.",
].join("\n"),
prompt: `Task: ${task?.title ?? "General curator chat"}\nSubtask: ${input.subtask ?? "none"}\nService: ${task?.serviceName ?? "none"}\nMessage: ${latest}`,
});
const parsedExtract = chatExtractSchema.parse(parseJsonObject(extract.text));
await emitCuratorEvent({
userId: input.userId,
type: "curator.chat.context_extracted",
mission: task ? { missionId: task.missionId, missionInstanceId: task.missionInstanceId, stageId: task.stageId } : undefined,
payload: { taskId: input.taskId, extract: parsedExtract },
});
} catch (error) {
console.warn("curator memory extraction failed; continuing chat", {
taskId: input.taskId,
subtaskIndex: input.subtaskIndex,
error: error instanceof Error ? error.message : String(error),
});
}
const result = await generateConversationResponse(conversationHistory.map((message) => ({
const modelMessages = conversationHistory.map((message) => ({
role: message.role,
content: message.content,
})), {
}));
if (/^start$/i.test(latest) && modelMessages.length === 0) {
modelMessages.push({ role: "user", content: firstTurnPrompt({ subtask: input.subtask, task }) });
}
const result = await generateConversationResponse(modelMessages, {
userId: input.userId,
conversationId: conversation.id,
missionInstanceId: task?.missionInstanceId,
@@ -294,10 +333,16 @@ export async function runCuratorChat(input: {
});
reply = sanitize(result.text);
if (/what should i capture next/i.test(reply) || !reply) {
reply = fallbackReply(task, latest, conversationHistory, input.subtask, input.subtaskIndex);
throw new Error("curator_generation_failed");
}
} catch (error) {
reply = fallbackReply(task, latest, conversationHistory, input.subtask, input.subtaskIndex);
console.warn("curator chat generation failed", {
taskId: input.taskId,
subtaskIndex: input.subtaskIndex,
subtask: input.subtask,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
await addMessagePg(input.userId, {

View File

@@ -57,6 +57,12 @@ export function v1CuratorRoutes() {
return c.json(await curatorActor.prepareTaskHandoff({ userId, taskId: c.req.param("taskId"), date: c.req.query("date") }));
});
app.post("/tasks/:taskId/complete", async (c) => {
const userId = c.get("userId");
const body = z.object({ reason: z.string().optional() }).parse(await c.req.json().catch(() => ({})));
return c.json(await curatorActor.completeTask({ userId, taskId: c.req.param("taskId"), date: c.req.query("date"), reason: body.reason }));
});
app.post("/events/service-impact", async (c) => {
const userId = c.get("userId");
const body = z.object({ eventId: z.string() }).parse(await c.req.json());

View File

@@ -202,7 +202,6 @@ async function taskCopy(input: {
role?: string;
serviceId?: CuratorServiceId;
}) {
const fallback = fallbackTaskCopy(input);
const cacheKey = JSON.stringify({
missionTitle: input.missionTitle,
stageTitle: input.stageTitle,
@@ -235,7 +234,6 @@ async function taskCopy(input: {
`Stage role: ${input.role ?? "none"}`,
`Service: ${input.serviceId ? serviceName(input.serviceId) : "Mission Planner"}`,
`Fallback intent: ${stageIntent(input)}`,
`Fallback copy to improve, not copy blindly: ${JSON.stringify(fallback)}`,
].join("\n"),
});
const copy = generatedTaskCopySchema.parse(parseJsonObject(result.text));
@@ -248,7 +246,7 @@ async function taskCopy(input: {
serviceId: input.serviceId,
error: error instanceof Error ? error.message : String(error),
});
return fallback;
throw error;
}
}