fixing api calls
This commit is contained in:
@@ -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 miniModel = isGeneric ? (process.env.MODEL_NAME ?? 'default') : 'gpt-5.4-mini';
|
||||||
export const serviceOptions = isGeneric ? {} : { service_tier: 'flex' as const };
|
export const serviceOptions = isGeneric ? {} : { service_tier: 'flex' as const };
|
||||||
export const supportsWebSearch = !isGeneric;
|
export const supportsWebSearch = !isGeneric;
|
||||||
|
|
||||||
|
export async function parseWithRetry<T>(fn: () => Promise<T>, retries = 2): Promise<T> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 type { InterpreterOutput, RankingOutput, CuratorOutput, MediaType } from '../types/agents.js';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { zodTextFormat } from 'openai/helpers/zod';
|
import { zodTextFormat } from 'openai/helpers/zod';
|
||||||
@@ -14,6 +14,8 @@ const CuratorSchema = z.object({
|
|||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const CHUNK_SIZE = 20;
|
||||||
|
|
||||||
export async function runCurator(
|
export async function runCurator(
|
||||||
ranking: RankingOutput,
|
ranking: RankingOutput,
|
||||||
interpreter: InterpreterOutput,
|
interpreter: InterpreterOutput,
|
||||||
@@ -32,19 +34,16 @@ export async function runCurator(
|
|||||||
|
|
||||||
if (allShows.length === 0) return [];
|
if (allShows.length === 0) return [];
|
||||||
|
|
||||||
const showList = allShows
|
|
||||||
.map((s) => `- "${s.title}" (${s.category})`)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const canSearch = useWebSearch && supportsWebSearch;
|
const canSearch = useWebSearch && supportsWebSearch;
|
||||||
const response = await openai.responses.parse({
|
const preferenceSummary = `User preferences summary:
|
||||||
model: defaultModel,
|
Liked: ${interpreter.liked.join(', ') || '(none)'}
|
||||||
temperature: 0.5,
|
Themes: ${interpreter.themes.join(', ') || '(none)'}
|
||||||
max_completion_tokens: 16384,
|
Tone: ${interpreter.tone.join(', ') || '(none)'}
|
||||||
...serviceOptions,
|
Character preferences: ${interpreter.character_preferences.join(', ') || '(none)'}
|
||||||
...(canSearch ? { tools: [{ type: 'web_search' as const }] } : {}),
|
Avoid: ${interpreter.avoid.join(', ') || '(none)'}
|
||||||
text: { format: zodTextFormat(CuratorSchema, "shows") },
|
Requirements: ${interpreter.requirements.join(', ') || '(none)'}`;
|
||||||
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 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:
|
Rules:
|
||||||
- Preserve the exact title and category as given
|
- 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")
|
- 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
|
- 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
|
- 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`,
|
- Be honest — explain why "Questionable" or "Will Not Like" ${mediaLabel}s got that rating`;
|
||||||
input: `User preferences summary:
|
|
||||||
Liked: ${interpreter.liked.join(', ') || '(none)'}
|
const chunks: typeof allShows[] = [];
|
||||||
Themes: ${interpreter.themes.join(', ') || '(none)'}
|
for (let i = 0; i < allShows.length; i += CHUNK_SIZE) {
|
||||||
Tone: ${interpreter.tone.join(', ') || '(none)'}
|
chunks.push(allShows.slice(i, i + CHUNK_SIZE));
|
||||||
Character preferences: ${interpreter.character_preferences.join(', ') || '(none)'}
|
}
|
||||||
Avoid: ${interpreter.avoid.join(', ') || '(none)'}
|
|
||||||
Requirements: ${interpreter.requirements.join(', ') || '(none)'}
|
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:
|
${mediaLabel}s to describe:
|
||||||
${showList}`,
|
${showList}`,
|
||||||
});
|
}));
|
||||||
|
results.push(...(response.output_parsed?.shows ?? []));
|
||||||
|
}
|
||||||
|
|
||||||
return response.output_parsed?.shows ?? [];
|
return results;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 type { InterpreterOutput, MediaType } from '../types/agents.js';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { zodTextFormat } from 'openai/helpers/zod';
|
import { zodTextFormat } from 'openai/helpers/zod';
|
||||||
@@ -28,7 +28,7 @@ export async function runInterpreter(input: InterpreterInput): Promise<Interpret
|
|||||||
? `\n\nUser Feedback Context (incorporate into preferences):\n${input.feedback_context}`
|
? `\n\nUser Feedback Context (incorporate into preferences):\n${input.feedback_context}`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const response = await openai.responses.parse({
|
const response = await parseWithRetry(() => openai.responses.parse({
|
||||||
model: defaultModel,
|
model: defaultModel,
|
||||||
temperature: 0.2,
|
temperature: 0.2,
|
||||||
...serviceOptions,
|
...serviceOptions,
|
||||||
@@ -46,7 +46,7 @@ Rules:
|
|||||||
Liked ${mediaLabel}s: ${input.liked_shows || '(none)'}
|
Liked ${mediaLabel}s: ${input.liked_shows || '(none)'}
|
||||||
Disliked ${mediaLabel}s: ${input.disliked_shows || '(none)'}
|
Disliked ${mediaLabel}s: ${input.disliked_shows || '(none)'}
|
||||||
Themes and requirements: ${input.themes || '(none)'}${feedbackSection}`,
|
Themes and requirements: ${input.themes || '(none)'}${feedbackSection}`,
|
||||||
});
|
}));
|
||||||
|
|
||||||
return (response.output_parsed as InterpreterOutput) ?? {
|
return (response.output_parsed as InterpreterOutput) ?? {
|
||||||
liked: [], disliked: [], themes: [], character_preferences: [], tone: [], avoid: [], requirements: []
|
liked: [], disliked: [], themes: [], character_preferences: [], tone: [], avoid: [], requirements: []
|
||||||
|
|||||||
@@ -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 type { InterpreterOutput, RetrievalOutput, RankingOutput, MediaType } from '../types/agents.js';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { zodTextFormat } from 'openai/helpers/zod';
|
import { zodTextFormat } from 'openai/helpers/zod';
|
||||||
@@ -44,7 +44,7 @@ export async function runRanking(
|
|||||||
for (const chunk of chunks) {
|
for (const chunk of chunks) {
|
||||||
const chunkTitles = chunk.map((c) => `- ${c.title}: ${c.reason}`).join('\n');
|
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,
|
model: defaultModel,
|
||||||
temperature: 0.2,
|
temperature: 0.2,
|
||||||
max_completion_tokens: 16384,
|
max_completion_tokens: 16384,
|
||||||
@@ -70,7 +70,7 @@ Requirements: ${interpreter.requirements.join(', ') || '(none)'}
|
|||||||
|
|
||||||
Rank these ${mediaLabel}s:
|
Rank these ${mediaLabel}s:
|
||||||
${chunkTitles}`,
|
${chunkTitles}`,
|
||||||
});
|
}));
|
||||||
|
|
||||||
const chunkResult = (response.output_parsed as Partial<RankingOutput>) ?? {};
|
const chunkResult = (response.output_parsed as Partial<RankingOutput>) ?? {};
|
||||||
|
|
||||||
|
|||||||
@@ -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 type { InterpreterOutput, RetrievalOutput, MediaType } from '../types/agents.js';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { zodTextFormat } from 'openai/helpers/zod';
|
import { zodTextFormat } from 'openai/helpers/zod';
|
||||||
@@ -22,7 +22,7 @@ export async function runRetrieval(
|
|||||||
const mediaLabelPlural = mediaType === 'movie' ? 'movies' : 'TV shows';
|
const mediaLabelPlural = mediaType === 'movie' ? 'movies' : 'TV shows';
|
||||||
|
|
||||||
const canSearch = useWebSearch && supportsWebSearch;
|
const canSearch = useWebSearch && supportsWebSearch;
|
||||||
const response = await openai.responses.parse({
|
const response = await parseWithRetry(() => openai.responses.parse({
|
||||||
model: defaultModel,
|
model: defaultModel,
|
||||||
temperature: 0.9,
|
temperature: 0.9,
|
||||||
max_completion_tokens: 16384,
|
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(', ')}` : ''}
|
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.`,
|
Generate a large, diverse pool of ${mediaLabel} candidates.`,
|
||||||
});
|
}));
|
||||||
|
|
||||||
return (response.output_parsed as RetrievalOutput) ?? { candidates: [] };
|
return (response.output_parsed as RetrievalOutput) ?? { candidates: [] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 type { RetrievalCandidate, ValidatorOutput, MediaType } from '../types/agents.js';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { zodTextFormat } from 'openai/helpers/zod';
|
import { zodTextFormat } from 'openai/helpers/zod';
|
||||||
@@ -27,7 +27,7 @@ async function runValidatorChunk(
|
|||||||
): Promise<ValidatorOutput> {
|
): Promise<ValidatorOutput> {
|
||||||
const list = candidates.map((c) => `- ${c.title}: ${c.reason}`).join('\n');
|
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,
|
model: defaultModel,
|
||||||
temperature: 0.1,
|
temperature: 0.1,
|
||||||
max_completion_tokens: 16384,
|
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.
|
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.`,
|
Return every candidate — do not drop any entries from the output.`,
|
||||||
input: `Validate these ${mediaLabel} candidates:\n${list}`,
|
input: `Validate these ${mediaLabel} candidates:\n${list}`,
|
||||||
});
|
}));
|
||||||
|
|
||||||
return (response.output_parsed as ValidatorOutput) ?? { candidates: [] };
|
return (response.output_parsed as ValidatorOutput) ?? { candidates: [] };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user