132 lines
6.1 KiB
TypeScript
132 lines
6.1 KiB
TypeScript
import { generateText, tool } from "ai";
|
|
import { z } from "zod";
|
|
import { desc, eq } from "drizzle-orm";
|
|
import { createClient, type Client } from "rivetkit/client";
|
|
import { config } from "../../config.js";
|
|
import type { Registry } from "../../actors/registry.js";
|
|
import { getConversationModel } from "../../actors/conversation/agent.js";
|
|
import { db } from "../../db/client.js";
|
|
import { growConversationMessages, growEvents, users } from "../../db/schema.js";
|
|
import { curatorActor } from "../curator/curator-actor.js";
|
|
import { curatorImprovementSignalSchema } from "../curator/curator-types.js";
|
|
|
|
let _client: Client<Registry> | null = null;
|
|
function getClient(): Client<Registry> {
|
|
return (_client ??= createClient<Registry>(config.rivetClientEndpoint));
|
|
}
|
|
|
|
const signalsSchema = z.object({
|
|
signals: z.array(curatorImprovementSignalSchema.omit({ userId: true, date: true }).extend({
|
|
id: z.string(),
|
|
})).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 = {
|
|
read_platform_events: tool({
|
|
description: "Read latest platform events.",
|
|
inputSchema: z.object({ limit: z.number().int().min(1).max(100).default(50) }),
|
|
execute: async ({ limit }) => db.select().from(growEvents).orderBy(desc(growEvents.occurredAt)).limit(limit),
|
|
}),
|
|
read_user_service_events: tool({
|
|
description: "Read latest service events for a user.",
|
|
inputSchema: z.object({ userId: z.string(), limit: z.number().int().min(1).max(100).default(50) }),
|
|
execute: async ({ userId, limit }) => db.select().from(growEvents).where(eq(growEvents.userId, userId)).orderBy(desc(growEvents.occurredAt)).limit(limit),
|
|
}),
|
|
read_conversation_summaries: tool({
|
|
description: "Read latest conversation messages for a user.",
|
|
inputSchema: z.object({ userId: z.string(), limit: z.number().int().min(1).max(100).default(30) }),
|
|
execute: async ({ userId, limit }) => db.select().from(growConversationMessages).where(eq(growConversationMessages.userId, userId)).orderBy(desc(growConversationMessages.createdAt)).limit(limit),
|
|
}),
|
|
generate_improvement_signals: tool({
|
|
description: "Generate curator improvement signals for a user.",
|
|
inputSchema: z.object({ userId: z.string(), date: z.string() }),
|
|
execute: async ({ userId, date }) => v1AnalyticsActor.generateImprovementSignals({ userId, date }),
|
|
}),
|
|
apply_improvement_to_curator: tool({
|
|
description: "Apply generated improvement signals to the curator.",
|
|
inputSchema: z.object({ userId: z.string(), date: z.string(), signals: z.array(curatorImprovementSignalSchema) }),
|
|
execute: async ({ userId, date, signals }) => curatorActor.applyImprovementSignals({ userId, date, signals }),
|
|
}),
|
|
};
|
|
|
|
export const v1AnalyticsActor = {
|
|
async getPlatform() {
|
|
return getClient().analyticsActor.getOrCreate(["platform"]).getPlatform();
|
|
},
|
|
|
|
async getUserQscore(input: { userId: string }) {
|
|
return getClient().analyticsActor.getOrCreate(["user", input.userId]).getUserQscore(input);
|
|
},
|
|
|
|
async getUserActivity(input: { userId: string }) {
|
|
return getClient().analyticsActor.getOrCreate(["user", input.userId]).getUserActivity(input);
|
|
},
|
|
|
|
async generateImprovementSignals(input: { userId: string; date: string }) {
|
|
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);
|
|
try {
|
|
const result = await generateText({
|
|
model: getConversationModel(),
|
|
system: [
|
|
"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),
|
|
});
|
|
const parsed = signalsSchema.parse(parseJsonObject(result.text));
|
|
return parsed.signals.map((signal) => curatorImprovementSignalSchema.parse({ ...signal, userId: input.userId, date: input.date }));
|
|
} catch {
|
|
return [curatorImprovementSignalSchema.parse({
|
|
id: `improvement:${input.userId}:${input.date}:streak`,
|
|
userId: input.userId,
|
|
date: input.date,
|
|
priority: 50,
|
|
reason: "Keep service usage meaningful and preserve streak momentum.",
|
|
nudgeText: "Pick one task that opens a real service today.",
|
|
status: "created",
|
|
})];
|
|
}
|
|
},
|
|
|
|
async applyImprovementSignals(input: { userId: string; date: string; signals: z.infer<typeof curatorImprovementSignalSchema>[] }) {
|
|
return curatorActor.applyImprovementSignals(input);
|
|
},
|
|
|
|
async runNightly(input: { date: string; userId?: string }) {
|
|
const userRows = input.userId
|
|
? [{ id: input.userId }]
|
|
: await db.select({ id: users.id }).from(users).limit(500);
|
|
let improvementSignalsCreated = 0;
|
|
for (const user of userRows) {
|
|
const signals = await this.generateImprovementSignals({ userId: user.id, date: input.date });
|
|
improvementSignalsCreated += signals.length;
|
|
await this.applyImprovementSignals({ userId: user.id, date: input.date, signals });
|
|
}
|
|
return { date: input.date, usersProcessed: userRows.length, improvementSignalsCreated };
|
|
},
|
|
|
|
async explain(input: { userId: string; question: string }) {
|
|
const answer = await generateText({
|
|
model: getConversationModel(),
|
|
system: "You are the GrowQR V1 Analytics Actor. Explain analytics and Q-score movement concisely.",
|
|
prompt: input.question,
|
|
tools: analyticsTools,
|
|
});
|
|
return { answer: answer.text };
|
|
},
|
|
};
|