← back to Dear Bubbe Nextjs

lib/user-memory.ts

321 lines

import fs from 'fs'
import path from 'path'

interface UserMemory {
  userId: string
  profile: any
  conversations: Array<{
    timestamp: string
    message: string
    response: string
    context?: string
  }>
  lastVisit: string
  totalMessages: number
  preferences: {
    voiceEnabled: boolean
    colorTheme: 'warm' | 'cool'
    language?: string
  }
  location?: {
    country?: string
    state?: string
    city?: string
    coordinates?: {
      lat: number
      lon: number
    }
  }
  topics: string[] // Topics user has discussed
  relationships?: {
    partnerName?: string
    kidsNames?: string[]
    familyMembers?: string[]
  }
  importantDates?: {
    anniversary?: string
    birthdays?: Array<{name: string, date: string}>
  }
}

const MEMORY_DIR = path.join(process.cwd(), 'user-memories')

// Ensure memory directory exists
if (!fs.existsSync(MEMORY_DIR)) {
  fs.mkdirSync(MEMORY_DIR, { recursive: true })
}

export function getUserMemoryPath(userId: string): string {
  // Sanitize userId to be filesystem-safe
  const safeUserId = userId.replace(/[^a-zA-Z0-9-_]/g, '_')
  return path.join(MEMORY_DIR, `${safeUserId}.md`)
}

export function loadUserMemory(userId: string): UserMemory | null {
  const memoryPath = getUserMemoryPath(userId)
  
  if (!fs.existsSync(memoryPath)) {
    return null
  }
  
  try {
    const content = fs.readFileSync(memoryPath, 'utf-8')
    // Parse the markdown file
    return parseMemoryMarkdown(content)
  } catch (error) {
    console.error('[MEMORY] Error loading user memory:', error)
    return null
  }
}

export function saveUserMemory(memory: UserMemory): void {
  const memoryPath = getUserMemoryPath(memory.userId)
  const markdown = generateMemoryMarkdown(memory)
  
  try {
    fs.writeFileSync(memoryPath, markdown, 'utf-8')
    console.log(`[MEMORY] Saved memory for user ${memory.userId}`)
  } catch (error) {
    console.error('[MEMORY] Error saving user memory:', error)
  }
}

export function updateConversation(
  userId: string,
  message: string,
  response: string,
  context?: string
): void {
  let memory = loadUserMemory(userId)
  
  if (!memory) {
    memory = {
      userId,
      profile: {},
      conversations: [],
      lastVisit: new Date().toISOString(),
      totalMessages: 0,
      preferences: {
        voiceEnabled: true,
        colorTheme: 'warm'
      },
      topics: []
    }
  }
  
  memory.conversations.push({
    timestamp: new Date().toISOString(),
    message,
    response,
    context
  })
  
  memory.totalMessages++
  memory.lastVisit = new Date().toISOString()
  
  // Extract topics from conversation
  const topics = extractTopics(message + ' ' + response)
  topics.forEach(topic => {
    if (!memory.topics.includes(topic)) {
      memory.topics.push(topic)
    }
  })
  
  // Keep only last 50 conversations to prevent file from getting too large
  if (memory.conversations.length > 50) {
    memory.conversations = memory.conversations.slice(-50)
  }
  
  saveUserMemory(memory)
}

function extractTopics(text: string): string[] {
  const topics: string[] = []
  const lowerText = text.toLowerCase()
  
  // Define topic patterns
  const topicPatterns = [
    { pattern: /\b(marriage|wedding|engaged|spouse|husband|wife)\b/gi, topic: 'marriage' },
    { pattern: /\b(kids?|children|baby|babies|pregnant)\b/gi, topic: 'children' },
    { pattern: /\b(job|work|career|salary|boss|company)\b/gi, topic: 'career' },
    { pattern: /\b(money|income|salary|debt|savings|rent)\b/gi, topic: 'finances' },
    { pattern: /\b(school|university|college|degree|education)\b/gi, topic: 'education' },
    { pattern: /\b(health|sick|doctor|medicine|hospital)\b/gi, topic: 'health' },
    { pattern: /\b(diet|weight|eating|food|cooking)\b/gi, topic: 'food' },
    { pattern: /\b(dating|relationship|boyfriend|girlfriend|single)\b/gi, topic: 'relationships' },
    { pattern: /\b(jewish|torah|synagogue|shabbat|kosher)\b/gi, topic: 'religion' },
    { pattern: /\b(news|israel|politics|election)\b/gi, topic: 'current_events' }
  ]
  
  topicPatterns.forEach(({ pattern, topic }) => {
    if (pattern.test(lowerText) && !topics.includes(topic)) {
      topics.push(topic)
    }
  })
  
  return topics
}

