← back to B Version 1

generator/bubbe-generator.ts

239 lines

/**
 * Dear Bubbe AI Generator
 *
 * Generates authentic Yiddish-grandma-style advice columns
 * Using Claude AI (Anthropic)
 */

import Anthropic from '@anthropic-ai/sdk';
import { randomUUID } from 'crypto';

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY || 'your-api-key-here'
});

interface BubbeEntry {
  id: string;
  originalSource: string;
  originalDate: Date | null;
  originalQuestionText: string | null;
  bubbeRewrittenQuestion: string;
  bubbeAnswer: string;
  category: string;
  tags: string[];
  tone: string;
  yiddishLevel: 'light' | 'medium' | 'heavy';
  sarcasmLevel: number;
  smsLengthOk: boolean;
  characterCount: number;
  approved: boolean;
  notes: string;
}

const CATEGORIES = [
  'family', 'romance', 'dating', 'marriage', 'health', 'money',
  'etiquette', 'work', 'neighbors', 'holidays', 'grief', 'kids',
  'teens', 'pets', 'mother-in-law', 'cheating', 'friendship',
  'moral-dilemma', 'awkward', 'misc'
];

const BUBBE_SYSTEM_PROMPT = `You are "Bubbe" - a wise, sarcastic, loving Jewish grandmother who gives advice.

Your personality:
- Sharp wit with a side of guilt
- Practical wisdom mixed with Yiddish sayings
- Loving but brutally honest
- Sprinkles in Yiddish words naturally (meshuggeneh, oy vey, bubbeleh, shvitz, kvetch, etc.)
- References food metaphors (brisket, challah, matzah ball soup)
- Compares everything to "in my day"
- Guilt-trips with love
- Short, punchy answers (SMS-friendly when possible)

Style rules:
1. Keep answers under 300 characters for SMS when possible
2. Use 2-5 Yiddish words per response
3. Be direct - no fluff
4. Mix tough love with humor
5. Always practical advice at the core

Examples of your voice:
"Bubbeleh, your boyfriend texts you once a week? My brisket gets more attention. Find someone who treats you like fresh challah, not day-old bread."

"Oy vey, your mother-in-law comes over unannounced? Tell her meshuggeneh self: 'Love you, but call first!' Problem solved."

"Your boss takes credit for your work? Document everything. Then schedule a meeting. Be firm, not a shmendrik. You deserve respect, sweetheart."`;

async function generateBubbeEntry(category: string, customPrompt?: string): Promise<BubbeEntry> {
  const prompt = customPrompt || `Generate a realistic advice question for the category "${category}".
  Then provide Bubbe's answer.

  Format your response as JSON:
  {
    "question": "the advice question",
    "answer": "Bubbe's response",
    "tags": ["tag1", "tag2"],
    "yiddishLevel": "light|medium|heavy",
    "sarcasmLevel": 1-5
  }`;

  const message = await anthropic.messages.create({
    model: 'claude-opus-4-8',
    max_tokens: 1024,
    system: BUBBE_SYSTEM_PROMPT,
    messages: [{
      role: 'user',
      content: prompt
    }]
  });

  const responseText = message.content[0].type === 'text' ? message.content[0].text : '';

  // Extract JSON from response
  const jsonMatch = responseText.match(/\{[\s\S]*\}/);
  if (!jsonMatch) {
    throw new Error('Failed to parse Bubbe response');
  }

  const parsed = JSON.parse(jsonMatch[0]);
  const answer = parsed.answer;
  const characterCount = answer.length;
  const smsLengthOk = characterCount <= 320; // SMS limit with some buffer

  return {
    id: randomUUID(),
    originalSource: 'Bubbe Original',
    originalDate: null,
    originalQuestionText: null,
    bubbeRewrittenQuestion: parsed.question,
    bubbeAnswer: answer,
    category,
    tags: parsed.tags || [],
    tone: 'yiddish-grandma',
    yiddishLevel: parsed.yiddishLevel || 'medium',
    sarcasmLevel: parsed.sarcasmLevel || 3,
    smsLengthOk,
    characterCount,
    approved: true,
    notes: 'AI generated'
  };
}

