← back to Dear Bubbe Nextjs

lib/moderation/content-filter.ts

221 lines

import bannedTerms from './banned-terms.json'

interface FilterResult {
  clean: boolean
  violations: string[]
  sanitizedText?: string
  severity: 'critical' | 'high' | 'medium' | 'low' | 'none'
}

export class ContentFilter {
  private bannedPatterns: Map<string, RegExp> = new Map()
  
  constructor() {
    this.initializePatterns()
  }
  
  private initializePatterns() {
    // Build regex patterns for all banned terms
    Object.values(bannedTerms.categories).forEach((category: any) => {
      category.terms.forEach((item: any) => {
        const term = item.term || item.phrase
        const variants = item.variants || []
        const allTerms = [term, ...variants]
        
        allTerms.forEach(t => {
          // Create case-insensitive pattern with word boundaries
          const escaped = t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
          const pattern = new RegExp(`\\b${escaped}\\b`, 'gi')
          this.bannedPatterns.set(t.toLowerCase(), pattern)
        })
      })
    })
  }
  
  /**
   * Check if text contains banned terms
   */
  public checkContent(text: string, allowBubbeExceptions: boolean = false): FilterResult {
    const violations: string[] = []
    let severity: FilterResult['severity'] = 'none'
    
    // Check each category
    for (const [categoryName, category] of Object.entries(bannedTerms.categories)) {
      const categoryData = category as any
      
      for (const item of categoryData.terms) {
        const term = item.term || item.phrase
        const variants = item.variants || []
        const allTerms = [term, ...variants]
        
        for (const t of allTerms) {
          const pattern = this.bannedPatterns.get(t.toLowerCase())
          if (pattern && pattern.test(text)) {
            // Check if this term has a Bubbe exception and we're allowing those
            if (allowBubbeExceptions && item.bubbe_exception) {
              continue // Skip this violation for Bubbe character responses
            }
            
            violations.push(`${categoryName}: ${t}`)
            
            // Update severity
            if (categoryData.severity === 'critical') {
              severity = 'critical'
            } else if (categoryData.severity === 'high' && severity !== 'critical') {
              severity = 'high'
            }
          }
        }
      }
    }
    
    return {
      clean: violations.length === 0,
      violations,
      severity,
      sanitizedText: violations.length > 0 ? this.sanitizeText(text) : text
    }
  }
  
  /**
   * Remove or replace banned terms in text
   */
  private sanitizeText(text: string): string {
    let sanitized = text
    
    // Replace banned terms with safe alternatives
    for (const [categoryName, category] of Object.entries(bannedTerms.categories)) {
      const categoryData = category as any
      
      for (const item of categoryData.terms) {
        const term = item.term || item.phrase
        const variants = item.variants || []
        const replacement = item.replacement || '[removed]'
        const allTerms = [term, ...variants]
        
        for (const t of allTerms) {
          const pattern = this.bannedPatterns.get(t.toLowerCase())
          if (pattern) {
            sanitized = sanitized.replace(pattern, replacement)
          }
        }
      }
    }
    
    return sanitized
  }
  
  /**
   * Check if a prompt is trying to elicit banned content
   */
  public checkPrompt(prompt: string): boolean {
    const lowerPrompt = prompt.toLowerCase()
    
    // Check for attempts to get Bubbe to use slurs
    const suspiciousPatterns = [
      /what.*call.*non-jew/i,
      /say.*about.*\brace\b/i,
      /opinion.*different.*people/i,
      /joke.*about.*\bjew/i,
      /tell.*about.*hitler/i,
      /holocaust.*joke/i,
      /use.*word.*goy/i,
      /call.*them.*shiksa/i
    ]
    
    return suspiciousPatterns.some(pattern => pattern.test(lowerPrompt))
  }
  
  /**
   * Get safe response for problematic prompts
   */
  public getSafeResponse(prompt: string): string | null {
    if (this.checkPrompt(prompt)) {
      const responses = [
        "Oy vey, bubbeleh! I don't talk like that. Let's keep it nice, yes?",
        "What kind of meshugana question is that? I raised you better!",
        "Listen here, I don't use such words. Find something else to talk about!",
        "Feh! My mother would wash my mouth with soap for such talk. Next question!",
        "I'm a classy lady, I don't speak that way. Now, what else can I help you with?"
      ]
      return responses[Math.floor(Math.random() * responses.length)]
    }
    return null
  }
  
  /**
   * Check if text contains Holocaust references that aren't educational
   */
  public checkHolocaustContext(text: string, isEducational: boolean = false): boolean {
    const holocaustTerms = bannedTerms.categories.holocaust_references.terms
    
    for (const item of holocaustTerms) {
      const term = item.term
      const variants = item.variants || []
      const allTerms = [term, ...variants]
      
      for (const t of allTerms) {
        if (new RegExp(`\\b${t}\\b`, 'gi').test(text)) {
          // If it's educational context, might be acceptable
          if (isEducational) {
            // Check if it's respectful discussion
            const respectfulMarkers = /\b(history|remembrance|education|memorial|survivor|testimony)\b/gi
            return respectfulMarkers.test(text)
          }
          return false // Not acceptable in casual context
        }
      }
    }
    
    return true // No Holocaust references found
  }
  
  /**
   * Validate Bubbe's response before sending
   */
  public validateResponse(response: string): { valid: boolean; reason?: string } {
    const filterResult = this.checkContent(response, true) // Allow Bubbe exceptions
    
    if (!filterResult.clean) {
      return {
        valid: false,
        reason: `Response contains banned terms: ${filterResult.violations.join(', ')}`
      }
    }
    
    // Additional checks for tone
    if (!this.checkHolocaustContext(response, false)) {
      return {
        valid: false,
        reason: 'Response contains inappropriate Holocaust reference'
      }
    }
    
    return { valid: true }
  }
}

// Export singleton instance
export const contentFilter = new ContentFilter()

// Export function for easy use in API routes
export function filterBubbeResponse(text: string): { 
  safe: boolean
  filtered?: string
  warning?: string 
} {
  const validation = contentFilter.validateResponse(text)
  
  if (!validation.valid) {
    console.error('[CONTENT FILTER] Blocked response:', validation.reason)
    
    // Return a safe fallback response
    return {
      safe: false,
      filtered: "Oy, something got mixed up in my old brain! Let me think again... What did you want to know, bubbeleh?",
      warning: validation.reason
    }
  }
  
  return { safe: true, filtered: text }
}