import { openai, defaultModel, serviceOptions } from '../agent.js'; import type { InterpreterOutput, RetrievalOutput, RankingOutput, MediaType } from '../types/agents.js'; import { z } from 'zod'; import { zodTextFormat } from 'openai/helpers/zod'; const RankingSchema = z.object({ definitely_like: z.array(z.string()), might_like: z.array(z.string()), questionable: z.array(z.string()), will_not_like: z.array(z.string()) }); export async function runRanking( interpreter: InterpreterOutput, retrieval: RetrievalOutput, mediaType: MediaType = 'tv_show', ): Promise { const mediaLabel = mediaType === 'movie' ? 'movie' : 'TV show'; // Phase 1: Pre-filter — remove avoidance violations const avoidList = interpreter.avoid.map((a) => a.toLowerCase()); const filtered = retrieval.candidates.filter((c) => { const text = (c.title + ' ' + c.reason).toLowerCase(); return !avoidList.some((a) => text.includes(a)); }); // Phase 2: Chunked ranking — split into groups of ~15 const CHUNK_SIZE = 15; const chunks: typeof filtered[] = []; for (let i = 0; i < filtered.length; i += CHUNK_SIZE) { chunks.push(filtered.slice(i, i + CHUNK_SIZE)); } const allTags: RankingOutput = { definitely_like: [], might_like: [], questionable: [], will_not_like: [], }; for (const chunk of chunks) { const chunkTitles = chunk.map((c) => `- ${c.title}: ${c.reason}`).join('\n'); const response = await openai.responses.parse({ model: defaultModel, temperature: 0.2, ...serviceOptions, text: { format: zodTextFormat(RankingSchema, "ranking") }, instructions: `You are a ${mediaLabel} ranking critic. Assign each ${mediaLabel} to exactly one of four confidence tags based on how well it matches the user's preferences. Tags: - "definitely_like": Near-perfect match to all preferences - "might_like": Strong match to most preferences - "questionable": Partial alignment, some aspects don't match - "will_not_like": Likely mismatch, conflicts with preferences or avoidance criteria Every ${mediaLabel} in the input must appear in exactly one tag. Use the title exactly as given.`, input: `User preferences: Liked ${mediaLabel}s: ${JSON.stringify(interpreter.liked)} Themes: ${JSON.stringify(interpreter.themes)} Character preferences: ${JSON.stringify(interpreter.character_preferences)} Tone: ${JSON.stringify(interpreter.tone)} Avoid: ${JSON.stringify(interpreter.avoid)} Rank these ${mediaLabel}s: ${chunkTitles}`, }); const chunkResult = (response.output_parsed as Partial) ?? {}; allTags.definitely_like.push(...(chunkResult.definitely_like ?? [])); allTags.might_like.push(...(chunkResult.might_like ?? [])); allTags.questionable.push(...(chunkResult.questionable ?? [])); allTags.will_not_like.push(...(chunkResult.will_not_like ?? [])); } return allTags; }