Make curator chats live generated
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user