81 lines
3.5 KiB
TypeScript
81 lines
3.5 KiB
TypeScript
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<CuratorOutput[]> {
|
|
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;
|
|
}
|