async function generateBatch(count: number, categories?: string[]): Promise<BubbeEntry[]> {
  const entries: BubbeEntry[] = [];
  const categoriesToUse = categories || CATEGORIES;

  console.log(`\n🥯 Bubbe is cooking up ${count} pieces of advice...\n`);

  for (let i = 0; i < count; i++) {
    const category = categoriesToUse[i % categoriesToUse.length];

    try {
      const entry = await generateBubbeEntry(category);
      entries.push(entry);

      console.log(`✅ [${i + 1}/${count}] ${category}: ${entry.bubbeAnswer.substring(0, 80)}...`);

      // Rate limiting - Claude has generous limits but let's be polite
      if (i < count - 1) {
        await new Promise(resolve => setTimeout(resolve, 1000));
      }
    } catch (error) {
      console.error(`❌ Failed to generate entry ${i + 1}:`, error);
    }
  }

  return entries;
}

async function transformExistingQuestion(
  originalQuestion: string,
  originalSource: string = 'Public Domain'
): Promise<BubbeEntry> {
  const prompt = `Transform this advice column question into Bubbe's voice and provide her answer.

Original question:
"${originalQuestion}"

Format response as JSON:
{
  "bubbeQuestion": "question rewritten in Bubbe's voice",
  "answer": "Bubbe's response",
  "category": "choose from: ${CATEGORIES.join(', ')}",
  "tags": ["tag1", "tag2"],
  "yiddishLevel": "light|medium|heavy",
  "sarcasmLevel": 1-5
}`;

  const message = await anthropic.messages.create({
    model: 'claude-opus-4-8',
    max_tokens: 1024,
    system: BUBBE_SYSTEM_PROMPT,
    messages: [{
      role: 'user',
      content: prompt
    }]
  });

  const responseText = message.content[0].type === 'text' ? message.content[0].text : '';
  const jsonMatch = responseText.match(/\{[\s\S]*\}/);

  if (!jsonMatch) {
    throw new Error('Failed to parse transformation response');
  }

  const parsed = JSON.parse(jsonMatch[0]);
  const answer = parsed.answer;
  const characterCount = answer.length;

  return {
    id: randomUUID(),
    originalSource,
    originalDate: null,
    originalQuestionText: originalQuestion,
    bubbeRewrittenQuestion: parsed.bubbeQuestion,
    bubbeAnswer: answer,
    category: parsed.category,
    tags: parsed.tags || [],
    tone: 'yiddish-grandma',
    yiddishLevel: parsed.yiddishLevel || 'medium',
    sarcasmLevel: parsed.sarcasmLevel || 3,
    smsLengthOk: characterCount <= 320,
    characterCount,
    approved: true,
    notes: `Transformed from ${originalSource}`
  };
}

// Export functions
export {
  generateBubbeEntry,
  generateBatch,
  transformExistingQuestion,
  BubbeEntry,
  CATEGORIES
};

// CLI usage
if (require.main === module) {
  const args = process.argv.slice(2);
  const count = parseInt(args[0]) || 10;

  generateBatch(count).then(entries => {
    console.log(`\n\n📊 Generated ${entries.length} Bubbe entries`);
    console.log(`\n💾 Saving to JSON...\n`);

    const fs = require('fs');
    fs.writeFileSync(
      'bubbe-entries.json',
      JSON.stringify(entries, null, 2)
    );

    console.log(`✅ Saved to bubbe-entries.json`);
    console.log(`\n📝 Sample entry:\n`);
    console.log(`Q: ${entries[0].bubbeRewrittenQuestion}`);
    console.log(`A: ${entries[0].bubbeAnswer}`);
    console.log(`Category: ${entries[0].category}`);
    console.log(`SMS-friendly: ${entries[0].smsLengthOk ? 'Yes' : 'No'}`);
  }).catch(console.error);
}