From a7d12acce60e3e16b676e03bf9e60df0ffe196d1 Mon Sep 17 00:00:00 2001 From: Jose Henrique Date: Fri, 3 Apr 2026 01:15:47 -0300 Subject: [PATCH] fixing api calls --- packages/backend/src/agent.ts | 16 ++++++ packages/backend/src/agents/curator.ts | 58 +++++++++++++--------- packages/backend/src/agents/interpreter.ts | 6 +-- packages/backend/src/agents/ranking.ts | 6 +-- packages/backend/src/agents/retrieval.ts | 6 +-- packages/backend/src/agents/validator.ts | 6 +-- 6 files changed, 63 insertions(+), 35 deletions(-) diff --git a/packages/backend/src/agent.ts b/packages/backend/src/agent.ts index b6a9e55..8695f9e 100644 --- a/packages/backend/src/agent.ts +++ b/packages/backend/src/agent.ts @@ -22,3 +22,19 @@ export const defaultModel = isGeneric ? (process.env.MODEL_NAME ?? 'default') : export const miniModel = isGeneric ? (process.env.MODEL_NAME ?? 'default') : 'gpt-5.4-mini'; export const serviceOptions = isGeneric ? {} : { service_tier: 'flex' as const }; export const supportsWebSearch = !isGeneric; + +export async function parseWithRetry(fn: () => Promise, retries = 2): Promise { + let lastErr: unknown; + for (let attempt = 0; attempt <= retries; attempt++) { + try { + return await fn(); + } catch (err) { + if (err instanceof SyntaxError && attempt < retries) { + lastErr = err; + continue; + } + throw err; + } + } + throw lastErr; +} diff --git a/packages/backend/src/agents/curator.ts b/packages/backend/src/agents/curator.ts index 58bf151..ae3a991 100644 --- a/packages/backend/src/agents/curator.ts +++ b/packages/backend/src/agents/curator.ts @@ -1,4 +1,4 @@ -import { openai, defaultModel, serviceOptions, supportsWebSearch } from '../agent.js'; +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'; @@ -14,6 +14,8 @@ const CuratorSchema = z.object({ })) }); +const CHUNK_SIZE = 20; + export async function runCurator( ranking: RankingOutput, interpreter: InterpreterOutput, @@ -32,19 +34,16 @@ export async function runCurator( if (allShows.length === 0) return []; - const showList = allShows - .map((s) => `- "${s.title}" (${s.category})`) - .join('\n'); - const canSearch = useWebSearch && supportsWebSearch; - const response = await openai.responses.parse({ - model: defaultModel, - temperature: 0.5, - max_completion_tokens: 16384, - ...serviceOptions, - ...(canSearch ? { tools: [{ type: 'web_search' as const }] } : {}), - text: { format: zodTextFormat(CuratorSchema, "shows") }, - 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.' : ''} + 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 @@ -52,18 +51,31 @@ Rules: - 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`, - input: `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)'} +- Be honest — explain why "Questionable" or "Will Not Like" ${mediaLabel}s got that rating`; + + const chunks: typeof allShows[] = []; + for (let i = 0; i < allShows.length; i += CHUNK_SIZE) { + chunks.push(allShows.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, + max_completion_tokens: 16384, + ...serviceOptions, + ...(canSearch ? { tools: [{ type: 'web_search' as const }] } : {}), + text: { format: zodTextFormat(CuratorSchema, "shows") }, + instructions, + input: `${preferenceSummary} ${mediaLabel}s to describe: ${showList}`, - }); + })); + results.push(...(response.output_parsed?.shows ?? [])); + } - return response.output_parsed?.shows ?? []; + return results; } diff --git a/packages/backend/src/agents/interpreter.ts b/packages/backend/src/agents/interpreter.ts index 3125988..b904cf4 100644 --- a/packages/backend/src/agents/interpreter.ts +++ b/packages/backend/src/agents/interpreter.ts @@ -1,4 +1,4 @@ -import { openai, defaultModel, serviceOptions } from '../agent.js'; +import { openai, defaultModel, serviceOptions, parseWithRetry } from '../agent.js'; import type { InterpreterOutput, MediaType } from '../types/agents.js'; import { z } from 'zod'; import { zodTextFormat } from 'openai/helpers/zod'; @@ -28,7 +28,7 @@ export async function runInterpreter(input: InterpreterInput): Promise openai.responses.parse({ model: defaultModel, temperature: 0.2, ...serviceOptions, @@ -46,7 +46,7 @@ Rules: Liked ${mediaLabel}s: ${input.liked_shows || '(none)'} Disliked ${mediaLabel}s: ${input.disliked_shows || '(none)'} Themes and requirements: ${input.themes || '(none)'}${feedbackSection}`, - }); + })); return (response.output_parsed as InterpreterOutput) ?? { liked: [], disliked: [], themes: [], character_preferences: [], tone: [], avoid: [], requirements: [] diff --git a/packages/backend/src/agents/ranking.ts b/packages/backend/src/agents/ranking.ts index ac9fc82..cb7cb82 100644 --- a/packages/backend/src/agents/ranking.ts +++ b/packages/backend/src/agents/ranking.ts @@ -1,4 +1,4 @@ -import { openai, defaultModel, serviceOptions } from '../agent.js'; +import { openai, defaultModel, serviceOptions, parseWithRetry } from '../agent.js'; import type { InterpreterOutput, RetrievalOutput, RankingOutput, MediaType } from '../types/agents.js'; import { z } from 'zod'; import { zodTextFormat } from 'openai/helpers/zod'; @@ -44,7 +44,7 @@ export async function runRanking( for (const chunk of chunks) { const chunkTitles = chunk.map((c) => `- ${c.title}: ${c.reason}`).join('\n'); - const response = await openai.responses.parse({ + const response = await parseWithRetry(() => openai.responses.parse({ model: defaultModel, temperature: 0.2, max_completion_tokens: 16384, @@ -70,7 +70,7 @@ Requirements: ${interpreter.requirements.join(', ') || '(none)'} Rank these ${mediaLabel}s: ${chunkTitles}`, - }); + })); const chunkResult = (response.output_parsed as Partial) ?? {}; diff --git a/packages/backend/src/agents/retrieval.ts b/packages/backend/src/agents/retrieval.ts index 188f47f..bc6da7b 100644 --- a/packages/backend/src/agents/retrieval.ts +++ b/packages/backend/src/agents/retrieval.ts @@ -1,4 +1,4 @@ -import { openai, defaultModel, serviceOptions, supportsWebSearch } from '../agent.js'; +import { openai, defaultModel, serviceOptions, supportsWebSearch, parseWithRetry } from '../agent.js'; import type { InterpreterOutput, RetrievalOutput, MediaType } from '../types/agents.js'; import { z } from 'zod'; import { zodTextFormat } from 'openai/helpers/zod'; @@ -22,7 +22,7 @@ export async function runRetrieval( const mediaLabelPlural = mediaType === 'movie' ? 'movies' : 'TV shows'; const canSearch = useWebSearch && supportsWebSearch; - const response = await openai.responses.parse({ + const response = await parseWithRetry(() => openai.responses.parse({ model: defaultModel, temperature: 0.9, max_completion_tokens: 16384, @@ -48,7 +48,7 @@ Avoid: ${input.avoid.join(', ') || '(none)'} Requirements: ${input.requirements.join(', ') || '(none)'}${previousFullMatches.length > 0 ? `\n\nPrevious Full Match titles (DO NOT repeat these; use them as inspiration for NEW candidates with similar qualities): ${previousFullMatches.join(', ')}` : ''} Generate a large, diverse pool of ${mediaLabel} candidates.`, - }); + })); return (response.output_parsed as RetrievalOutput) ?? { candidates: [] }; } diff --git a/packages/backend/src/agents/validator.ts b/packages/backend/src/agents/validator.ts index 71a87f9..981a117 100644 --- a/packages/backend/src/agents/validator.ts +++ b/packages/backend/src/agents/validator.ts @@ -1,4 +1,4 @@ -import { openai, defaultModel, serviceOptions, supportsWebSearch } from '../agent.js'; +import { openai, defaultModel, serviceOptions, supportsWebSearch, parseWithRetry } from '../agent.js'; import type { RetrievalCandidate, ValidatorOutput, MediaType } from '../types/agents.js'; import { z } from 'zod'; import { zodTextFormat } from 'openai/helpers/zod'; @@ -27,7 +27,7 @@ async function runValidatorChunk( ): Promise { const list = candidates.map((c) => `- ${c.title}: ${c.reason}`).join('\n'); - const response = await openai.responses.parse({ + const response = await parseWithRetry(() => openai.responses.parse({ model: defaultModel, temperature: 0.1, max_completion_tokens: 16384, @@ -46,7 +46,7 @@ Set isTrash: true for entries that: Set isTrash: false for real, verifiable ${mediaLabel}s, even if minor metadata corrections are needed. Return every candidate — do not drop any entries from the output.`, input: `Validate these ${mediaLabel} candidates:\n${list}`, - }); + })); return (response.output_parsed as ValidatorOutput) ?? { candidates: [] }; }