Stabilize curator handoff generation
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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`,
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
Reference in New Issue
Block a user