adding web search to all recs
This commit is contained in:
12
package-lock.json
generated
12
package-lock.json
generated
@@ -4455,6 +4455,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "4.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||||
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/backend": {
|
"packages/backend": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@@ -4463,7 +4472,8 @@
|
|||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
"fastify": "^5.8.4",
|
"fastify": "^5.8.4",
|
||||||
"openai": "^6.32.0",
|
"openai": "^6.32.0",
|
||||||
"postgres": "^3.4.8"
|
"postgres": "^3.4.8",
|
||||||
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.12.0",
|
"@types/node": "^24.12.0",
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
"fastify": "^5.8.4",
|
"fastify": "^5.8.4",
|
||||||
"openai": "^6.32.0",
|
"openai": "^6.32.0",
|
||||||
"postgres": "^3.4.8"
|
"postgres": "^3.4.8",
|
||||||
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.12.0",
|
"@types/node": "^24.12.0",
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
import { openai } from '../agent.js';
|
import { openai } from '../agent.js';
|
||||||
import type { InterpreterOutput, RankingOutput, CuratorOutput } from '../types/agents.js';
|
import type { InterpreterOutput, RankingOutput, CuratorOutput } from '../types/agents.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { zodTextFormat } from 'openai/helpers/zod';
|
||||||
|
|
||||||
|
const CuratorSchema = z.object({
|
||||||
|
shows: z.array(z.object({
|
||||||
|
title: z.string(),
|
||||||
|
explanation: z.string(),
|
||||||
|
category: z.enum(["Definitely Like", "Might Like", "Questionable", "Will Not Like"])
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
export async function runCurator(
|
export async function runCurator(
|
||||||
ranking: RankingOutput,
|
ranking: RankingOutput,
|
||||||
@@ -18,36 +28,22 @@ export async function runCurator(
|
|||||||
.map((s) => `- "${s.title}" (${s.category})`)
|
.map((s) => `- "${s.title}" (${s.category})`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
const response = await openai.chat.completions.create({
|
const response = await openai.responses.parse({
|
||||||
model: 'gpt-5.4-mini',
|
model: 'gpt-5.4',
|
||||||
temperature: 0.5,
|
temperature: 0.5,
|
||||||
service_tier: 'flex',
|
service_tier: 'flex',
|
||||||
response_format: { type: 'json_object' },
|
tools: [
|
||||||
messages: [
|
{ type: 'web_search' }
|
||||||
{
|
],
|
||||||
role: 'system',
|
text: { format: zodTextFormat(CuratorSchema, "shows") },
|
||||||
content: `You are a TV show recommendation curator. For each show, write a concise 1-2 sentence explanation of why it was assigned to its category based on the user's preferences.
|
instructions: `You are a TV show recommendation curator. For each show, write a concise 1-2 sentence explanation of why it was assigned to its category based on the user's preferences.
|
||||||
|
|
||||||
Your output MUST be valid JSON:
|
|
||||||
{
|
|
||||||
"shows": [
|
|
||||||
{
|
|
||||||
"title": string,
|
|
||||||
"explanation": string,
|
|
||||||
"category": "Definitely Like" | "Might Like" | "Questionable" | "Will Not Like"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- Preserve the exact title and category as given
|
- Preserve the exact title and category as given
|
||||||
- Keep explanations concise (1-2 sentences max)
|
- Keep explanations concise (1-2 sentences max)
|
||||||
- Reference specific user preferences in the explanation
|
- Reference specific user preferences in the explanation
|
||||||
- Be honest — explain why "Questionable" or "Will Not Like" shows got that rating`,
|
- Be honest — explain why "Questionable" or "Will Not Like" shows got that rating`,
|
||||||
},
|
input: `User preferences summary:
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: `User preferences summary:
|
|
||||||
Liked: ${JSON.stringify(interpreter.liked)}
|
Liked: ${JSON.stringify(interpreter.liked)}
|
||||||
Themes: ${JSON.stringify(interpreter.themes)}
|
Themes: ${JSON.stringify(interpreter.themes)}
|
||||||
Tone: ${JSON.stringify(interpreter.tone)}
|
Tone: ${JSON.stringify(interpreter.tone)}
|
||||||
@@ -56,11 +52,7 @@ Avoid: ${JSON.stringify(interpreter.avoid)}
|
|||||||
|
|
||||||
Shows to describe:
|
Shows to describe:
|
||||||
${showList}`,
|
${showList}`,
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const content = response.choices[0]?.message?.content ?? '{"shows":[]}';
|
return response.output_parsed?.shows ?? [];
|
||||||
const result = JSON.parse(content) as { shows: CuratorOutput[] };
|
|
||||||
return result.shows ?? [];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import { openai } from '../agent.js';
|
import { openai } from '../agent.js';
|
||||||
import type { InterpreterOutput } from '../types/agents.js';
|
import type { InterpreterOutput } from '../types/agents.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { zodTextFormat } from 'openai/helpers/zod';
|
||||||
|
|
||||||
|
const InterpreterSchema = z.object({
|
||||||
|
liked: z.array(z.string()),
|
||||||
|
disliked: z.array(z.string()),
|
||||||
|
themes: z.array(z.string()),
|
||||||
|
character_preferences: z.array(z.string()),
|
||||||
|
tone: z.array(z.string()),
|
||||||
|
avoid: z.array(z.string())
|
||||||
|
});
|
||||||
|
|
||||||
interface InterpreterInput {
|
interface InterpreterInput {
|
||||||
main_prompt: string;
|
main_prompt: string;
|
||||||
@@ -14,25 +25,12 @@ 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.chat.completions.create({
|
const response = await openai.responses.parse({
|
||||||
model: 'gpt-5.4-mini',
|
model: 'gpt-5.4-mini',
|
||||||
temperature: 0.2,
|
temperature: 0.2,
|
||||||
service_tier: 'flex',
|
service_tier: 'flex',
|
||||||
response_format: { type: 'json_object' },
|
text: { format: zodTextFormat(InterpreterSchema, "preferences") },
|
||||||
messages: [
|
instructions: `You are a TV show preference interpreter. Transform raw user input into structured, normalized preferences.
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: `You are a TV show preference interpreter. Transform raw user input into structured, normalized preferences.
|
|
||||||
|
|
||||||
Your output MUST be valid JSON matching this schema:
|
|
||||||
{
|
|
||||||
"liked": string[], // shows the user likes
|
|
||||||
"disliked": string[], // shows the user dislikes
|
|
||||||
"themes": string[], // normalized themes (e.g. "spy" -> "espionage")
|
|
||||||
"character_preferences": string[], // character types they prefer
|
|
||||||
"tone": string[], // tone descriptors (e.g. "serious", "grounded", "dark")
|
|
||||||
"avoid": string[] // things to explicitly avoid
|
|
||||||
}
|
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- Extract implicit preferences from the main prompt
|
- Extract implicit preferences from the main prompt
|
||||||
@@ -40,17 +38,13 @@ Rules:
|
|||||||
- Detect and resolve contradictions (prefer explicit over implicit)
|
- Detect and resolve contradictions (prefer explicit over implicit)
|
||||||
- Do NOT assume anything not stated or clearly implied
|
- Do NOT assume anything not stated or clearly implied
|
||||||
- Be specific and concrete, not vague`,
|
- Be specific and concrete, not vague`,
|
||||||
},
|
input: `Main prompt: ${input.main_prompt}
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: `Main prompt: ${input.main_prompt}
|
|
||||||
Liked shows: ${input.liked_shows || '(none)'}
|
Liked shows: ${input.liked_shows || '(none)'}
|
||||||
Disliked shows: ${input.disliked_shows || '(none)'}
|
Disliked shows: ${input.disliked_shows || '(none)'}
|
||||||
Themes and requirements: ${input.themes || '(none)'}${feedbackSection}`,
|
Themes and requirements: ${input.themes || '(none)'}${feedbackSection}`,
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const content = response.choices[0]?.message?.content ?? '{}';
|
return (response.output_parsed as InterpreterOutput) ?? {
|
||||||
return JSON.parse(content) as InterpreterOutput;
|
liked: [], disliked: [], themes: [], character_preferences: [], tone: [], avoid: []
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import { openai } from '../agent.js';
|
import { openai } from '../agent.js';
|
||||||
import type { InterpreterOutput, RetrievalOutput, RankingOutput } from '../types/agents.js';
|
import type { InterpreterOutput, RetrievalOutput, RankingOutput } 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(
|
export async function runRanking(
|
||||||
interpreter: InterpreterOutput,
|
interpreter: InterpreterOutput,
|
||||||
@@ -29,15 +38,12 @@ 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.chat.completions.create({
|
const response = await openai.responses.parse({
|
||||||
model: 'gpt-5.4',
|
model: 'gpt-5.4',
|
||||||
temperature: 0.2,
|
temperature: 0.2,
|
||||||
service_tier: 'flex',
|
service_tier: 'flex',
|
||||||
response_format: { type: 'json_object' },
|
text: { format: zodTextFormat(RankingSchema, "ranking") },
|
||||||
messages: [
|
instructions: `You are a TV show ranking critic. Assign each show to exactly one of four confidence buckets based on how well it matches the user's preferences.
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: `You are a TV show ranking critic. Assign each show to exactly one of four confidence buckets based on how well it matches the user's preferences.
|
|
||||||
|
|
||||||
Buckets:
|
Buckets:
|
||||||
- "definitely_like": Near-perfect match to all preferences
|
- "definitely_like": Near-perfect match to all preferences
|
||||||
@@ -45,19 +51,8 @@ Buckets:
|
|||||||
- "questionable": Partial alignment, some aspects don't match
|
- "questionable": Partial alignment, some aspects don't match
|
||||||
- "will_not_like": Likely mismatch, conflicts with preferences or avoidance criteria
|
- "will_not_like": Likely mismatch, conflicts with preferences or avoidance criteria
|
||||||
|
|
||||||
Your output MUST be valid JSON:
|
|
||||||
{
|
|
||||||
"definitely_like": string[],
|
|
||||||
"might_like": string[],
|
|
||||||
"questionable": string[],
|
|
||||||
"will_not_like": string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
Every show in the input must appear in exactly one bucket. Use the title exactly as given.`,
|
Every show in the input must appear in exactly one bucket. Use the title exactly as given.`,
|
||||||
},
|
input: `User preferences:
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: `User preferences:
|
|
||||||
Liked shows: ${JSON.stringify(interpreter.liked)}
|
Liked shows: ${JSON.stringify(interpreter.liked)}
|
||||||
Themes: ${JSON.stringify(interpreter.themes)}
|
Themes: ${JSON.stringify(interpreter.themes)}
|
||||||
Character preferences: ${JSON.stringify(interpreter.character_preferences)}
|
Character preferences: ${JSON.stringify(interpreter.character_preferences)}
|
||||||
@@ -66,12 +61,9 @@ Avoid: ${JSON.stringify(interpreter.avoid)}
|
|||||||
|
|
||||||
Rank these shows:
|
Rank these shows:
|
||||||
${chunkTitles}`,
|
${chunkTitles}`,
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const content = response.choices[0]?.message?.content ?? '{}';
|
const chunkResult = (response.output_parsed as Partial<RankingOutput>) ?? {};
|
||||||
const chunkResult = JSON.parse(content) as Partial<RankingOutput>;
|
|
||||||
|
|
||||||
allBuckets.definitely_like.push(...(chunkResult.definitely_like ?? []));
|
allBuckets.definitely_like.push(...(chunkResult.definitely_like ?? []));
|
||||||
allBuckets.might_like.push(...(chunkResult.might_like ?? []));
|
allBuckets.might_like.push(...(chunkResult.might_like ?? []));
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import { openai } from '../agent.js';
|
import { openai } from '../agent.js';
|
||||||
import type { InterpreterOutput, RetrievalOutput } from '../types/agents.js';
|
import type { InterpreterOutput, RetrievalOutput } from '../types/agents.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { zodTextFormat } from 'openai/helpers/zod';
|
||||||
|
|
||||||
|
const RetrievalSchema = z.object({
|
||||||
|
candidates: z.array(z.object({
|
||||||
|
title: z.string(),
|
||||||
|
reason: z.string()
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
export async function runRetrieval(input: InterpreterOutput, brainstormCount = 100): Promise<RetrievalOutput> {
|
export async function runRetrieval(input: InterpreterOutput, brainstormCount = 100): Promise<RetrievalOutput> {
|
||||||
const response = await openai.chat.completions.create({
|
const response = await openai.responses.parse({
|
||||||
model: 'gpt-5.4',
|
model: 'gpt-5.4',
|
||||||
temperature: 0.9,
|
temperature: 0.9,
|
||||||
service_tier: 'flex',
|
service_tier: 'flex',
|
||||||
response_format: { type: 'json_object' },
|
tools: [
|
||||||
messages: [
|
{ type: 'web_search' }
|
||||||
{
|
],
|
||||||
role: 'system',
|
text: { format: zodTextFormat(RetrievalSchema, "candidates") },
|
||||||
content: `You are a TV show candidate generator. Your goal is to brainstorm a LARGE, DIVERSE pool of ${brainstormCount} TV show candidates that match the user's structured preferences.
|
instructions: `You are a TV show candidate generator. Your goal is to brainstorm a LARGE, DIVERSE pool of ${brainstormCount} TV show candidates that match the user's structured preferences.
|
||||||
|
|
||||||
Your output MUST be valid JSON matching this schema:
|
|
||||||
{
|
|
||||||
"candidates": [
|
|
||||||
{ "title": string, "reason": string }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- Include both well-known and obscure shows
|
- Include both well-known and obscure shows
|
||||||
@@ -26,10 +28,7 @@ Rules:
|
|||||||
- Avoid duplicates
|
- Avoid duplicates
|
||||||
- Include shows from different decades, countries, and networks
|
- Include shows from different decades, countries, and networks
|
||||||
- Aim for ${brainstormCount} candidates minimum`,
|
- Aim for ${brainstormCount} candidates minimum`,
|
||||||
},
|
input: `Structured preferences:
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: `Structured preferences:
|
|
||||||
Liked shows: ${JSON.stringify(input.liked)}
|
Liked shows: ${JSON.stringify(input.liked)}
|
||||||
Disliked shows: ${JSON.stringify(input.disliked)}
|
Disliked shows: ${JSON.stringify(input.disliked)}
|
||||||
Themes: ${JSON.stringify(input.themes)}
|
Themes: ${JSON.stringify(input.themes)}
|
||||||
@@ -38,10 +37,7 @@ Tone: ${JSON.stringify(input.tone)}
|
|||||||
Avoid: ${JSON.stringify(input.avoid)}
|
Avoid: ${JSON.stringify(input.avoid)}
|
||||||
|
|
||||||
Generate a large, diverse pool of TV show candidates.`,
|
Generate a large, diverse pool of TV show candidates.`,
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const content = response.choices[0]?.message?.content ?? '{"candidates":[]}';
|
return (response.output_parsed as RetrievalOutput) ?? { candidates: [] };
|
||||||
return JSON.parse(content) as RetrievalOutput;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,27 +2,19 @@ import { openai } from '../agent.js';
|
|||||||
import type { InterpreterOutput } from '../types/agents.js';
|
import type { InterpreterOutput } from '../types/agents.js';
|
||||||
|
|
||||||
export async function generateTitle(interpreter: InterpreterOutput): Promise<string> {
|
export async function generateTitle(interpreter: InterpreterOutput): Promise<string> {
|
||||||
const response = await openai.chat.completions.create({
|
const response = await openai.responses.create({
|
||||||
model: 'gpt-4o-mini',
|
model: 'gpt-5.4-mini',
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
service_tier: 'flex',
|
service_tier: 'flex',
|
||||||
messages: [
|
instructions: `Generate a concise 5-8 word title for a TV show recommendation session.
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: `Generate a concise 5-8 word title for a TV show recommendation session.
|
|
||||||
Capture the essence of the user's taste — genre, tone, key themes.
|
Capture the essence of the user's taste — genre, tone, key themes.
|
||||||
Respond with ONLY the title. No quotes, no trailing punctuation.
|
Respond with ONLY the title. No quotes, no trailing punctuation.
|
||||||
Examples: "Dark Crime Dramas With Moral Ambiguity", "Cozy British Mysteries With Quirky Detectives"`,
|
Examples: "Dark Crime Dramas With Moral Ambiguity", "Cozy British Mysteries With Quirky Detectives"`,
|
||||||
},
|
input: `Liked: ${JSON.stringify(interpreter.liked)}
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: `Liked: ${JSON.stringify(interpreter.liked)}
|
|
||||||
Themes: ${JSON.stringify(interpreter.themes)}
|
Themes: ${JSON.stringify(interpreter.themes)}
|
||||||
Tone: ${JSON.stringify(interpreter.tone)}
|
Tone: ${JSON.stringify(interpreter.tone)}
|
||||||
Character preferences: ${JSON.stringify(interpreter.character_preferences)}`,
|
Character preferences: ${JSON.stringify(interpreter.character_preferences)}`,
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (response.choices[0]?.message?.content ?? '').trim() || 'My Recommendation Session';
|
return (response.output_text ?? '').trim() || 'My Recommendation Session';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user