mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-02 02:19:43 +00:00
Update
This commit is contained in:
1 parent
8b73781272
commit
75509b0e45
41 files changed
+5896
-2931
No files matched your search
@@ -0,0 +1,115 @@
|
||||
// =============================================================================
|
||||
// Category Generation Functions
|
||||
// Generate category names and category ideas
|
||||
// =============================================================================
|
||||
|
||||
import { getOpenAI, getModel } from '../client';
|
||||
import type { GeneratedCategoryIdea } from '../types';
|
||||
|
||||
/**
|
||||
* Generate a single category
|
||||
*/
|
||||
export async function generateCategory(): Promise<{ name: string; description: string; icon: string }> {
|
||||
const openai = getOpenAI();
|
||||
const prompt = `Generate a unique category for organizing activity packs in a couples' intimacy/connection app.
|
||||
|
||||
This is an ACTIVITY-FOCUSED app where couples swipe on activity proposals.
|
||||
Each partner independently swipes Like/Dislike/Maybe, and when both swipe positively they "match".
|
||||
Categories organize collections of activity packs (date ideas, intimate experiences, adventures, challenges, etc.).
|
||||
This is NOT a Q&A app - it's about discovering shared interests in activities.
|
||||
|
||||
Return a JSON object with:
|
||||
- name: Category name (1-3 words, e.g., "Romance", "Adventure", "Date Nights")
|
||||
- description: Brief description of what activity packs in this category contain (1 sentence)
|
||||
- icon: A single emoji that represents this category
|
||||
|
||||
Be creative and think of categories that help couples explore different types of activities together.`;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: getModel('generate'),
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'You are a creative content organizer for a couples activity/intimacy app. The app helps couples discover shared interests by swiping on activity proposals. Generate engaging category ideas for organizing activity packs. Always respond with valid JSON only.',
|
||||
},
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
response_format: { type: 'json_object' },
|
||||
temperature: 0.9,
|
||||
});
|
||||
|
||||
const content = response.choices[0].message.content;
|
||||
if (!content) throw new Error('No content generated');
|
||||
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest multiple category ideas
|
||||
*/
|
||||
export async function suggestCategories(
|
||||
existingCategories: string[],
|
||||
explicit: boolean,
|
||||
crudeLang: boolean = false,
|
||||
inspiration?: string,
|
||||
previousSuggestions?: string[]
|
||||
): Promise<GeneratedCategoryIdea[]> {
|
||||
const openai = getOpenAI();
|
||||
const existingList = existingCategories.length > 0
|
||||
? existingCategories.join(', ')
|
||||
: 'None';
|
||||
|
||||
const explicitInstruction = explicit
|
||||
? 'Include bold, spicy, and explicitly intimate categories (NSFW is allowed).'
|
||||
: 'Do NOT include any explicit or NSFW themes. Keep it romantic, emotional, playful, and clean.';
|
||||
|
||||
const crudeLangInstruction = crudeLang
|
||||
? '\nCRUDE LANGUAGE OVERRIDE: Use crude, vulgar terms in the names and descriptions.'
|
||||
: '';
|
||||
|
||||
const inspirationInstruction = inspiration
|
||||
? `\n\nINSPIRATION/GUIDANCE FROM ADMIN:\n${inspiration}\n\nUse the above inspiration to guide your category suggestions.`
|
||||
: '';
|
||||
|
||||
const avoidInstruction = previousSuggestions && previousSuggestions.length > 0
|
||||
? `\n\nIMPORTANT - DO NOT suggest any of these category names (already suggested): ${previousSuggestions.join(', ')}\nGenerate completely different and fresh ideas.`
|
||||
: '';
|
||||
|
||||
const prompt = `Here are the current categories in our couples' activity/intimacy app: ${existingList}.
|
||||
|
||||
IMPORTANT - HOW THE APP WORKS:
|
||||
- This is an ACTIVITY-FOCUSED app where couples swipe on activity proposals
|
||||
- Each partner independently swipes Like/Dislike/Maybe on activities
|
||||
- When BOTH partners swipe positively, they "match" and can discuss the activity
|
||||
- Categories organize collections of activity packs (date ideas, intimate experiences, adventures, etc.)
|
||||
- This is NOT a Q&A app - it's about discovering shared interests in activities to do together
|
||||
|
||||
Suggest 5 NEW, UNIQUE category ideas that differ from the existing ones.
|
||||
${explicitInstruction}${crudeLangInstruction}${inspirationInstruction}${avoidInstruction}
|
||||
|
||||
Return a JSON object with an "ideas" array containing 5 objects, where each object has:
|
||||
- name: Category name (1-3 words)
|
||||
- description: Brief description of what activity packs in this category contain (1 sentence)
|
||||
- icon: A single descriptive emoji
|
||||
|
||||
Focus on diverse activity themes like: date ideas, adventures, intimate experiences, bonding activities, challenges, travel, home activities, etc.`;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: getModel('generate'),
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'You are a creative content strategist for a couples activity/intimacy app. The app helps couples discover shared interests by swiping on activity proposals. Generate engaging, diverse category ideas for organizing activity packs. Always respond with valid JSON only.',
|
||||
},
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
response_format: { type: 'json_object' },
|
||||
temperature: 0.9,
|
||||
});
|
||||
|
||||
const content = response.choices[0].message.content;
|
||||
if (!content) throw new Error('No content generated');
|
||||
|
||||
const parsed = JSON.parse(content);
|
||||
return parsed.ideas as GeneratedCategoryIdea[];
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
// =============================================================================
|
||||
// Cherry-Pick Mode Functions
|
||||
// Pool questions from multiple generators and select the best individuals
|
||||
// =============================================================================
|
||||
|
||||
import { getOpenAI } from '../client';
|
||||
import { getCouncilConfig, REVIEW_GUIDELINES, TONE_LEVELS } from '../config';
|
||||
import type {
|
||||
GeneratedQuestion,
|
||||
QuestionReview,
|
||||
ReviewResult,
|
||||
ToneLevel,
|
||||
GenerationCandidate,
|
||||
PooledQuestion,
|
||||
CherryPickEvaluation,
|
||||
CherryPickResult,
|
||||
CouncilConfig,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* Evaluate all questions in the pool and identify duplicates
|
||||
*/
|
||||
export async function evaluateQuestionPool(
|
||||
pooledQuestions: PooledQuestion[],
|
||||
packContext: { name: string; description?: string | null; isExplicit: boolean; tone: ToneLevel },
|
||||
requestedCount: number
|
||||
): Promise<CherryPickEvaluation[]> {
|
||||
const openai = getOpenAI();
|
||||
const config = getCouncilConfig();
|
||||
|
||||
const toneDescription = TONE_LEVELS.find(t => t.level === packContext.tone)?.label || 'Unknown';
|
||||
|
||||
const questionsForEval = pooledQuestions.map((q, i) => ({
|
||||
index: i,
|
||||
text: q.text,
|
||||
partner_text: q.partner_text || null,
|
||||
intensity: q.intensity,
|
||||
generatorIndex: q.sourceGeneratorIndex,
|
||||
}));
|
||||
|
||||
const prompt = `You are evaluating a pool of ${pooledQuestions.length} questions from ${new Set(pooledQuestions.map(q => q.sourceGeneratorIndex)).size} different AI generators.
|
||||
|
||||
Your task is to:
|
||||
1. Score each question individually (1-10 on each criterion)
|
||||
2. Identify duplicates/semantically similar questions
|
||||
3. Assess uniqueness relative to the pool
|
||||
4. Consider pack theme fit
|
||||
|
||||
PACK CONTEXT:
|
||||
- Pack Name: "${packContext.name}"
|
||||
${packContext.description ? `- Pack Description: "${packContext.description}"` : ''}
|
||||
- Content Type: ${packContext.isExplicit ? 'EXPLICIT (adult content allowed)' : 'NON-EXPLICIT (clean/romantic only)'}
|
||||
- Tone Level: ${packContext.tone} (${toneDescription})
|
||||
- Requested question count: ${requestedCount}
|
||||
|
||||
${REVIEW_GUIDELINES}
|
||||
|
||||
ADDITIONAL CRITERIA FOR CHERRY-PICK:
|
||||
|
||||
5. UNIQUENESS (1-10):
|
||||
- How unique is this question compared to others in the pool?
|
||||
- 10: Completely unique concept
|
||||
- 7-9: Somewhat similar theme but distinct execution
|
||||
- 4-6: Similar to another question but different enough to keep
|
||||
- 1-3: Near-duplicate of another question
|
||||
|
||||
DUPLICATE DETECTION:
|
||||
Two questions are duplicates if they:
|
||||
- Describe the same core activity/action
|
||||
- Differ only in minor phrasing (e.g., "Have sex outdoors" vs "Make love outside")
|
||||
- Would feel repetitive if both appeared in the same pack
|
||||
|
||||
For duplicates, keep the version with:
|
||||
1. Better phrasing/creativity
|
||||
2. More appropriate intensity grading
|
||||
3. Better partner_text (if applicable)
|
||||
|
||||
VERDICT RULES:
|
||||
- SELECT: High quality (avg score >= 7), no major issues - should be included
|
||||
- CONSIDER: Medium quality (avg score 5-7) - include if needed to fill count
|
||||
- SKIP: Low quality (avg score < 5) OR is a duplicate of a better question
|
||||
|
||||
QUESTIONS TO EVALUATE:
|
||||
${JSON.stringify(questionsForEval, null, 2)}
|
||||
|
||||
Return a JSON object with:
|
||||
{
|
||||
"evaluations": [
|
||||
{
|
||||
"questionIndex": 0,
|
||||
"overallScore": 82,
|
||||
"scores": {
|
||||
"guidelineCompliance": 8,
|
||||
"creativity": 9,
|
||||
"clarity": 8,
|
||||
"intensityAccuracy": 8,
|
||||
"uniqueness": 9
|
||||
},
|
||||
"isDuplicate": false,
|
||||
"duplicateOf": null,
|
||||
"issues": ["Minor issue 1"],
|
||||
"verdict": "select"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Evaluate ALL ${pooledQuestions.length} questions.`;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: config.reviewerModel,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'You are a quality reviewer for a couples intimacy app. You evaluate and compare questions from multiple AI generators to select the best ones. Be thorough and fair. Always respond with valid JSON only.',
|
||||
},
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
response_format: { type: 'json_object' },
|
||||
temperature: 0.3,
|
||||
});
|
||||
|
||||
const content = response.choices[0].message.content;
|
||||
if (!content) throw new Error('No evaluation content generated');
|
||||
|
||||
// Strip markdown code blocks if present
|
||||
let jsonContent = content.trim();
|
||||
if (jsonContent.startsWith('```')) {
|
||||
jsonContent = jsonContent.replace(/^```(?:json)?\s*\n?/, '').replace(/\n?```\s*$/, '');
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonContent);
|
||||
return parsed.evaluations || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Select top N questions from evaluations, optionally balancing intensity levels
|
||||
*/
|
||||
export function selectTopQuestions(
|
||||
pooledQuestions: PooledQuestion[],
|
||||
evaluations: CherryPickEvaluation[],
|
||||
requestedCount: number,
|
||||
options: {
|
||||
ensureIntensityDistribution: boolean;
|
||||
requestedIntensity?: number;
|
||||
}
|
||||
): CherryPickResult {
|
||||
// Step 1: Filter out duplicates (keep only the original, not the duplicate)
|
||||
const duplicateIndices = new Set<number>();
|
||||
for (const evaluation of evaluations) {
|
||||
if (evaluation.isDuplicate && evaluation.duplicateOf !== undefined) {
|
||||
duplicateIndices.add(evaluation.questionIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Create scored list of non-duplicate questions
|
||||
const scoredQuestions = evaluations
|
||||
.filter(e => !duplicateIndices.has(e.questionIndex))
|
||||
.map(e => ({
|
||||
evaluation: e,
|
||||
question: pooledQuestions[e.questionIndex],
|
||||
// Weighted overall score
|
||||
weightedScore: (
|
||||
(e.scores.guidelineCompliance * 0.25) +
|
||||
(e.scores.creativity * 0.20) +
|
||||
(e.scores.clarity * 0.20) +
|
||||
(e.scores.intensityAccuracy * 0.15) +
|
||||
(e.scores.uniqueness * 0.20)
|
||||
) * 10,
|
||||
}))
|
||||
.sort((a, b) => b.weightedScore - a.weightedScore);
|
||||
|
||||
let selected: typeof scoredQuestions = [];
|
||||
|
||||
// Step 3: Select questions
|
||||
if (options.requestedIntensity) {
|
||||
// If specific intensity requested, just take top N of that intensity
|
||||
const matchingIntensity = scoredQuestions.filter(
|
||||
sq => sq.question.intensity === options.requestedIntensity
|
||||
);
|
||||
selected = matchingIntensity.slice(0, requestedCount);
|
||||
} else if (options.ensureIntensityDistribution) {
|
||||
// Balance across intensity levels
|
||||
const perIntensity = Math.ceil(requestedCount / 5);
|
||||
const byIntensity: Record<number, typeof scoredQuestions> = { 1: [], 2: [], 3: [], 4: [], 5: [] };
|
||||
|
||||
for (const sq of scoredQuestions) {
|
||||
const intensity = sq.question.intensity;
|
||||
if (intensity >= 1 && intensity <= 5) {
|
||||
byIntensity[intensity].push(sq);
|
||||
}
|
||||
}
|
||||
|
||||
// Take top N from each intensity level
|
||||
for (let intensity = 1; intensity <= 5; intensity++) {
|
||||
const toTake = byIntensity[intensity].slice(0, perIntensity);
|
||||
selected.push(...toTake);
|
||||
}
|
||||
|
||||
// If we still need more, fill from remaining top-scored questions
|
||||
if (selected.length < requestedCount) {
|
||||
const selectedIndices = new Set(selected.map(s => s.evaluation.questionIndex));
|
||||
const remaining = scoredQuestions.filter(
|
||||
sq => !selectedIndices.has(sq.evaluation.questionIndex)
|
||||
);
|
||||
selected.push(...remaining.slice(0, requestedCount - selected.length));
|
||||
}
|
||||
|
||||
// Sort final selection by score
|
||||
selected.sort((a, b) => b.weightedScore - a.weightedScore);
|
||||
selected = selected.slice(0, requestedCount);
|
||||
} else {
|
||||
// Simple top N by score
|
||||
selected = scoredQuestions.slice(0, requestedCount);
|
||||
}
|
||||
|
||||
// Step 4: Build result
|
||||
const selectedQuestions = selected.map(s => s.question);
|
||||
|
||||
// Calculate intensity distribution
|
||||
const intensityDistribution: Record<number, number> = {};
|
||||
for (const q of selectedQuestions) {
|
||||
intensityDistribution[q.intensity] = (intensityDistribution[q.intensity] || 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
selectedQuestions,
|
||||
evaluations,
|
||||
poolSize: pooledQuestions.length,
|
||||
duplicatesRemoved: duplicateIndices.size,
|
||||
intensityDistribution,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform cherry-pick selection: pool all questions, evaluate, and select best
|
||||
*/
|
||||
export async function performCherryPickSelection(
|
||||
candidates: GenerationCandidate[],
|
||||
packContext: { name: string; description?: string | null; isExplicit: boolean; tone: ToneLevel },
|
||||
requestedCount: number,
|
||||
config: CouncilConfig
|
||||
): Promise<{
|
||||
questions: GeneratedQuestion[];
|
||||
reviews: QuestionReview[];
|
||||
summary: ReviewResult['summary'];
|
||||
cherryPickResult: CherryPickResult;
|
||||
}> {
|
||||
// Step 1: Pool all questions with provenance
|
||||
const pooledQuestions: PooledQuestion[] = [];
|
||||
for (const candidate of candidates) {
|
||||
for (let qi = 0; qi < candidate.questions.length; qi++) {
|
||||
pooledQuestions.push({
|
||||
...candidate.questions[qi],
|
||||
sourceGeneratorIndex: candidate.generatorIndex,
|
||||
sourceGeneratorModel: candidate.generatorModel,
|
||||
sourceQuestionIndex: qi,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Evaluate entire pool
|
||||
const evaluations = await evaluateQuestionPool(pooledQuestions, packContext, requestedCount);
|
||||
|
||||
// Step 3: Select top questions
|
||||
const cherryPickResult = selectTopQuestions(pooledQuestions, evaluations, requestedCount, {
|
||||
ensureIntensityDistribution: config.cherryPickEnsureIntensityDistribution,
|
||||
requestedIntensity: undefined,
|
||||
});
|
||||
|
||||
// Step 4: Convert to standard review format for UI compatibility
|
||||
const reviews: QuestionReview[] = cherryPickResult.selectedQuestions.map((q, i) => {
|
||||
// Find the evaluation for this question
|
||||
const poolIndex = pooledQuestions.findIndex(
|
||||
pq => pq.text === q.text && pq.sourceGeneratorIndex === q.sourceGeneratorIndex
|
||||
);
|
||||
const evaluation = evaluations.find(e => e.questionIndex === poolIndex);
|
||||
|
||||
if (!evaluation) {
|
||||
return {
|
||||
index: i,
|
||||
verdict: 'pass' as const,
|
||||
issues: [],
|
||||
scores: { guidelineCompliance: 7, creativity: 7, clarity: 7, intensityAccuracy: 7 },
|
||||
};
|
||||
}
|
||||
|
||||
// Map cherry-pick verdict to standard verdict
|
||||
const verdict: 'pass' | 'flag' | 'reject' =
|
||||
evaluation.verdict === 'select' ? 'pass' :
|
||||
evaluation.verdict === 'consider' ? 'flag' : 'reject';
|
||||
|
||||
return {
|
||||
index: i,
|
||||
verdict,
|
||||
issues: evaluation.issues,
|
||||
scores: {
|
||||
guidelineCompliance: evaluation.scores.guidelineCompliance,
|
||||
creativity: evaluation.scores.creativity,
|
||||
clarity: evaluation.scores.clarity,
|
||||
intensityAccuracy: evaluation.scores.intensityAccuracy,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Step 5: Calculate summary
|
||||
const passed = reviews.filter(r => r.verdict === 'pass').length;
|
||||
const flagged = reviews.filter(r => r.verdict === 'flag').length;
|
||||
const rejected = reviews.filter(r => r.verdict === 'reject').length;
|
||||
|
||||
let totalScore = 0;
|
||||
let scoreCount = 0;
|
||||
for (const review of reviews) {
|
||||
if (review.scores) {
|
||||
totalScore += review.scores.guidelineCompliance || 0;
|
||||
totalScore += review.scores.creativity || 0;
|
||||
totalScore += review.scores.clarity || 0;
|
||||
totalScore += review.scores.intensityAccuracy || 0;
|
||||
scoreCount += 4;
|
||||
}
|
||||
}
|
||||
const overallQuality = scoreCount > 0 ? Math.round((totalScore / scoreCount) * 10) / 10 : 0;
|
||||
|
||||
return {
|
||||
questions: cherryPickResult.selectedQuestions,
|
||||
reviews,
|
||||
summary: { passed, flagged, rejected, overallQuality },
|
||||
cherryPickResult,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
// =============================================================================
|
||||
// Council Generation
|
||||
// Multi-model generation with review and selection
|
||||
// =============================================================================
|
||||
|
||||
import { getModel, getShortModelName } from '../client';
|
||||
import { getCouncilConfig } from '../config';
|
||||
import { generateQuestions, generateQuestionsWithModel } from './questions';
|
||||
import { selectBestGeneration } from '../review';
|
||||
import { performCherryPickSelection } from './cherry-pick';
|
||||
import type {
|
||||
ToneLevel,
|
||||
GenerationProgress,
|
||||
ProgressCallback,
|
||||
GeneratorStatus,
|
||||
GenerationCandidate,
|
||||
CouncilGenerationResult,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* Generate questions using the council system
|
||||
* Runs multiple generators in parallel and uses a reviewer to select the best output
|
||||
*/
|
||||
export async function generateQuestionsWithCouncil(
|
||||
packName: string,
|
||||
count: number = 10,
|
||||
intensity?: number,
|
||||
tone: ToneLevel = 3,
|
||||
packDescription?: string,
|
||||
existingQuestions?: string[],
|
||||
crudeLang: boolean = false,
|
||||
inspiration?: string,
|
||||
onProgress?: ProgressCallback
|
||||
): Promise<CouncilGenerationResult> {
|
||||
const config = getCouncilConfig();
|
||||
|
||||
// Helper to build progress update
|
||||
const buildProgress = (
|
||||
phase: GenerationProgress['phase'],
|
||||
generatorStatuses: GeneratorStatus[],
|
||||
generatorResults: (GenerationCandidate | null)[],
|
||||
reviewerStatus?: 'pending' | 'reviewing' | 'completed'
|
||||
): GenerationProgress => {
|
||||
const completedCount = generatorStatuses.filter(s => s === 'completed').length;
|
||||
const totalCount = config.generators.length;
|
||||
|
||||
let message = '';
|
||||
if (phase === 'generating') {
|
||||
if (completedCount === 0) {
|
||||
message = `Generating with ${totalCount} model${totalCount > 1 ? 's' : ''}...`;
|
||||
} else if (completedCount < totalCount) {
|
||||
message = `${completedCount}/${totalCount} generators complete...`;
|
||||
} else {
|
||||
message = 'All generators complete';
|
||||
}
|
||||
} else if (phase === 'reviewing') {
|
||||
message = 'Reviewing and selecting best questions...';
|
||||
} else {
|
||||
message = 'Complete';
|
||||
}
|
||||
|
||||
return {
|
||||
phase,
|
||||
generators: config.generators.map((gen, i) => ({
|
||||
model: gen.model,
|
||||
shortName: getShortModelName(gen.model),
|
||||
status: generatorStatuses[i],
|
||||
questionCount: generatorResults[i]?.questions.length,
|
||||
timeMs: generatorResults[i]?.generationTime,
|
||||
})),
|
||||
reviewer: config.enabled ? {
|
||||
model: config.reviewerModel,
|
||||
shortName: getShortModelName(config.reviewerModel),
|
||||
status: reviewerStatus || 'pending',
|
||||
} : undefined,
|
||||
message,
|
||||
};
|
||||
};
|
||||
|
||||
// If council is disabled, use the default generation model
|
||||
if (!config.enabled) {
|
||||
const defaultModel = getModel('generate');
|
||||
onProgress?.({
|
||||
phase: 'generating',
|
||||
generators: [{ model: defaultModel, shortName: getShortModelName(defaultModel), status: 'generating' }],
|
||||
message: 'Generating questions...',
|
||||
});
|
||||
|
||||
const startGen = Date.now();
|
||||
const questions = await generateQuestions(
|
||||
packName,
|
||||
count,
|
||||
intensity,
|
||||
tone,
|
||||
packDescription,
|
||||
existingQuestions,
|
||||
crudeLang,
|
||||
inspiration
|
||||
);
|
||||
const genTime = Date.now() - startGen;
|
||||
|
||||
onProgress?.({
|
||||
phase: 'complete',
|
||||
generators: [{ model: defaultModel, shortName: getShortModelName(defaultModel), status: 'completed', questionCount: questions.length, timeMs: genTime }],
|
||||
message: 'Complete',
|
||||
});
|
||||
|
||||
return {
|
||||
questions,
|
||||
reviews: [],
|
||||
summary: null,
|
||||
selectedGeneratorIndex: null,
|
||||
allCandidates: null,
|
||||
selectionMode: 'whole_set',
|
||||
metadata: {
|
||||
generatorModels: [getModel('generate')],
|
||||
reviewerModel: null,
|
||||
totalGenerationTime: genTime,
|
||||
reviewTime: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize tracking for parallel generation
|
||||
const generatorStatuses: GeneratorStatus[] = config.generators.map(() => 'pending');
|
||||
const generatorResults: (GenerationCandidate | null)[] = config.generators.map(() => null);
|
||||
|
||||
// Initial progress - all generators starting
|
||||
generatorStatuses.forEach((_, i) => { generatorStatuses[i] = 'generating'; });
|
||||
onProgress?.(buildProgress('generating', generatorStatuses, generatorResults));
|
||||
|
||||
// Step 1: Generate questions with all configured generators in parallel
|
||||
const startGen = Date.now();
|
||||
const generatorPromises = config.generators.map(async (gen, index) => {
|
||||
const genStart = Date.now();
|
||||
try {
|
||||
const questions = await generateQuestionsWithModel(
|
||||
gen.model,
|
||||
packName,
|
||||
count,
|
||||
intensity,
|
||||
tone,
|
||||
packDescription,
|
||||
existingQuestions,
|
||||
crudeLang,
|
||||
inspiration
|
||||
);
|
||||
const result = {
|
||||
generatorIndex: index,
|
||||
generatorModel: gen.model,
|
||||
questions,
|
||||
generationTime: Date.now() - genStart,
|
||||
} as GenerationCandidate;
|
||||
|
||||
// Update status and notify
|
||||
generatorStatuses[index] = 'completed';
|
||||
generatorResults[index] = result;
|
||||
onProgress?.(buildProgress('generating', generatorStatuses, generatorResults));
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Generator ${index} (${gen.model}) failed:`, error);
|
||||
generatorStatuses[index] = 'failed';
|
||||
onProgress?.(buildProgress('generating', generatorStatuses, generatorResults));
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(generatorPromises);
|
||||
const successfulCandidates = results.filter((r): r is GenerationCandidate => r !== null);
|
||||
const totalGenerationTime = Date.now() - startGen;
|
||||
|
||||
// If no generators succeeded, throw error
|
||||
if (successfulCandidates.length === 0) {
|
||||
throw new Error('All generators failed to produce results');
|
||||
}
|
||||
|
||||
// Step 2: Selection based on mode
|
||||
const startReview = Date.now();
|
||||
const isExplicit = tone >= 4;
|
||||
const packContext = { name: packName, description: packDescription, isExplicit, tone };
|
||||
|
||||
// Notify that we're entering review phase
|
||||
onProgress?.(buildProgress('reviewing', generatorStatuses, generatorResults, 'reviewing'));
|
||||
|
||||
// Branch based on selection mode
|
||||
if (config.selectionMode === 'cherry_pick') {
|
||||
// Cherry-pick mode: pool all questions and select best individuals
|
||||
try {
|
||||
const cherryPickResult = await performCherryPickSelection(
|
||||
successfulCandidates,
|
||||
packContext,
|
||||
count,
|
||||
config
|
||||
);
|
||||
const reviewTime = Date.now() - startReview;
|
||||
|
||||
// Calculate total pooled questions
|
||||
const pooledCount = successfulCandidates.reduce((sum, c) => sum + c.questions.length, 0);
|
||||
|
||||
// Notify completion
|
||||
onProgress?.(buildProgress('complete', generatorStatuses, generatorResults, 'completed'));
|
||||
|
||||
return {
|
||||
questions: cherryPickResult.questions,
|
||||
reviews: cherryPickResult.reviews,
|
||||
summary: cherryPickResult.summary,
|
||||
selectedGeneratorIndex: null, // Not applicable for cherry-pick
|
||||
allCandidates: successfulCandidates,
|
||||
cherryPickResult: cherryPickResult.cherryPickResult,
|
||||
selectionMode: 'cherry_pick',
|
||||
metadata: {
|
||||
generatorModels: config.generators.map(g => g.model),
|
||||
reviewerModel: config.reviewerModel,
|
||||
totalGenerationTime,
|
||||
reviewTime,
|
||||
pooledQuestionCount: pooledCount,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
// If cherry-pick fails, fall back to first generation
|
||||
console.error('Cherry-pick selection failed:', error);
|
||||
const firstCandidate = successfulCandidates[0];
|
||||
|
||||
onProgress?.(buildProgress('complete', generatorStatuses, generatorResults, 'completed'));
|
||||
|
||||
return {
|
||||
questions: firstCandidate.questions,
|
||||
reviews: [],
|
||||
summary: null,
|
||||
selectedGeneratorIndex: 0,
|
||||
allCandidates: successfulCandidates,
|
||||
selectionMode: 'cherry_pick',
|
||||
metadata: {
|
||||
generatorModels: config.generators.map(g => g.model),
|
||||
reviewerModel: null,
|
||||
totalGenerationTime,
|
||||
reviewTime: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Whole-set mode: select best complete set from one generator
|
||||
try {
|
||||
const selectionResult = await selectBestGeneration(successfulCandidates, packContext);
|
||||
const reviewTime = Date.now() - startReview;
|
||||
|
||||
const selectedCandidate = successfulCandidates[selectionResult.selectedIndex];
|
||||
|
||||
// Notify completion
|
||||
onProgress?.(buildProgress('complete', generatorStatuses, generatorResults, 'completed'));
|
||||
|
||||
return {
|
||||
questions: selectedCandidate.questions,
|
||||
reviews: selectionResult.reviews,
|
||||
summary: selectionResult.summary,
|
||||
selectedGeneratorIndex: selectionResult.selectedIndex,
|
||||
allCandidates: successfulCandidates,
|
||||
selectionMode: 'whole_set',
|
||||
metadata: {
|
||||
generatorModels: config.generators.map(g => g.model),
|
||||
reviewerModel: config.reviewerModel,
|
||||
totalGenerationTime,
|
||||
reviewTime,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
// If selection/review fails, return the first successful generation without review
|
||||
console.error('Council selection/review failed:', error);
|
||||
const firstCandidate = successfulCandidates[0];
|
||||
|
||||
onProgress?.(buildProgress('complete', generatorStatuses, generatorResults, 'completed'));
|
||||
|
||||
return {
|
||||
questions: firstCandidate.questions,
|
||||
reviews: [],
|
||||
summary: null,
|
||||
selectedGeneratorIndex: 0,
|
||||
allCandidates: successfulCandidates,
|
||||
selectionMode: 'whole_set',
|
||||
metadata: {
|
||||
generatorModels: config.generators.map(g => g.model),
|
||||
reviewerModel: null,
|
||||
totalGenerationTime,
|
||||
reviewTime: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// =============================================================================
|
||||
// Pack Generation Functions
|
||||
// Generate pack names, descriptions, and pack ideas
|
||||
// =============================================================================
|
||||
|
||||
import { getOpenAI, getModel } from '../client';
|
||||
import type { GeneratedPack, GeneratedPackIdea } from '../types';
|
||||
|
||||
/**
|
||||
* Generate a single pack name and description
|
||||
*/
|
||||
export async function generatePack(
|
||||
categoryName?: string,
|
||||
explicit: boolean = false,
|
||||
crudeLang: boolean = false,
|
||||
inspiration?: string,
|
||||
previousNames?: string[]
|
||||
): Promise<GeneratedPack> {
|
||||
const openai = getOpenAI();
|
||||
|
||||
const explicitInstruction = explicit
|
||||
? 'Include bold, spicy, and uncensored themes (NSFW is allowed).'
|
||||
: 'Do NOT include any explicit or NSFW themes. Keep it romantic, emotional, playful, and clean.';
|
||||
|
||||
const crudeLangInstruction = crudeLang
|
||||
? 'CRUDE LANGUAGE OVERRIDE: Use crude, vulgar terms throughout - "fuck", "cock", "pussy" etc.'
|
||||
: '';
|
||||
|
||||
const inspirationInstruction = inspiration
|
||||
? `\n\nINSPIRATION/GUIDANCE FROM ADMIN:\n${inspiration}\n\nUse the above inspiration to guide your generation.`
|
||||
: '';
|
||||
|
||||
const avoidInstruction = previousNames && previousNames.length > 0
|
||||
? `\n\nIMPORTANT - DO NOT generate any of these pack names (already suggested): ${previousNames.join(', ')}\nGenerate something completely different and fresh.`
|
||||
: '';
|
||||
|
||||
const appContext = `This is an ACTIVITY-FOCUSED app where couples swipe on activity proposals.
|
||||
Each partner independently swipes Like/Dislike/Maybe, and when both swipe positively they "match".
|
||||
Packs contain collections of activities to try together (date ideas, intimate experiences, adventures, challenges, etc.).
|
||||
This is NOT a Q&A app - it's about discovering shared interests in activities.`;
|
||||
|
||||
const prompt = categoryName
|
||||
? `Generate a creative activity pack for a couples' intimacy/connection app in the category "${categoryName}".
|
||||
|
||||
${appContext}
|
||||
|
||||
The pack should contain a themed collection of activities couples can explore together.
|
||||
|
||||
${explicitInstruction}
|
||||
${crudeLangInstruction}${inspirationInstruction}${avoidInstruction}
|
||||
|
||||
Return a JSON object with:
|
||||
- name: A catchy, engaging pack name (3-6 words) that evokes activities/experiences
|
||||
- description: A brief, enticing description (1-2 sentences) focusing on the activities in the pack
|
||||
|
||||
Make it romantic, playful, and activity-focused.`
|
||||
: `Generate a creative activity pack for a couples' intimacy/connection app.
|
||||
|
||||
${appContext}
|
||||
|
||||
The pack should contain a themed collection of activities couples can explore together.
|
||||
|
||||
${explicitInstruction}
|
||||
${crudeLangInstruction}${inspirationInstruction}${avoidInstruction}
|
||||
|
||||
Return a JSON object with:
|
||||
- name: A catchy, engaging pack name (3-6 words) that evokes activities/experiences
|
||||
- description: A brief, enticing description (1-2 sentences) focusing on the activities in the pack
|
||||
|
||||
Make it romantic, playful, and activity-focused.`;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: getModel('generate'),
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'You are a creative content writer for a couples activity/intimacy app. The app helps couples discover shared interests by swiping on activity proposals. Generate engaging pack ideas that contain collections of activities couples can do together. Always respond with valid JSON only.',
|
||||
},
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
response_format: { type: 'json_object' },
|
||||
temperature: 0.8,
|
||||
});
|
||||
|
||||
const content = response.choices[0].message.content;
|
||||
if (!content) throw new Error('No content generated');
|
||||
|
||||
return JSON.parse(content) as GeneratedPack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate pack ideas for a category
|
||||
*/
|
||||
export async function suggestPacks(
|
||||
categoryName: string,
|
||||
existingPacks: string[],
|
||||
explicit: boolean,
|
||||
crudeLang: boolean = false,
|
||||
inspiration?: string,
|
||||
previousSuggestions?: string[]
|
||||
): Promise<GeneratedPackIdea[]> {
|
||||
const openai = getOpenAI();
|
||||
const existingList = existingPacks.length > 0
|
||||
? existingPacks.join(', ')
|
||||
: 'None';
|
||||
|
||||
const explicitInstruction = explicit
|
||||
? 'Include bold, spicy, and uncensored pack ideas (NSFW is allowed).'
|
||||
: 'Do NOT include any explicit or NSFW themes. Keep it romantic, emotional, playful, and clean.';
|
||||
|
||||
const crudeLangInstruction = crudeLang
|
||||
? '\nCRUDE LANGUAGE OVERRIDE: Use crude, vulgar terms in the pack names and descriptions.'
|
||||
: '';
|
||||
|
||||
const inspirationInstruction = inspiration
|
||||
? `\n\nINSPIRATION/GUIDANCE FROM ADMIN:\n${inspiration}\n\nUse the above inspiration to guide your pack suggestions.`
|
||||
: '';
|
||||
|
||||
const avoidInstruction = previousSuggestions && previousSuggestions.length > 0
|
||||
? `\n\nIMPORTANT - DO NOT suggest any of these pack names (already suggested): ${previousSuggestions.join(', ')}\nGenerate completely different and fresh ideas.`
|
||||
: '';
|
||||
|
||||
const prompt = `We are building activity packs for the category "${categoryName}" in a couples' intimacy app.
|
||||
|
||||
IMPORTANT - HOW THE APP WORKS:
|
||||
- This is an ACTIVITY-FOCUSED app, NOT a Q&A app
|
||||
- Packs contain ACTIVITY PROPOSALS (things couples can do together)
|
||||
- Each partner independently swipes Like/Dislike/Maybe on activities
|
||||
- When BOTH partners swipe positively on the same activity, they "match" and can discuss it
|
||||
- Activities are things like: date ideas, intimate experiences, adventures, challenges, conversations to have, etc.
|
||||
- This is NOT about asking each other questions - it's about discovering shared interests in activities
|
||||
|
||||
PACK THEMES should focus on:
|
||||
- Collections of related activities couples might want to try
|
||||
- Experiences to share together
|
||||
- Things to do, not questions to ask
|
||||
- Date ideas, adventures, intimate moments, challenges, bonding activities
|
||||
|
||||
Existing packs in this category: ${existingList}.
|
||||
|
||||
Suggest 5 NEW, UNIQUE pack ideas that fit this category and differ from existing ones.
|
||||
${explicitInstruction}${crudeLangInstruction}${inspirationInstruction}${avoidInstruction}
|
||||
|
||||
Return a JSON object with an "ideas" array containing 5 objects, where each object has:
|
||||
- name: Pack name (catchy, 3-6 words) - should evoke activities/experiences, not questions
|
||||
- description: Brief description (1-2 sentences) focusing on the activities/experiences in the pack
|
||||
- icon: A single descriptive emoji
|
||||
|
||||
Make them engaging and specific to "${categoryName}".`;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: getModel('generate'),
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'You are a creative content strategist for a couples activity/intimacy app. The app helps couples discover shared interests by swiping on activity proposals. Generate engaging pack ideas that contain collections of activities couples can do together. Always respond with valid JSON only.',
|
||||
},
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
response_format: { type: 'json_object' },
|
||||
temperature: 0.9,
|
||||
});
|
||||
|
||||
const content = response.choices[0].message.content;
|
||||
if (!content) throw new Error('No content generated');
|
||||
|
||||
const parsed = JSON.parse(content);
|
||||
return parsed.ideas as GeneratedPackIdea[];
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
// =============================================================================
|
||||
// Question Generation Functions
|
||||
// Generate questions for packs with various tone/intensity settings
|
||||
// =============================================================================
|
||||
|
||||
import { getOpenAI, getModel } from '../client';
|
||||
import { INTENSITY_GUIDE_SHORT, TONE_INSTRUCTIONS, SYSTEM_MESSAGES } from '../config';
|
||||
import type { GeneratedQuestion, ToneLevel } from '../types';
|
||||
|
||||
/**
|
||||
* Build the question generation prompt
|
||||
*/
|
||||
function buildQuestionPrompt(
|
||||
packName: string,
|
||||
count: number,
|
||||
intensity: number | undefined,
|
||||
tone: ToneLevel,
|
||||
packDescription?: string,
|
||||
existingQuestions?: string[],
|
||||
crudeLang: boolean = false,
|
||||
inspiration?: string
|
||||
): string {
|
||||
const isClean = tone === 0;
|
||||
const isExplicit = tone >= 4;
|
||||
|
||||
const crudeLangInstruction = crudeLang
|
||||
? '\n\nCRUDE LANGUAGE OVERRIDE: Ignore nuanced language rules. Use crude, vulgar terms throughout - "fuck" instead of "have sex", "suck cock" instead of "perform oral", etc. Be raw and direct like uncensored sexting.'
|
||||
: '';
|
||||
|
||||
const inspirationInstruction = inspiration
|
||||
? `\n\nINSPIRATION/GUIDANCE FROM ADMIN:\n${inspiration}\n\nUse the above inspiration to guide the types of questions you generate.`
|
||||
: '';
|
||||
|
||||
const intensityInstruction = isClean
|
||||
? 'All questions should be intensity level 1 (non-physical activities).'
|
||||
: intensity
|
||||
? `All questions should be at intensity level ${intensity}.`
|
||||
: 'Vary the intensity levels from 1 to 5 across the questions for good variety.';
|
||||
|
||||
const toneInstruction = TONE_INSTRUCTIONS[tone];
|
||||
|
||||
// Examples based on tone level
|
||||
const symmetricExamples = isClean
|
||||
? 'GOOD: "Cook a new recipe together", "Play a board game", "Go on a hike", "Have a deep conversation about your goals".'
|
||||
: isExplicit
|
||||
? 'GOOD: "Fuck in a risky place", "Try a new position", "Sixty-nine together", "Watch porn and recreate a scene".'
|
||||
: 'GOOD: "Cook a romantic dinner together", "Stargaze and share dreams", "Give each other massages", "Dance together at home".';
|
||||
|
||||
const asymmetricExamples = isClean
|
||||
? `Examples:
|
||||
- text: "Cook your partner their favorite meal" → partner_text: "Have your partner cook your favorite meal"
|
||||
- text: "Plan a surprise for your partner" → partner_text: "Be surprised by your partner"
|
||||
- text: "Give your partner a compliment" → partner_text: "Receive a compliment from your partner"`
|
||||
: isExplicit
|
||||
? `Examples:
|
||||
- text: "Go down on your partner" → partner_text: "Have your partner go down on you"
|
||||
- text: "Send your partner a nude" → partner_text: "Receive a nude from your partner"
|
||||
- text: "Tie your partner up" → partner_text: "Get tied up by your partner"
|
||||
- text: "Spank your partner" → partner_text: "Get spanked by your partner"`
|
||||
: `Examples:
|
||||
- text: "Give your partner a massage" → partner_text: "Receive a massage from your partner"
|
||||
- text: "Write your partner a love letter" → partner_text: "Receive a love letter from your partner"
|
||||
- text: "Plan a surprise date for your partner" → partner_text: "Be surprised with a date by your partner"`;
|
||||
|
||||
const explicitWarning = isClean
|
||||
? '\n\nCRITICAL: This is a CLEAN pack with NO romantic or sexual content. Focus ONLY on communication, activities, challenges, and bonding. NO romance, NO flirting, NO intimacy, NO physical affection beyond friendly gestures.'
|
||||
: isExplicit
|
||||
? ''
|
||||
: '\n\nCRITICAL: This is a NON-EXPLICIT pack. Do NOT include any sexual acts, crude language, or NSFW content. Keep all proposals romantic, playful, or emotionally intimate without being sexually explicit.';
|
||||
|
||||
const descriptionContext = packDescription
|
||||
? `\nPack Description: "${packDescription}"\nUse this description to guide the theme and style of questions.`
|
||||
: '';
|
||||
|
||||
const existingQuestionsContext = existingQuestions && existingQuestions.length > 0
|
||||
? `\n\nEXISTING QUESTIONS IN THIS PACK (DO NOT DUPLICATE OR CREATE SIMILAR VARIATIONS):
|
||||
${existingQuestions.map((q, i) => `${i + 1}. ${q}`).join('\n')}
|
||||
|
||||
CRITICAL: Generate completely NEW and DIFFERENT questions. Do not repeat any of the above questions or create minor variations of them. Each new question must offer a genuinely distinct activity or experience.`
|
||||
: '';
|
||||
|
||||
return `Generate ${count} unique questions for a couples' intimacy question pack called "${packName}".${descriptionContext}${existingQuestionsContext}
|
||||
|
||||
${INTENSITY_GUIDE_SHORT}
|
||||
|
||||
${intensityInstruction}
|
||||
${toneInstruction}${explicitWarning}${crudeLangInstruction}${inspirationInstruction}
|
||||
|
||||
IMPORTANT: The app uses a swipe-based interface (Like/Dislike/Maybe).
|
||||
Cards should be "Proposals" for specific actions, NOT interview questions.
|
||||
|
||||
CRITICAL LANGUAGE RULES:
|
||||
- ALWAYS use "your partner" - NEVER use "me", "I", "you" (as the receiver), "him", "her"
|
||||
- BAD: "Send me a photo" (who is "me"?), "Let me spank you", "I want you to..."
|
||||
- GOOD: "Send your partner a photo", "Spank your partner", "Give your partner..."
|
||||
- The card reader is the DOER. Their partner is "your partner".
|
||||
|
||||
For each question, decide if it is SYMMETRIC (shared) or ASYMMETRIC (one does to the other):
|
||||
|
||||
1. SYMMETRIC Activities (partner_text = null):
|
||||
- Both partners do the same thing together.
|
||||
- ${symmetricExamples}
|
||||
|
||||
2. ASYMMETRIC Actions (requires partner_text):
|
||||
- One partner does something TO/FOR the other, or roles differ.
|
||||
- text = what the INITIATOR does
|
||||
- partner_text = what the RECEIVER does/experiences
|
||||
- ${asymmetricExamples}
|
||||
|
||||
Required JSON structure:
|
||||
- text: The proposal (DOER's perspective, uses "your partner")
|
||||
- partner_text: For asymmetric only - RECEIVER's perspective
|
||||
- intensity: 1-5 based on INTENSITY LEVELS above.
|
||||
- requires_props: (optional) Array of items needed
|
||||
- location_type: (optional) "home" | "public" | "outdoors" | "travel" | "anywhere"
|
||||
- effort_level: (optional) "spontaneous" | "low" | "medium" | "planned"
|
||||
|
||||
Return a JSON object with a "questions" array.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate questions using the default generation model
|
||||
*/
|
||||
export async function generateQuestions(
|
||||
packName: string,
|
||||
count: number = 10,
|
||||
intensity?: number,
|
||||
tone: ToneLevel = 3,
|
||||
packDescription?: string,
|
||||
existingQuestions?: string[],
|
||||
crudeLang: boolean = false,
|
||||
inspiration?: string
|
||||
): Promise<GeneratedQuestion[]> {
|
||||
const openai = getOpenAI();
|
||||
|
||||
const prompt = buildQuestionPrompt(
|
||||
packName,
|
||||
count,
|
||||
intensity,
|
||||
tone,
|
||||
packDescription,
|
||||
existingQuestions,
|
||||
crudeLang,
|
||||
inspiration
|
||||
);
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: getModel('generate'),
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: SYSTEM_MESSAGES[tone],
|
||||
},
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
response_format: { type: 'json_object' },
|
||||
temperature: 0.9,
|
||||
});
|
||||
|
||||
const content = response.choices[0].message.content;
|
||||
if (!content) throw new Error('No content generated');
|
||||
|
||||
const parsed = JSON.parse(content);
|
||||
return parsed.questions as GeneratedQuestion[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate questions using a specific model
|
||||
* Used by council generation for parallel model comparison
|
||||
*/
|
||||
export async function generateQuestionsWithModel(
|
||||
model: string,
|
||||
packName: string,
|
||||
count: number,
|
||||
intensity: number | undefined,
|
||||
tone: ToneLevel,
|
||||
packDescription: string | undefined,
|
||||
existingQuestions: string[] | undefined,
|
||||
crudeLang: boolean,
|
||||
inspiration: string | undefined
|
||||
): Promise<GeneratedQuestion[]> {
|
||||
const openai = getOpenAI();
|
||||
|
||||
const prompt = buildQuestionPrompt(
|
||||
packName,
|
||||
count,
|
||||
intensity,
|
||||
tone,
|
||||
packDescription,
|
||||
existingQuestions,
|
||||
crudeLang,
|
||||
inspiration
|
||||
);
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: model,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: SYSTEM_MESSAGES[tone],
|
||||
},
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
response_format: { type: 'json_object' },
|
||||
temperature: 0.9,
|
||||
});
|
||||
|
||||
const content = response.choices[0].message.content;
|
||||
if (!content) throw new Error('No content generated');
|
||||
|
||||
const parsed = JSON.parse(content);
|
||||
return parsed.questions as GeneratedQuestion[];
|
||||
}
|
||||
Reference in new issue
Block a user