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

@@ -0,0 +1,22 @@
CREATE TABLE "feedback" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tv_show_name" text NOT NULL,
"stars" integer NOT NULL,
"feedback" text DEFAULT '' NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "recommendations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" text NOT NULL,
"main_prompt" text NOT NULL,
"liked_shows" text DEFAULT '' NOT NULL,
"disliked_shows" text DEFAULT '' NOT NULL,
"themes" text DEFAULT '' NOT NULL,
"brainstorm_count" integer DEFAULT 100 NOT NULL,
"recommendations" jsonb,
"status" text DEFAULT 'pending' NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX "feedback_tv_show_name_idx" ON "feedback" USING btree ("tv_show_name");

View File

@@ -0,0 +1,161 @@
{
"id": "4ca81e42-26b0-46f5-988e-957360067861",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.feedback": {
"name": "feedback",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"tv_show_name": {
"name": "tv_show_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"stars": {
"name": "stars",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"feedback": {
"name": "feedback",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"feedback_tv_show_name_idx": {
"name": "feedback_tv_show_name_idx",
"columns": [
{
"expression": "tv_show_name",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.recommendations": {
"name": "recommendations",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"main_prompt": {
"name": "main_prompt",
"type": "text",
"primaryKey": false,
"notNull": true
},
"liked_shows": {
"name": "liked_shows",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"disliked_shows": {
"name": "disliked_shows",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"themes": {
"name": "themes",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"brainstorm_count": {
"name": "brainstorm_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 100
},
"recommendations": {
"name": "recommendations",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'pending'"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1774479321371,
"tag": "0000_wild_joseph",
"breakpoints": true
}
]
}

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 });