feature: new changes!
All checks were successful
Recommender Build and Deploy (internal) / Build Recommender Image (push) Successful in 3m59s
Recommender Build and Deploy (internal) / Deploy Recommender (internal) (push) Successful in 10s

This commit is contained in:
2026-03-25 20:09:32 -03:00
parent 26f61077b8
commit f9f3d95406
25 changed files with 964 additions and 696 deletions

View File

@@ -1,7 +1,7 @@
import { openai } from '../agent.js';
import type { InterpreterOutput, RetrievalOutput } from '../types/agents.js';
export async function runRetrieval(input: InterpreterOutput): Promise<RetrievalOutput> {
export async function runRetrieval(input: InterpreterOutput, brainstormCount = 100): Promise<RetrievalOutput> {
const response = await openai.chat.completions.create({
model: 'gpt-5.4',
temperature: 0.9,
@@ -10,7 +10,7 @@ export async function runRetrieval(input: InterpreterOutput): Promise<RetrievalO
messages: [
{
role: 'system',
content: `You are a TV show candidate generator. Your goal is to brainstorm a LARGE, DIVERSE pool of 6080 TV show candidates that match the user's structured preferences.
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.
Your output MUST be valid JSON matching this schema:
{
@@ -25,7 +25,7 @@ Rules:
- Each "reason" should briefly explain why the show matches the preferences
- Avoid duplicates
- Include shows from different decades, countries, and networks
- Aim for 6080 candidates minimum`,
- Aim for ${brainstormCount} candidates minimum`,
},
{
role: 'user',

View File

@@ -0,0 +1,28 @@
import { openai } from '../agent.js';
import type { InterpreterOutput } from '../types/agents.js';
export async function generateTitle(interpreter: InterpreterOutput): Promise<string> {
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
temperature: 0.7,
service_tier: 'flex',
messages: [
{
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.
Respond with ONLY the title. No quotes, no trailing punctuation.
Examples: "Dark Crime Dramas With Moral Ambiguity", "Cozy British Mysteries With Quirky Detectives"`,
},
{
role: 'user',
content: `Liked: ${JSON.stringify(interpreter.liked)}
Themes: ${JSON.stringify(interpreter.themes)}
Tone: ${JSON.stringify(interpreter.tone)}
Character preferences: ${JSON.stringify(interpreter.character_preferences)}`,
},
],
});
return (response.choices[0]?.message?.content ?? '').trim() || 'My Recommendation Session';
}

View File

@@ -8,6 +8,7 @@ export const recommendations = pgTable('recommendations', {
liked_shows: text('liked_shows').notNull().default(''),
disliked_shows: text('disliked_shows').notNull().default(''),
themes: text('themes').notNull().default(''),
brainstorm_count: integer('brainstorm_count').notNull().default(100),
recommendations: jsonb('recommendations').$type<CuratorOutput[]>(),
status: text('status').notNull().default('pending'),
created_at: timestamp('created_at').defaultNow().notNull(),

View File

@@ -6,6 +6,7 @@ import { runRetrieval } from '../agents/retrieval.js';
import { runRanking } from '../agents/ranking.js';
import { runCurator } from '../agents/curator.js';
import type { CuratorOutput, SSEEvent } from '../types/agents.js';
import { generateTitle } from '../agents/titleGenerator.js';
/* -- Agent pipeline --
[1] Interpreter -> gets user input, transforms into structured data
@@ -69,7 +70,7 @@ export async function runPipeline(
log(rec.id, 'Retrieval: start');
sseWrite({ stage: 'retrieval', status: 'start' });
const t1 = Date.now();
const retrievalOutput = await runRetrieval(interpreterOutput);
const retrievalOutput = await runRetrieval(interpreterOutput, rec.brainstorm_count);
log(rec.id, `Retrieval: done (${Date.now() - t1}ms) — ${retrievalOutput.candidates.length} candidates`, {
titles: retrievalOutput.candidates.map((c) => c.title),
});
@@ -98,11 +99,21 @@ export async function runPipeline(
log(rec.id, `Curator: done (${Date.now() - t3}ms) — ${curatorOutput.length} shows curated`);
sseWrite({ stage: 'curator', status: 'done', data: curatorOutput });
// Generate AI title
let aiTitle: string = rec.title;
try {
log(rec.id, 'Title generation: start');
aiTitle = await generateTitle(interpreterOutput);
log(rec.id, `Title generation: done — "${aiTitle}"`);
} catch (err) {
log(rec.id, `Title generation failed, keeping initial title: ${String(err)}`);
}
// Save results to DB
log(rec.id, 'Saving results to DB');
await db
.update(recommendations)
.set({ recommendations: curatorOutput, status: 'done' })
.set({ recommendations: curatorOutput, status: 'done', title: aiTitle })
.where(eq(recommendations.id, rec.id));
sseWrite({ stage: 'complete', status: 'done' });

View File

@@ -13,6 +13,7 @@ export default async function recommendationsRoute(fastify: FastifyInstance) {
liked_shows?: string;
disliked_shows?: string;
themes?: string;
brainstorm_count?: number;
};
const title = (body.main_prompt ?? '')
@@ -21,6 +22,9 @@ export default async function recommendationsRoute(fastify: FastifyInstance) {
.slice(0, 5)
.join(' ');
const rawCount = Number(body.brainstorm_count ?? 100);
const brainstorm_count = Number.isFinite(rawCount) ? Math.min(200, Math.max(50, rawCount)) : 100;
const [rec] = await db
.insert(recommendations)
.values({
@@ -29,6 +33,7 @@ export default async function recommendationsRoute(fastify: FastifyInstance) {
liked_shows: body.liked_shows ?? '',
disliked_shows: body.disliked_shows ?? '',
themes: body.themes ?? '',
brainstorm_count,
status: 'pending',
})
.returning({ id: recommendations.id });