Stabilize curator handoff generation

This commit is contained in:
Sai-karthik
2026-06-15 12:50:25 +00:00
parent 2ccc0ea48d
commit 89e1be4b12
3 changed files with 51 additions and 14 deletions

View File

@@ -1,4 +1,4 @@
import { Output, generateText } from "ai"; import { generateText } from "ai";
import { z } from "zod"; import { z } from "zod";
import { getConversationModel } from "../actors/conversation/agent.js"; import { getConversationModel } from "../actors/conversation/agent.js";
import { config } from "../config.js"; import { config } from "../config.js";
@@ -20,7 +20,7 @@ const feedSchema = z.object({
notifications: z.array(notificationSchema).min(6).max(24), notifications: z.array(notificationSchema).min(6).max(24),
}); });
const HOME_FEED_AGENT_TIMEOUT_MS = Number(process.env.HOME_FEED_AGENT_TIMEOUT_MS ?? 8000); const HOME_FEED_AGENT_TIMEOUT_MS = Number(process.env.HOME_FEED_AGENT_TIMEOUT_MS ?? 20000);
export type AgentHomeNotification = z.infer<typeof notificationSchema>; export type AgentHomeNotification = z.infer<typeof notificationSchema>;
@@ -54,6 +54,18 @@ function stableId(prefix: string, index: number) {
return `${prefix}-${index + 1}`; return `${prefix}-${index + 1}`;
} }
function parseJsonObject(text: string) {
const cleaned = text.trim().replace(/^```(?:json)?/i, "").replace(/```$/i, "").trim();
try {
return JSON.parse(cleaned);
} catch {
const start = cleaned.indexOf("{");
const end = cleaned.lastIndexOf("}");
if (start === -1 || end === -1 || end <= start) throw new Error("home_feed_agent_invalid_json");
return JSON.parse(cleaned.slice(start, end + 1));
}
}
export async function refineHomeNotificationsWithAgent(input: { export async function refineHomeNotificationsWithAgent(input: {
userId: string; userId: string;
context: Record<string, unknown>; context: Record<string, unknown>;
@@ -64,8 +76,11 @@ export async function refineHomeNotificationsWithAgent(input: {
try { try {
const result = await generateText({ const result = await generateText({
model: getConversationModel(), model: getConversationModel(),
output: Output.object({ schema: feedSchema }), system: [
system: SYSTEM, SYSTEM,
"Return JSON only. Shape: {\"notifications\": [...]}. Do not use markdown.",
"Use ASCII punctuation only.",
].join("\n"),
timeout: HOME_FEED_AGENT_TIMEOUT_MS, timeout: HOME_FEED_AGENT_TIMEOUT_MS,
prompt: JSON.stringify({ prompt: JSON.stringify({
task: "Create coherent GrowQR home dashboard notifications from the provided service context and deterministic candidates.", task: "Create coherent GrowQR home dashboard notifications from the provided service context and deterministic candidates.",
@@ -75,8 +90,9 @@ export async function refineHomeNotificationsWithAgent(input: {
}), }),
}); });
const parsed = feedSchema.parse(parseJsonObject(result.text));
const now = new Date().toISOString(); const now = new Date().toISOString();
return result.output.notifications.map((n, index) => ({ return parsed.notifications.map((n, index) => ({
...n, ...n,
href: sanitizeHref(n.href, n.moduleId), href: sanitizeHref(n.href, n.moduleId),
urgency: n.urgency as HomeUrgency, urgency: n.urgency as HomeUrgency,

View File

@@ -1,4 +1,4 @@
import { generateObject, generateText, tool } from "ai"; import { generateText, tool } from "ai";
import { z } from "zod"; import { z } from "zod";
import { desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
import { createClient, type Client } from "rivetkit/client"; import { createClient, type Client } from "rivetkit/client";
@@ -21,6 +21,18 @@ const signalsSchema = z.object({
})).max(5), })).max(5),
}); });
function parseJsonObject(text: string) {
const cleaned = text.trim().replace(/^```(?:json)?/i, "").replace(/```$/i, "").trim();
try {
return JSON.parse(cleaned);
} catch {
const start = cleaned.indexOf("{");
const end = cleaned.lastIndexOf("}");
if (start === -1 || end === -1 || end <= start) throw new Error("analytics_actor_invalid_json");
return JSON.parse(cleaned.slice(start, end + 1));
}
}
export const analyticsTools = { export const analyticsTools = {
read_platform_events: tool({ read_platform_events: tool({
description: "Read latest platform events.", description: "Read latest platform events.",
@@ -66,13 +78,17 @@ export const v1AnalyticsActor = {
const events = await db.select().from(growEvents).where(eq(growEvents.userId, input.userId)).orderBy(desc(growEvents.occurredAt)).limit(80); const events = await db.select().from(growEvents).where(eq(growEvents.userId, input.userId)).orderBy(desc(growEvents.occurredAt)).limit(80);
const messages = await db.select().from(growConversationMessages).where(eq(growConversationMessages.userId, input.userId)).orderBy(desc(growConversationMessages.createdAt)).limit(40); const messages = await db.select().from(growConversationMessages).where(eq(growConversationMessages.userId, input.userId)).orderBy(desc(growConversationMessages.createdAt)).limit(40);
try { try {
const result = await generateObject({ const result = await generateText({
model: getConversationModel(), model: getConversationModel(),
schema: signalsSchema, system: [
system: "You are the GrowQR V1 Analytics Actor. Generate small overnight improvement signals for the Curator. Use ASCII punctuation.", "You are the GrowQR V1 Analytics Actor. Generate small overnight improvement signals for the Curator.",
"Return JSON only. Shape: {\"signals\": [...]}. Do not use markdown.",
"Use ASCII punctuation.",
].join("\n"),
prompt: JSON.stringify({ date: input.date, events, messages }).slice(0, 20000), prompt: JSON.stringify({ date: input.date, events, messages }).slice(0, 20000),
}); });
return result.object.signals.map((signal) => curatorImprovementSignalSchema.parse({ ...signal, userId: input.userId, date: input.date })); const parsed = signalsSchema.parse(parseJsonObject(result.text));
return parsed.signals.map((signal) => curatorImprovementSignalSchema.parse({ ...signal, userId: input.userId, date: input.date }));
} catch { } catch {
return [curatorImprovementSignalSchema.parse({ return [curatorImprovementSignalSchema.parse({
id: `improvement:${input.userId}:${input.date}:streak`, id: `improvement:${input.userId}:${input.date}:streak`,

View File

@@ -219,7 +219,12 @@ function firstTurnPrompt(input: {
function isExplicitHandoffRequest(text: string) { function isExplicitHandoffRequest(text: string) {
const trimmed = text.trim(); const trimmed = text.trim();
if (/^start$/i.test(trimmed)) return false; if (/^start$/i.test(trimmed)) return false;
return /\b(start|open|launch|begin|set up|setup|create|generate)\b/i.test(trimmed); return /\b(start|open|launch|begin|set up|setup|create|generate|room|ready|go|give)\b/i.test(trimmed);
}
function shouldPrepareServiceHandoff(status: CuratorSubtaskStatusUpdate, task?: Awaited<ReturnType<typeof buildCuratorTasks>>[number]) {
if (!task?.serviceId) return false;
return status.status === "ready_to_capture" || status.status === "handoff_ready";
} }
async function evaluateSubtaskStatus(input: { async function evaluateSubtaskStatus(input: {
@@ -404,7 +409,7 @@ export async function runCuratorChat(input: {
reply, reply,
history: conversationHistory, history: conversationHistory,
}); });
if (task?.serviceId && isExplicitHandoffRequest(latest)) { if (task?.serviceId && (isExplicitHandoffRequest(latest) || statusUpdate.status === "ready_to_capture")) {
statusUpdate = { statusUpdate = {
status: "handoff_ready", status: "handoff_ready",
summary: `${task.serviceName} setup is ready. Use the action below to open it.`, summary: `${task.serviceName} setup is ready. Use the action below to open it.`,
@@ -412,8 +417,8 @@ export async function runCuratorChat(input: {
}; };
} }
const handoff = task?.serviceId && statusUpdate.status === "handoff_ready" const handoff = shouldPrepareServiceHandoff(statusUpdate, task)
? await prepareHandoffForTask(input.userId, task, task.serviceId) ? await prepareHandoffForTask(input.userId, task!, task!.serviceId)
: undefined; : undefined;
if (statusUpdate.status !== "needs_more_context") { if (statusUpdate.status !== "needs_more_context") {