adding support for generic AI provider (OpenAI compatible)
This commit is contained in:
@@ -3,4 +3,14 @@
|
|||||||
# In production / Docker, supply these as environment variables.
|
# In production / Docker, supply these as environment variables.
|
||||||
|
|
||||||
DATABASE_URL=postgres://user:password@localhost:5432/recommender
|
DATABASE_URL=postgres://user:password@localhost:5432/recommender
|
||||||
|
|
||||||
|
# AI provider selection: OPENAI (default) or GENERIC
|
||||||
|
AI_PROVIDER=OPENAI
|
||||||
|
|
||||||
|
# OpenAI provider settings (used when AI_PROVIDER=OPENAI)
|
||||||
OPENAI_API_KEY=your-openai-api-key-here
|
OPENAI_API_KEY=your-openai-api-key-here
|
||||||
|
|
||||||
|
# Generic provider settings (used when AI_PROVIDER=GENERIC)
|
||||||
|
PROVIDER_URL=https://your-provider.example.com/v1
|
||||||
|
BEARER_TOKEN=your-bearer-token
|
||||||
|
MODEL_NAME=your-model-name
|
||||||
|
|||||||
@@ -2,9 +2,22 @@ import OpenAI from 'openai';
|
|||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from 'dotenv';
|
||||||
dotenv.config({ path: ['.env.local', '.env'] });
|
dotenv.config({ path: ['.env.local', '.env'] });
|
||||||
|
|
||||||
export const openai = new OpenAI({
|
const AI_PROVIDER = process.env.AI_PROVIDER ?? 'OPENAI';
|
||||||
apiKey: process.env.OPENAI_API_KEY,
|
const isGeneric = AI_PROVIDER === 'GENERIC';
|
||||||
});
|
|
||||||
|
export const openai = isGeneric
|
||||||
|
? new OpenAI({
|
||||||
|
apiKey: process.env.BEARER_TOKEN,
|
||||||
|
baseURL: process.env.PROVIDER_URL,
|
||||||
|
})
|
||||||
|
: new OpenAI({
|
||||||
|
apiKey: process.env.OPENAI_API_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const defaultModel = isGeneric ? (process.env.MODEL_NAME ?? 'default') : 'gpt-5.4';
|
||||||
|
export const miniModel = isGeneric ? (process.env.MODEL_NAME ?? 'default') : 'gpt-5.4-mini';
|
||||||
|
export const serviceOptions = isGeneric ? {} : { service_tier: 'flex' as const };
|
||||||
|
export const supportsWebSearch = !isGeneric;
|
||||||
|
|
||||||
export async function askAgent(prompt: string) {
|
export async function askAgent(prompt: string) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { openai } from '../agent.js';
|
import { openai, defaultModel, serviceOptions, supportsWebSearch } 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';
|
||||||
@@ -32,11 +32,12 @@ export async function runCurator(
|
|||||||
.map((s) => `- "${s.title}" (${s.category})`)
|
.map((s) => `- "${s.title}" (${s.category})`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
|
const canSearch = useWebSearch && supportsWebSearch;
|
||||||
const response = await openai.responses.parse({
|
const response = await openai.responses.parse({
|
||||||
model: 'gpt-5.4',
|
model: defaultModel,
|
||||||
temperature: 0.5,
|
temperature: 0.5,
|
||||||
service_tier: 'flex',
|
...serviceOptions,
|
||||||
...(useWebSearch ? { tools: [{ type: 'web_search' as const }] } : {}),
|
...(canSearch ? { tools: [{ type: 'web_search' as const }] } : {}),
|
||||||
text: { format: zodTextFormat(CuratorSchema, "shows") },
|
text: { format: zodTextFormat(CuratorSchema, "shows") },
|
||||||
instructions: `You are a ${mediaLabel} recommendation curator. For each ${mediaLabel}, write a concise 1-2 sentence explanation of why it was assigned to its category based on the user's preferences.${useWebSearch ? '\n\nUse web search to verify details and enrich explanations with accurate information.' : ''}
|
instructions: `You are a ${mediaLabel} recommendation curator. For each ${mediaLabel}, write a concise 1-2 sentence explanation of why it was assigned to its category based on the user's preferences.${useWebSearch ? '\n\nUse web search to verify details and enrich explanations with accurate information.' : ''}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { openai } from '../agent.js';
|
import { openai, defaultModel, serviceOptions } 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,9 +28,9 @@ export async function runInterpreter(input: InterpreterInput): Promise<Interpret
|
|||||||
: '';
|
: '';
|
||||||
|
|
||||||
const response = await openai.responses.parse({
|
const response = await openai.responses.parse({
|
||||||
model: 'gpt-5.4',
|
model: defaultModel,
|
||||||
temperature: 0.2,
|
temperature: 0.2,
|
||||||
service_tier: 'flex',
|
...serviceOptions,
|
||||||
text: { format: zodTextFormat(InterpreterSchema, "preferences") },
|
text: { format: zodTextFormat(InterpreterSchema, "preferences") },
|
||||||
instructions: `You are a ${mediaLabel} preference interpreter. Transform raw user input into structured, normalized preferences.
|
instructions: `You are a ${mediaLabel} preference interpreter. Transform raw user input into structured, normalized preferences.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { openai } from '../agent.js';
|
import { openai, defaultModel, serviceOptions } 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';
|
||||||
@@ -42,9 +42,9 @@ export async function runRanking(
|
|||||||
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 openai.responses.parse({
|
||||||
model: 'gpt-5.4',
|
model: defaultModel,
|
||||||
temperature: 0.2,
|
temperature: 0.2,
|
||||||
service_tier: 'flex',
|
...serviceOptions,
|
||||||
text: { format: zodTextFormat(RankingSchema, "ranking") },
|
text: { format: zodTextFormat(RankingSchema, "ranking") },
|
||||||
instructions: `You are a ${mediaLabel} ranking critic. Assign each ${mediaLabel} to exactly one of four confidence buckets based on how well it matches the user's preferences.
|
instructions: `You are a ${mediaLabel} ranking critic. Assign each ${mediaLabel} to exactly one of four confidence buckets based on how well it matches the user's preferences.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { openai } from '../agent.js';
|
import { openai, defaultModel, serviceOptions, supportsWebSearch } 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';
|
||||||
@@ -19,11 +19,12 @@ export async function runRetrieval(
|
|||||||
const mediaLabel = mediaType === 'movie' ? 'movie' : 'TV show';
|
const mediaLabel = mediaType === 'movie' ? 'movie' : 'TV show';
|
||||||
const mediaLabelPlural = mediaType === 'movie' ? 'movies' : 'TV shows';
|
const mediaLabelPlural = mediaType === 'movie' ? 'movies' : 'TV shows';
|
||||||
|
|
||||||
|
const canSearch = useWebSearch && supportsWebSearch;
|
||||||
const response = await openai.responses.parse({
|
const response = await openai.responses.parse({
|
||||||
model: 'gpt-5.4',
|
model: defaultModel,
|
||||||
temperature: 0.9,
|
temperature: 0.9,
|
||||||
service_tier: 'flex',
|
...serviceOptions,
|
||||||
...(useWebSearch ? { tools: [{ type: 'web_search' as const }] } : {}),
|
...(canSearch ? { tools: [{ type: 'web_search' as const }] } : {}),
|
||||||
text: { format: zodTextFormat(RetrievalSchema, "candidates") },
|
text: { format: zodTextFormat(RetrievalSchema, "candidates") },
|
||||||
instructions: `You are a ${mediaLabel} candidate generator. Your goal is to brainstorm a LARGE, DIVERSE pool of ${brainstormCount} ${mediaLabel} candidates that match the user's structured preferences.${useWebSearch ? '\n\nUse web search to find recent and accurate titles, including newer releases.' : ''}
|
instructions: `You are a ${mediaLabel} candidate generator. Your goal is to brainstorm a LARGE, DIVERSE pool of ${brainstormCount} ${mediaLabel} candidates that match the user's structured preferences.${useWebSearch ? '\n\nUse web search to find recent and accurate titles, including newer releases.' : ''}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { openai } from '../agent.js';
|
import { openai, miniModel, serviceOptions } from '../agent.js';
|
||||||
import type { InterpreterOutput, MediaType } from '../types/agents.js';
|
import type { InterpreterOutput, MediaType } from '../types/agents.js';
|
||||||
|
|
||||||
export async function generateTitle(interpreter: InterpreterOutput, mediaType: MediaType = 'tv_show'): Promise<string> {
|
export async function generateTitle(interpreter: InterpreterOutput, mediaType: MediaType = 'tv_show'): Promise<string> {
|
||||||
const mediaLabel = mediaType === 'movie' ? 'movie' : 'TV show';
|
const mediaLabel = mediaType === 'movie' ? 'movie' : 'TV show';
|
||||||
|
|
||||||
const response = await openai.responses.create({
|
const response = await openai.responses.create({
|
||||||
model: 'gpt-5.4-mini',
|
model: miniModel,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
service_tier: 'flex',
|
...serviceOptions,
|
||||||
instructions: `Generate a concise 5-8 word title for a ${mediaLabel} recommendation session.
|
instructions: `Generate a concise 5-8 word title for a ${mediaLabel} 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user