feature: new changes!
This commit is contained in:
22
packages/backend/drizzle/0000_wild_joseph.sql
Normal file
22
packages/backend/drizzle/0000_wild_joseph.sql
Normal 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");
|
||||
161
packages/backend/drizzle/meta/0000_snapshot.json
Normal file
161
packages/backend/drizzle/meta/0000_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
13
packages/backend/drizzle/meta/_journal.json
Normal file
13
packages/backend/drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1774479321371,
|
||||
"tag": "0000_wild_joseph",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 60–80 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 60–80 candidates minimum`,
|
||||
- Aim for ${brainstormCount} candidates minimum`,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
|
||||
28
packages/backend/src/agents/titleGenerator.ts
Normal file
28
packages/backend/src/agents/titleGenerator.ts
Normal 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';
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user