import { openai, defaultModel, serviceOptions, supportsWebSearch, parseWithRetry } from '../agent.js'; import type { InterpreterOutput, RankingOutput, CuratorOutput, MediaType } from '../types/agents.js'; import { z } from 'zod'; import { zodTextFormat } from 'openai/helpers/zod'; const CuratorSchema = z.object({ series: z.array(z.object({ title: z.string(), explanation: z.string(), category: z.enum(["Full Match", "Definitely Like", "Might Like", "Questionable", "Will Not Like"]), genre: z.string(), pros: z.array(z.string()).max(3), cons: z.array(z.string()).max(3) })) }); const CHUNK_SIZE = 20; export async function runCurator( ranking: RankingOutput, interpreter: InterpreterOutput, mediaType: MediaType = 'tv_show', useWebSearch = false, ): Promise { const mediaLabel = mediaType === 'movie' ? 'movie' : 'TV show'; const allSeries = [ ...(ranking.full_match ?? []).map((t) => ({ title: t, category: 'Full Match' as const })), ...ranking.definitely_like.map((t) => ({ title: t, category: 'Definitely Like' as const })), ...ranking.might_like.map((t) => ({ title: t, category: 'Might Like' as const })), ...ranking.questionable.map((t) => ({ title: t, category: 'Questionable' as const })), ...ranking.will_not_like.map((t) => ({ title: t, category: 'Will Not Like' as const })), ]; if (allSeries.length === 0) return []; const canSearch = useWebSearch && supportsWebSearch; const preferenceSummary = `User preferences summary: Liked: ${interpreter.liked.join(', ') || '(none)'} Themes: ${interpreter.themes.join(', ') || '(none)'} Tone: ${interpreter.tone.join(', ') || '(none)'} Character preferences: ${interpreter.character_preferences.join(', ') || '(none)'} Avoid: ${interpreter.avoid.join(', ') || '(none)'} Requirements: ${interpreter.requirements.join(', ') || '(none)'}`; const instructions = `You are a ${mediaLabel} recommendation curator. For each ${mediaLabel}, write a concise explanation and surface the most useful details for the user.${useWebSearch ? '\n\nUse web search to verify details and enrich explanations with accurate information.' : ''} Rules: - Preserve the exact title and category as given - explanation: 1-2 sentences explaining why it was assigned to its category, referencing specific user preferences - genre: 1-3 words capturing the most prominent genre of the title (e.g. "Crime Drama", "Sci-Fi Thriller", "Romantic Comedy") - pros: up to 3 short bullet points about what this title does well relative to the user's taste - cons: up to 3 short bullet points about what the user might not like based on their preferences - Be honest — explain why "Questionable" or "Will Not Like" ${mediaLabel}s got that rating`; const chunks: typeof allSeries[] = []; for (let i = 0; i < allSeries.length; i += CHUNK_SIZE) { chunks.push(allSeries.slice(i, i + CHUNK_SIZE)); } const results: CuratorOutput[] = []; for (const chunk of chunks) { const showList = chunk.map((s) => `- "${s.title}" (${s.category})`).join('\n'); const response = await parseWithRetry(() => openai.responses.parse({ model: defaultModel, temperature: 0.5, ...serviceOptions, ...(canSearch ? { tools: [{ type: 'web_search' as const }] } : {}), text: { format: zodTextFormat(CuratorSchema, "series") }, instructions, input: `${preferenceSummary} ${mediaLabel}s to describe: ${showList}`, })); results.push(...(response.output_parsed?.series ?? [])); } return results; }