import { LanguageTagSchema, TimeZoneSchema, USER_COMMUNICATION_STYLE_MAX_LENGTH, UserFactCategorySchema, UserFactValueSchema, } from "@rakazo/contracts"; import type { PrismaClient } from "@rakazo/db"; import { InvalidUserFactError, updateUserProfile } from "@rakazo/db"; import { z } from "zod"; const UpdateUserProfileArgs = z.object({ facts: z .array(z.object({ category: UserFactCategorySchema, value: UserFactValueSchema })) .max(20) .optional(), time_zone: TimeZoneSchema.optional(), language: LanguageTagSchema.optional(), communication_style: z.string().trim().min(1).max(USER_COMMUNICATION_STYLE_MAX_LENGTH).optional(), }); export interface UserProfileToolContext { userId: string; bot: { id: string; kind: string }; threadId: string; /** The message the run answers, so a fact can be traced to where it was learned. */ sourceMessageId: string | null; } /** Records what the main bot learned about the user, attributed to the conversation. */ export async function updateUserProfileFromTool( prisma: PrismaClient, context: UserProfileToolContext, rawArgs: Record, ) { if (context.bot.kind !== "main") { return { error: "Only the user's main bot can update the user profile." }; } const parsed = UpdateUserProfileArgs.safeParse(rawArgs); if (!parsed.success) { const issues = parsed.error.issues .map((issue) => `${issue.path.join(".") || "arguments"}: ${issue.message}`) .join("; "); return { error: `Invalid profile update: ${issues}` }; } const args = parsed.data; const facts = args.facts ?? []; if ( facts.length === 0 && args.time_zone === undefined && args.language === undefined && args.communication_style === undefined ) { return { error: "Nothing to save. Pass facts or a profile field." }; } try { const added = await updateUserProfile(prisma, context.userId, { timeZone: args.time_zone, language: args.language, communicationStyle: args.communication_style, addFacts: facts.map((fact) => ({ ...fact, source: "conversation" as const, sourceBotId: context.bot.id, sourceThreadId: context.threadId, sourceMessageId: context.sourceMessageId, })), }); const pending = added.filter((fact) => fact.status === "pending").length; return pending > 0 ? { ok: true, saved: added.length - pending, pendingConfirmation: pending, note: "Health and values facts stay hidden from bots until the user confirms them in their settings.", } : { ok: true, saved: added.length }; } catch (error) { if (error instanceof InvalidUserFactError) return { error: error.message }; throw error; } }