← back to Dear Bubbe Admin
backend/models/Conversation.js
319 lines
const mongoose = require('mongoose')
const conversationSchema = new mongoose.Schema({
userId: {
type: String,
required: true,
index: true
},
sessionId: {
type: String,
required: true,
index: true
},
timestamp: {
type: Date,
default: Date.now,
index: true
},
message: {
text: { type: String, required: true },
length: { type: Number },
language: { type: String, default: 'en' },
sentiment: {
type: String,
enum: ['positive', 'neutral', 'negative'],
default: 'neutral'
},
toxicity: {
score: { type: Number, min: 0, max: 1, default: 0 },
categories: [{ type: String }] // hate, insult, threat, etc.
}
},
response: {
text: { type: String, required: true },
mode: { type: String, default: 'bubbe' }, // bubbe, meshugana, comedian
length: { type: Number },
generationTime: { type: Number }, // in ms
model: { type: String, default: 'claude-3-haiku' },
voiceGenerated: { type: Boolean, default: false },
voiceUrl: { type: String }
},
context: {
conversationNumber: { type: Number, default: 1 },
timeSinceLastMessage: { type: Number }, // in seconds
previousTopic: { type: String },
userProfile: { type: mongoose.Schema.Types.Mixed }, // Snapshot of user profile
location: { type: mongoose.Schema.Types.Mixed }
},
topics: [{ type: String }], // Extracted topics like 'marriage', 'career', 'food'
analysis: {
responseTime: { type: Number }, // in ms
userEngagement: {
type: String,
enum: ['low', 'medium', 'high'],
default: 'medium'
},
topicShift: { type: Boolean, default: false },
escalation: { type: Boolean, default: false }, // If conversation became heated
satisfaction: {
type: String,
enum: ['satisfied', 'neutral', 'frustrated'],
default: 'neutral'
}
},
flags: {
flaggedContent: { type: Boolean, default: false },
flagReason: [{ type: String }], // antisemitism, racism, harassment, etc.
alertsTriggered: { type: Number, default: 0 },
reviewed: { type: Boolean, default: false },
reviewedBy: { type: String }, // Admin user ID
reviewedAt: { type: Date },
reviewNotes: { type: String }
},
technicalData: {
ip: { type: String, index: true },
userAgent: { type: String },
referer: { type: String },
apiEndpoint: { type: String },
requestTime: { type: Number },
errors: [{
timestamp: { type: Date },
error: { type: String },
code: { type: String }
}]
}
}, {
timestamps: true
})
// Indexes for performance
conversationSchema.index({ timestamp: -1 })
conversationSchema.index({ userId: 1, timestamp: -1 })
conversationSchema.index({ sessionId: 1, 'context.conversationNumber': 1 })
conversationSchema.index({ 'flags.flaggedContent': 1, timestamp: -1 })
conversationSchema.index({ topics: 1 })
// Pre-save middleware to extract topics and analyze content
conversationSchema.pre('save', function(next) {
// Calculate message length
if (this.message.text) {
this.message.length = this.message.text.length
}
if (this.response.text) {
this.response.length = this.response.text.length
}
// Extract topics from message and response text
if (!this.topics || this.topics.length === 0) {
this.topics = this.extractTopics(this.message.text + ' ' + this.response.text)
}
// Analyze sentiment
this.message.sentiment = this.analyzeSentiment(this.message.text)
next()
})
// Method to extract topics from text
conversationSchema.methods.extractTopics = function(text) {
const topics = []
const lowerText = text.toLowerCase()
const topicPatterns = [
{ pattern: /\b(marriage|wedding|engaged|spouse|husband|wife|marry)\b/gi, topic: 'marriage' },
{ pattern: /\b(kids?|children|baby|babies|pregnant|family)\b/gi, topic: 'children' },
{ pattern: /\b(job|work|career|salary|boss|company|employment)\b/gi, topic: 'career' },
{ pattern: /\b(money|income|salary|debt|savings|rent|cost|expensive)\b/gi, topic: 'finances' },
{ pattern: /\b(school|university|college|degree|education|student)\b/gi, topic: 'education' },
{ pattern: /\b(health|sick|doctor|medicine|hospital|diet)\b/gi, topic: 'health' },
{ pattern: /\b(food|cooking|eating|restaurant|recipe)\b/gi, topic: 'food' },
{ pattern: /\b(dating|relationship|boyfriend|girlfriend|single|love)\b/gi, topic: 'relationships' },
{ pattern: /\b(jewish|torah|synagogue|shabbat|kosher|yiddish)\b/gi, topic: 'religion' },
{ pattern: /\b(news|israel|politics|election|trump|biden)\b/gi, topic: 'current_events' },
{ pattern: /\b(weather|temperature|rain|snow|storm|forecast)\b/gi, topic: 'weather' },
{ pattern: /\b(sports|game|team|basketball|football|baseball)\b/gi, topic: 'sports' },
{ pattern: /\b(travel|vacation|trip|hotel|flight)\b/gi, topic: 'travel' },
{ pattern: /\b(house|apartment|rent|buy|move|neighborhood)\b/gi, topic: 'housing' }
]
topicPatterns.forEach(({ pattern, topic }) => {
if (pattern.test(lowerText) && !topics.includes(topic)) {
topics.push(topic)
}
})
return topics
}
// Method to analyze sentiment
conversationSchema.methods.analyzeSentiment = function(text) {
const positiveWords = [
'happy', 'great', 'good', 'excellent', 'wonderful', 'amazing', 'love', 'fantastic',
'awesome', 'perfect', 'beautiful', 'brilliant', 'outstanding', 'magnificent'
]
const negativeWords = [
'sad', 'bad', 'terrible', 'awful', 'hate', 'horrible', 'disgusting', 'worst',
'annoying', 'frustrated', 'angry', 'disappointed', 'upset', 'depressed'
]
const lowerText = text.toLowerCase()
let positiveScore = 0
let negativeScore = 0
positiveWords.forEach(word => {
const regex = new RegExp(`\\b${word}\\b`, 'gi')
const matches = (lowerText.match(regex) || []).length
positiveScore += matches
})
negativeWords.forEach(word => {
const regex = new RegExp(`\\b${word}\\b`, 'gi')
const matches = (lowerText.match(regex) || []).length
negativeScore += matches
})
if (positiveScore > negativeScore) return 'positive'
if (negativeScore > positiveScore) return 'negative'
return 'neutral'
}
// Method to flag conversation
conversationSchema.methods.flagContent = function(reasons, adminId = null) {
this.flags.flaggedContent = true
this.flags.flagReason = Array.isArray(reasons) ? reasons : [reasons]
this.flags.alertsTriggered++
if (adminId) {
this.flags.reviewedBy = adminId
this.flags.reviewedAt = new Date()
this.flags.reviewed = true
}
return this.save()
}
// Static method to get conversation statistics
conversationSchema.statics.getStats = function(timeframe = '24h') {
const now = new Date()
let startTime
switch (timeframe) {
case '1h':
startTime = new Date(now - 60 * 60 * 1000)
break
case '24h':
startTime = new Date(now - 24 * 60 * 60 * 1000)
break
case '7d':
startTime = new Date(now - 7 * 24 * 60 * 60 * 1000)
break
case '30d':
startTime = new Date(now - 30 * 24 * 60 * 60 * 1000)
break
default:
startTime = new Date(now - 24 * 60 * 60 * 1000)
}
return this.aggregate([
{
$match: {
timestamp: { $gte: startTime }
}
},
{
$group: {
_id: null,
totalConversations: { $sum: 1 },
uniqueUsers: { $addToSet: '$userId' },
averageResponseTime: { $avg: '$analysis.responseTime' },
topicDistribution: { $push: '$topics' },
sentimentDistribution: {
positive: { $sum: { $cond: [{ $eq: ['$message.sentiment', 'positive'] }, 1, 0] } },
neutral: { $sum: { $cond: [{ $eq: ['$message.sentiment', 'neutral'] }, 1, 0] } },
negative: { $sum: { $cond: [{ $eq: ['$message.sentiment', 'negative'] }, 1, 0] } }
},
modeDistribution: {
bubbe: { $sum: { $cond: [{ $eq: ['$response.mode', 'bubbe'] }, 1, 0] } },
meshugana: { $sum: { $cond: [{ $eq: ['$response.mode', 'meshugana'] }, 1, 0] } },
comedian: { $sum: { $cond: [{ $eq: ['$response.mode', 'comedian'] }, 1, 0] } }
},
flaggedConversations: { $sum: { $cond: ['$flags.flaggedContent', 1, 0] } },
voiceUsage: { $sum: { $cond: ['$response.voiceGenerated', 1, 0] } }
}
},
{
$addFields: {
uniqueUserCount: { $size: '$uniqueUsers' }
}
}
])
}
// Static method to get top topics
conversationSchema.statics.getTopTopics = function(limit = 10, timeframe = '24h') {
const now = new Date()
let startTime
switch (timeframe) {
case '1h':
startTime = new Date(now - 60 * 60 * 1000)
break
case '24h':
startTime = new Date(now - 24 * 60 * 60 * 1000)
break
case '7d':
startTime = new Date(now - 7 * 24 * 60 * 60 * 1000)
break
default:
startTime = new Date(now - 24 * 60 * 60 * 1000)
}
return this.aggregate([
{
$match: {
timestamp: { $gte: startTime }
}
},
{
$unwind: '$topics'
},
{
$group: {
_id: '$topics',
count: { $sum: 1 },
uniqueUsers: { $addToSet: '$userId' }
}
},
{
$addFields: {
uniqueUserCount: { $size: '$uniqueUsers' }
}
},
{
$sort: { count: -1 }
},
{
$limit: limit
}
])
}
// Static method to get flagged conversations
conversationSchema.statics.getFlagged = function(limit = 50) {
return this.find({
'flags.flaggedContent': true
})
.sort({ timestamp: -1 })
.limit(limit)
.populate('userId', 'profile.name profile.preferredName')
.lean()
}
module.exports = mongoose.model('Conversation', conversationSchema)