← 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'
}