function generateMemoryMarkdown(memory: UserMemory): string {
  const md: string[] = []
  
  md.push(`# User Memory: ${memory.userId}`)
  md.push(`Last Visit: ${memory.lastVisit}`)
  md.push(`Total Messages: ${memory.totalMessages}`)
  md.push('')
  
  // Profile Section
  md.push('## Profile')
  if (memory.profile) {
    Object.entries(memory.profile).forEach(([key, value]) => {
      if (value) {
        md.push(`- **${key}**: ${value}`)
      }
    })
  }
  md.push('')
  
  // Location Section
  if (memory.location) {
    md.push('## Location')
    if (memory.location.city) md.push(`- City: ${memory.location.city}`)
    if (memory.location.state) md.push(`- State: ${memory.location.state}`)
    if (memory.location.country) md.push(`- Country: ${memory.location.country}`)
    md.push('')
  }
  
  // Topics Discussed
  if (memory.topics && memory.topics.length > 0) {
    md.push('## Topics Discussed')
    memory.topics.forEach(topic => {
      md.push(`- ${topic}`)
    })
    md.push('')
  }
  
  // Relationships
  if (memory.relationships) {
    md.push('## Relationships')
    if (memory.relationships.partnerName) {
      md.push(`- Partner: ${memory.relationships.partnerName}`)
    }
    if (memory.relationships.kidsNames && memory.relationships.kidsNames.length > 0) {
      md.push(`- Children: ${memory.relationships.kidsNames.join(', ')}`)
    }
    md.push('')
  }
  
  // Recent Conversations (last 10)
  md.push('## Recent Conversations')
  const recentConvos = memory.conversations.slice(-10).reverse()
  recentConvos.forEach((convo, index) => {
    md.push(`### ${new Date(convo.timestamp).toLocaleString()}`)
    md.push(`**User**: ${convo.message}`)
    md.push(`**Bubbe**: ${convo.response}`)
    if (convo.context) {
      md.push(`*Context: ${convo.context}*`)
    }
    md.push('')
  })
  
  return md.join('\n')
}

function parseMemoryMarkdown(content: string): UserMemory {
  const lines = content.split('\n')
  const memory: UserMemory = {
    userId: '',
    profile: {},
    conversations: [],
    lastVisit: '',
    totalMessages: 0,
    preferences: {
      voiceEnabled: true,
      colorTheme: 'warm'
    },
    topics: []
  }
  
  let currentSection = ''
  let currentConvo: any = null
  
  lines.forEach(line => {
    // Parse headers
    if (line.startsWith('# User Memory:')) {
      memory.userId = line.replace('# User Memory:', '').trim()
    } else if (line.startsWith('Last Visit:')) {
      memory.lastVisit = line.replace('Last Visit:', '').trim()
    } else if (line.startsWith('Total Messages:')) {
      memory.totalMessages = parseInt(line.replace('Total Messages:', '').trim()) || 0
    } else if (line.startsWith('## ')) {
      currentSection = line.replace('## ', '').trim()
    } else if (line.startsWith('### ') && currentSection === 'Recent Conversations') {
      if (currentConvo) {
        memory.conversations.push(currentConvo)
      }
      currentConvo = {
        timestamp: line.replace('### ', '').trim(),
        message: '',
        response: ''
      }
    } else if (line.startsWith('**User**:')) {
      if (currentConvo) {
        currentConvo.message = line.replace('**User**:', '').trim()
      }
    } else if (line.startsWith('**Bubbe**:')) {
      if (currentConvo) {
        currentConvo.response = line.replace('**Bubbe**:', '').trim()
      }
    } else if (line.startsWith('- ') && currentSection === 'Topics Discussed') {
      memory.topics.push(line.replace('- ', '').trim())
    } else if (line.startsWith('- **') && currentSection === 'Profile') {
      const match = line.match(/- \*\*(.+?)\*\*: (.+)/)
      if (match) {
        memory.profile[match[1]] = match[2]
      }
    }
  })
  
  if (currentConvo) {
    memory.conversations.push(currentConvo)
  }
  
  return memory
}

export function getUserConversationSummary(userId: string): string {
  const memory = loadUserMemory(userId)
  if (!memory) return ''
  
  const summary: string[] = []
  
  if (memory.profile.preferredName) {
    summary.push(`User prefers to be called ${memory.profile.preferredName}.`)
  }
  
  if (memory.topics.length > 0) {
    summary.push(`They've discussed: ${memory.topics.join(', ')}.`)
  }
  
  if (memory.conversations.length > 0) {
    const lastConvo = memory.conversations[memory.conversations.length - 1]
    const timeSince = getTimeSince(lastConvo.timestamp)
    summary.push(`Last conversation was ${timeSince} ago.`)
  }
  
  return summary.join(' ')
}

function getTimeSince(timestamp: string): string {
  const now = new Date()
  const then = new Date(timestamp)
  const diffMs = now.getTime() - then.getTime()
  const diffMins = Math.floor(diffMs / 60000)
  const diffHours = Math.floor(diffMins / 60)
  const diffDays = Math.floor(diffHours / 24)
  
  if (diffDays > 0) return `${diffDays} day${diffDays > 1 ? 's' : ''}`
  if (diffHours > 0) return `${diffHours} hour${diffHours > 1 ? 's' : ''}`
  if (diffMins > 0) return `${diffMins} minute${diffMins > 1 ? 's' : ''}`
  return 'moments'
}