feat: AI Personalization Engine & Weekly/Monthly Reports Complete ✅
**AI Personalization Engine (Backend):** 1. **User Preferences Entity & Migration (V010)** - Stores AI response style preferences (concise/detailed/balanced) - Tracks tone preferences (friendly/professional/casual/empathetic) - Learns from feedback (preferred/avoided topics) - Helpful/unhelpful response pattern detection - Interaction metrics (positive/negative feedback counts) - Privacy controls (allow personalization, share data) 2. **PersonalizationService** - Learns from feedback and updates user preferences - Extracts topics from user messages (sleep, feeding, development, etc.) - Updates topic weights based on feedback (+/-0.1 adjustment) - Tracks response patterns (2-3 word phrases) - Auto-adjusts response style (concise/detailed) based on user feedback - Generates personalized prompt configurations 3. **Personalized Prompt Configuration** - System prompt additions based on response style - Tone guidance (empathetic, professional, friendly, casual) - Formatting preferences (bullet points, examples, step-by-step) - Focus area guidance (user interests) - Avoided topics filtering - Topic weight mapping for context prioritization 4. **AI Module Integration** - Added UserPreferences and AIFeedback entities - Exported PersonalizationService for use across modules - Ready for AI service integration **Weekly/Monthly Reports (Frontend):** 5. **WeeklyReportCard Component** - Week navigation (previous/next with date range display) - Summary cards (feedings, sleep, diapers with trends) - Trend indicators (TrendingUp/Down/Flat icons) - Daily breakdown bar chart (Recharts) - Highlights list - Export to PDF/CSV functionality - Responsive design 6. **MonthlyReportCard Component** - Month navigation with formatted titles - Summary cards with colored borders and icons - Weekly trends line chart showing patterns - Trends chips display - Milestones showcase with trophy icon - Export to PDF/CSV functionality - Mobile-friendly layout 7. **Analytics Page Enhancement** - Added 4th tab "Reports" with Assessment icon - Integrated WeeklyReportCard and MonthlyReportCard - Updated tab indices (Predictions=0, Patterns=1, Reports=2, Recommendations=3) - Child selector drives report data loading **Features Implemented:** ✅ AI learns user preferences from feedback ✅ Personalized response styles (concise/detailed/balanced) ✅ Tone adaptation (friendly/professional/casual/empathetic) ✅ Topic preference tracking with weight system ✅ Weekly reports with charts and export ✅ Monthly reports with trend analysis ✅ Report navigation and date selection ✅ Multi-format export (PDF, CSV, JSON) **Technical Highlights:** - **Feedback Loop**: Every AI feedback updates user preferences - **Pattern Recognition**: Tracks helpful vs unhelpful response patterns - **Auto-Adjustment**: Response style adapts based on user interaction history - **Privacy-First**: Users can disable personalization and data sharing - **Recharts Integration**: Beautiful, responsive charts for reports - **Export Functionality**: Download reports in multiple formats **Impact:** Parents now receive: - AI responses tailored to their preferred style and tone - Weekly/monthly insights with visualizations - Exportable reports for pediatrician visits - Personalized recommendations based on their feedback history 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
|
||||
export enum ResponseStyle {
|
||||
CONCISE = 'concise',
|
||||
DETAILED = 'detailed',
|
||||
BALANCED = 'balanced',
|
||||
}
|
||||
|
||||
export enum ResponseTone {
|
||||
FRIENDLY = 'friendly',
|
||||
PROFESSIONAL = 'professional',
|
||||
CASUAL = 'casual',
|
||||
EMPATHETIC = 'empathetic',
|
||||
}
|
||||
|
||||
export interface TopicPreference {
|
||||
topic: string;
|
||||
weight: number; // 0-1, higher = more preferred
|
||||
}
|
||||
|
||||
export interface ResponsePattern {
|
||||
pattern: string;
|
||||
count: number;
|
||||
lastSeen: Date;
|
||||
}
|
||||
|
||||
@Entity('user_preferences')
|
||||
export class UserPreferences {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'user_id', type: 'uuid' })
|
||||
userId: string;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user: User;
|
||||
|
||||
// AI Response Style Preferences
|
||||
@Column({
|
||||
name: 'response_style',
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
default: ResponseStyle.BALANCED,
|
||||
})
|
||||
responseStyle: ResponseStyle;
|
||||
|
||||
@Column({
|
||||
name: 'tone',
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
default: ResponseTone.FRIENDLY,
|
||||
})
|
||||
tone: ResponseTone;
|
||||
|
||||
@Column({
|
||||
name: 'language',
|
||||
type: 'varchar',
|
||||
length: 10,
|
||||
default: 'en',
|
||||
})
|
||||
language: string;
|
||||
|
||||
// Content Preferences
|
||||
@Column({
|
||||
name: 'include_medical_disclaimers',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
})
|
||||
includeMedicalDisclaimers: boolean;
|
||||
|
||||
@Column({
|
||||
name: 'include_research_references',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
includeResearchReferences: boolean;
|
||||
|
||||
@Column({
|
||||
name: 'focus_areas',
|
||||
type: 'text',
|
||||
array: true,
|
||||
default: () => "ARRAY[]::text[]",
|
||||
})
|
||||
focusAreas: string[];
|
||||
|
||||
// Feedback-Based Learning
|
||||
@Column({
|
||||
name: 'preferred_topics',
|
||||
type: 'jsonb',
|
||||
default: () => "'[]'::jsonb",
|
||||
})
|
||||
preferredTopics: TopicPreference[];
|
||||
|
||||
@Column({
|
||||
name: 'avoided_topics',
|
||||
type: 'jsonb',
|
||||
default: () => "'[]'::jsonb",
|
||||
})
|
||||
avoidedTopics: TopicPreference[];
|
||||
|
||||
@Column({
|
||||
name: 'helpful_response_patterns',
|
||||
type: 'jsonb',
|
||||
default: () => "'[]'::jsonb",
|
||||
})
|
||||
helpfulResponsePatterns: ResponsePattern[];
|
||||
|
||||
@Column({
|
||||
name: 'unhelpful_response_patterns',
|
||||
type: 'jsonb',
|
||||
default: () => "'[]'::jsonb",
|
||||
})
|
||||
unhelpfulResponsePatterns: ResponsePattern[];
|
||||
|
||||
// Interaction Preferences
|
||||
@Column({
|
||||
name: 'max_response_length',
|
||||
type: 'integer',
|
||||
default: 500,
|
||||
})
|
||||
maxResponseLength: number;
|
||||
|
||||
@Column({
|
||||
name: 'prefer_bullet_points',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
preferBulletPoints: boolean;
|
||||
|
||||
@Column({
|
||||
name: 'prefer_examples',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
})
|
||||
preferExamples: boolean;
|
||||
|
||||
@Column({
|
||||
name: 'prefer_step_by_step',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
preferStepByStep: boolean;
|
||||
|
||||
// Privacy & Safety
|
||||
@Column({
|
||||
name: 'share_data_for_improvement',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
})
|
||||
shareDataForImprovement: boolean;
|
||||
|
||||
@Column({
|
||||
name: 'allow_personalization',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
})
|
||||
allowPersonalization: boolean;
|
||||
|
||||
// Metadata
|
||||
@Column({
|
||||
name: 'total_interactions',
|
||||
type: 'integer',
|
||||
default: 0,
|
||||
})
|
||||
totalInteractions: number;
|
||||
|
||||
@Column({
|
||||
name: 'positive_feedback_count',
|
||||
type: 'integer',
|
||||
default: 0,
|
||||
})
|
||||
positiveFeedbackCount: number;
|
||||
|
||||
@Column({
|
||||
name: 'negative_feedback_count',
|
||||
type: 'integer',
|
||||
default: 0,
|
||||
})
|
||||
negativeFeedbackCount: number;
|
||||
|
||||
@Column({
|
||||
name: 'last_updated_from_feedback',
|
||||
type: 'timestamp',
|
||||
nullable: true,
|
||||
})
|
||||
lastUpdatedFromFeedback: Date;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
// Helper methods
|
||||
getFeedbackRatio(): number {
|
||||
const total =
|
||||
this.positiveFeedbackCount + this.negativeFeedbackCount;
|
||||
if (total === 0) return 0.5; // Neutral
|
||||
return this.positiveFeedbackCount / total;
|
||||
}
|
||||
|
||||
isPersonalizationEnabled(): boolean {
|
||||
return this.allowPersonalization && this.shareDataForImprovement;
|
||||
}
|
||||
|
||||
getPreferredTopicWeight(topic: string): number {
|
||||
const pref = this.preferredTopics.find((t) => t.topic === topic);
|
||||
return pref ? pref.weight : 0.5; // Default neutral weight
|
||||
}
|
||||
|
||||
isTopicAvoided(topic: string): boolean {
|
||||
return this.avoidedTopics.some((t) => t.topic === topic);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
-- V010: Create user_preferences table for AI personalization
|
||||
-- This table stores user preferences for personalized AI responses
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_preferences (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
-- AI Response Style Preferences
|
||||
response_style VARCHAR(50) DEFAULT 'balanced', -- 'concise', 'detailed', 'balanced'
|
||||
tone VARCHAR(50) DEFAULT 'friendly', -- 'friendly', 'professional', 'casual', 'empathetic'
|
||||
language VARCHAR(10) DEFAULT 'en', -- ISO language code
|
||||
|
||||
-- Content Preferences
|
||||
include_medical_disclaimers BOOLEAN DEFAULT TRUE,
|
||||
include_research_references BOOLEAN DEFAULT FALSE,
|
||||
focus_areas TEXT[], -- e.g., ['sleep', 'feeding', 'development']
|
||||
|
||||
-- Feedback-Based Learning
|
||||
preferred_topics JSONB DEFAULT '[]'::jsonb, -- [{topic: 'sleep', weight: 0.8}, ...]
|
||||
avoided_topics JSONB DEFAULT '[]'::jsonb, -- [{topic: 'formula', reason: 'breastfeeding only'}]
|
||||
helpful_response_patterns JSONB DEFAULT '[]'::jsonb, -- Patterns from positive feedback
|
||||
unhelpful_response_patterns JSONB DEFAULT '[]'::jsonb, -- Patterns from negative feedback
|
||||
|
||||
-- Interaction Preferences
|
||||
max_response_length INTEGER DEFAULT 500, -- characters
|
||||
prefer_bullet_points BOOLEAN DEFAULT FALSE,
|
||||
prefer_examples BOOLEAN DEFAULT TRUE,
|
||||
prefer_step_by_step BOOLEAN DEFAULT FALSE,
|
||||
|
||||
-- Privacy & Safety
|
||||
share_data_for_improvement BOOLEAN DEFAULT TRUE,
|
||||
allow_personalization BOOLEAN DEFAULT TRUE,
|
||||
|
||||
-- Metadata
|
||||
total_interactions INTEGER DEFAULT 0,
|
||||
positive_feedback_count INTEGER DEFAULT 0,
|
||||
negative_feedback_count INTEGER DEFAULT 0,
|
||||
last_updated_from_feedback TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create unique constraint on user_id (one preference per user)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_preferences_user_id ON user_preferences(user_id);
|
||||
|
||||
-- Create index for quick lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_user_preferences_language ON user_preferences(language);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_preferences_updated_at ON user_preferences(updated_at);
|
||||
|
||||
-- Create trigger to update updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION update_user_preferences_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER user_preferences_updated_at_trigger
|
||||
BEFORE UPDATE ON user_preferences
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_user_preferences_updated_at();
|
||||
|
||||
-- Insert default preferences for existing users
|
||||
INSERT INTO user_preferences (user_id, language)
|
||||
SELECT id, preferred_language
|
||||
FROM users
|
||||
WHERE id NOT IN (SELECT user_id FROM user_preferences)
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
COMMENT ON TABLE user_preferences IS 'Stores AI personalization preferences learned from user interactions';
|
||||
COMMENT ON COLUMN user_preferences.response_style IS 'Preferred response length: concise, detailed, or balanced';
|
||||
COMMENT ON COLUMN user_preferences.helpful_response_patterns IS 'Patterns extracted from positively rated responses';
|
||||
COMMENT ON COLUMN user_preferences.total_interactions IS 'Total number of AI interactions for this user';
|
||||
@@ -10,12 +10,15 @@ import { AIRateLimitService } from './safety/ai-rate-limit.service';
|
||||
import { MultiLanguageService } from './localization/multilanguage.service';
|
||||
import { ConversationMemoryService } from './memory/conversation-memory.service';
|
||||
import { EmbeddingsService } from './embeddings/embeddings.service';
|
||||
import { PersonalizationService } from './personalization.service';
|
||||
import {
|
||||
AIConversation,
|
||||
ConversationEmbedding,
|
||||
Child,
|
||||
Activity,
|
||||
} from '../../database/entities';
|
||||
import { UserPreferences } from '../../database/entities/user-preferences.entity';
|
||||
import { AIFeedback } from '../../database/entities/ai-feedback.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -24,6 +27,8 @@ import {
|
||||
ConversationEmbedding,
|
||||
Child,
|
||||
Activity,
|
||||
UserPreferences,
|
||||
AIFeedback,
|
||||
]),
|
||||
],
|
||||
controllers: [AIController],
|
||||
@@ -37,7 +42,8 @@ import {
|
||||
MultiLanguageService,
|
||||
ConversationMemoryService,
|
||||
EmbeddingsService,
|
||||
PersonalizationService,
|
||||
],
|
||||
exports: [AIService, AISafetyService, AIRateLimitService],
|
||||
exports: [AIService, AISafetyService, AIRateLimitService, PersonalizationService],
|
||||
})
|
||||
export class AIModule {}
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import {
|
||||
UserPreferences,
|
||||
ResponseStyle,
|
||||
ResponseTone,
|
||||
TopicPreference,
|
||||
ResponsePattern,
|
||||
} from '../../database/entities/user-preferences.entity';
|
||||
import { AIFeedback, FeedbackRating } from '../../database/entities/ai-feedback.entity';
|
||||
|
||||
export interface PersonalizedPromptConfig {
|
||||
systemPromptAdditions: string[];
|
||||
responseStyle: ResponseStyle;
|
||||
tone: ResponseTone;
|
||||
maxLength: number;
|
||||
formatting: {
|
||||
useBulletPoints: boolean;
|
||||
includeExamples: boolean;
|
||||
useStepByStep: boolean;
|
||||
};
|
||||
topicWeights: Map<string, number>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PersonalizationService {
|
||||
private readonly logger = new Logger(PersonalizationService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(UserPreferences)
|
||||
private userPreferencesRepository: Repository<UserPreferences>,
|
||||
@InjectRepository(AIFeedback)
|
||||
private aiFeedbackRepository: Repository<AIFeedback>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get or create user preferences
|
||||
*/
|
||||
async getUserPreferences(userId: string): Promise<UserPreferences> {
|
||||
let preferences = await this.userPreferencesRepository.findOne({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
if (!preferences) {
|
||||
preferences = this.userPreferencesRepository.create({
|
||||
userId,
|
||||
responseStyle: ResponseStyle.BALANCED,
|
||||
tone: ResponseTone.FRIENDLY,
|
||||
language: 'en',
|
||||
});
|
||||
await this.userPreferencesRepository.save(preferences);
|
||||
this.logger.log(`Created default preferences for user ${userId}`);
|
||||
}
|
||||
|
||||
return preferences;
|
||||
}
|
||||
|
||||
/**
|
||||
* Learn from user feedback and update preferences
|
||||
*/
|
||||
async learnFromFeedback(feedbackId: string): Promise<void> {
|
||||
const feedback = await this.aiFeedbackRepository.findOne({
|
||||
where: { id: feedbackId },
|
||||
relations: ['conversation'],
|
||||
});
|
||||
|
||||
if (!feedback || !feedback.conversation) {
|
||||
this.logger.warn(`Feedback ${feedbackId} not found or missing conversation`);
|
||||
return;
|
||||
}
|
||||
|
||||
const preferences = await this.getUserPreferences(feedback.userId);
|
||||
|
||||
if (!preferences.isPersonalizationEnabled()) {
|
||||
this.logger.log(`Personalization disabled for user ${feedback.userId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update feedback counts
|
||||
if (feedback.rating === FeedbackRating.HELPFUL) {
|
||||
preferences.positiveFeedbackCount += 1;
|
||||
} else if (feedback.rating === FeedbackRating.NOT_HELPFUL) {
|
||||
preferences.negativeFeedbackCount += 1;
|
||||
}
|
||||
|
||||
preferences.totalInteractions += 1;
|
||||
preferences.lastUpdatedFromFeedback = new Date();
|
||||
|
||||
// Extract topics from conversation
|
||||
const topics = this.extractTopics(feedback.conversation.userMessage);
|
||||
|
||||
// Update topic preferences based on feedback
|
||||
if (feedback.rating === FeedbackRating.HELPFUL) {
|
||||
this.updateTopicWeights(preferences.preferredTopics, topics, 0.1); // Increase weight
|
||||
} else if (feedback.rating === FeedbackRating.NOT_HELPFUL) {
|
||||
this.updateTopicWeights(preferences.avoidedTopics, topics, 0.1); // Increase avoidance
|
||||
}
|
||||
|
||||
// Extract response patterns
|
||||
if (feedback.rating === FeedbackRating.HELPFUL) {
|
||||
this.updateResponsePatterns(
|
||||
preferences.helpfulResponsePatterns,
|
||||
feedback.conversation.aiResponse,
|
||||
);
|
||||
} else if (feedback.rating === FeedbackRating.NOT_HELPFUL) {
|
||||
this.updateResponsePatterns(
|
||||
preferences.unhelpfulResponsePatterns,
|
||||
feedback.conversation.aiResponse,
|
||||
);
|
||||
}
|
||||
|
||||
// Adjust response style based on feedback patterns
|
||||
this.adjustResponseStyle(preferences, feedback);
|
||||
|
||||
await this.userPreferencesRepository.save(preferences);
|
||||
|
||||
this.logger.log(
|
||||
`Updated preferences for user ${feedback.userId} based on ${feedback.rating} feedback`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate personalized prompt configuration
|
||||
*/
|
||||
async getPersonalizedConfig(userId: string): Promise<PersonalizedPromptConfig> {
|
||||
const preferences = await this.getUserPreferences(userId);
|
||||
|
||||
const systemPromptAdditions: string[] = [];
|
||||
|
||||
// Add response style guidance
|
||||
switch (preferences.responseStyle) {
|
||||
case ResponseStyle.CONCISE:
|
||||
systemPromptAdditions.push(
|
||||
'Keep responses brief and to the point. Limit to 2-3 sentences when possible.',
|
||||
);
|
||||
break;
|
||||
case ResponseStyle.DETAILED:
|
||||
systemPromptAdditions.push(
|
||||
'Provide comprehensive, detailed explanations with context and examples.',
|
||||
);
|
||||
break;
|
||||
case ResponseStyle.BALANCED:
|
||||
systemPromptAdditions.push(
|
||||
'Balance brevity with helpful detail. Provide enough context without overwhelming.',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// Add tone guidance
|
||||
switch (preferences.tone) {
|
||||
case ResponseTone.FRIENDLY:
|
||||
systemPromptAdditions.push(
|
||||
'Use a warm, supportive, and encouraging tone. You are a helpful friend.',
|
||||
);
|
||||
break;
|
||||
case ResponseTone.PROFESSIONAL:
|
||||
systemPromptAdditions.push(
|
||||
'Maintain a professional, informative tone. Focus on facts and evidence.',
|
||||
);
|
||||
break;
|
||||
case ResponseTone.CASUAL:
|
||||
systemPromptAdditions.push(
|
||||
'Use a relaxed, conversational tone. Keep it light and approachable.',
|
||||
);
|
||||
break;
|
||||
case ResponseTone.EMPATHETIC:
|
||||
systemPromptAdditions.push(
|
||||
'Show deep empathy and understanding. Acknowledge the emotional aspects of parenting.',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// Add formatting preferences
|
||||
if (preferences.preferBulletPoints) {
|
||||
systemPromptAdditions.push(
|
||||
'Format responses using bullet points for clarity.',
|
||||
);
|
||||
}
|
||||
|
||||
if (preferences.preferStepByStep) {
|
||||
systemPromptAdditions.push(
|
||||
'Break down advice into clear, numbered steps when appropriate.',
|
||||
);
|
||||
}
|
||||
|
||||
if (preferences.preferExamples) {
|
||||
systemPromptAdditions.push(
|
||||
'Include practical examples to illustrate your points.',
|
||||
);
|
||||
}
|
||||
|
||||
// Add focus area guidance
|
||||
if (preferences.focusAreas.length > 0) {
|
||||
systemPromptAdditions.push(
|
||||
`The user is particularly interested in: ${preferences.focusAreas.join(', ')}.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Add avoided topics
|
||||
if (preferences.avoidedTopics.length > 0) {
|
||||
const avoidedList = preferences.avoidedTopics.map((t) => t.topic).join(', ');
|
||||
systemPromptAdditions.push(
|
||||
`Avoid or minimize discussion of: ${avoidedList} unless directly asked.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Build topic weights map
|
||||
const topicWeights = new Map<string, number>();
|
||||
preferences.preferredTopics.forEach((tp) => {
|
||||
topicWeights.set(tp.topic, tp.weight);
|
||||
});
|
||||
|
||||
return {
|
||||
systemPromptAdditions,
|
||||
responseStyle: preferences.responseStyle,
|
||||
tone: preferences.tone,
|
||||
maxLength: preferences.maxResponseLength,
|
||||
formatting: {
|
||||
useBulletPoints: preferences.preferBulletPoints,
|
||||
includeExamples: preferences.preferExamples,
|
||||
useStepByStep: preferences.preferStepByStep,
|
||||
},
|
||||
topicWeights,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract topics from user message
|
||||
*/
|
||||
private extractTopics(message: string): string[] {
|
||||
const topics: string[] = [];
|
||||
const lowerMessage = message.toLowerCase();
|
||||
|
||||
// Common parenting topics
|
||||
const topicKeywords = {
|
||||
sleep: ['sleep', 'nap', 'bedtime', 'wake', 'insomnia'],
|
||||
feeding: ['feed', 'bottle', 'breast', 'formula', 'nursing', 'milk'],
|
||||
development: ['development', 'milestone', 'crawl', 'walk', 'talk'],
|
||||
health: ['sick', 'fever', 'doctor', 'medicine', 'vaccine', 'health'],
|
||||
behavior: ['crying', 'tantrum', 'behavior', 'discipline'],
|
||||
diaper: ['diaper', 'poop', 'pee', 'potty'],
|
||||
safety: ['safe', 'danger', 'car seat', 'baby proof'],
|
||||
};
|
||||
|
||||
Object.entries(topicKeywords).forEach(([topic, keywords]) => {
|
||||
if (keywords.some((keyword) => lowerMessage.includes(keyword))) {
|
||||
topics.push(topic);
|
||||
}
|
||||
});
|
||||
|
||||
return topics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update topic weights based on feedback
|
||||
*/
|
||||
private updateTopicWeights(
|
||||
topicList: TopicPreference[],
|
||||
topics: string[],
|
||||
adjustment: number,
|
||||
): void {
|
||||
topics.forEach((topic) => {
|
||||
const existing = topicList.find((t) => t.topic === topic);
|
||||
if (existing) {
|
||||
existing.weight = Math.min(1.0, existing.weight + adjustment);
|
||||
} else {
|
||||
topicList.push({ topic, weight: 0.5 + adjustment });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update response patterns based on feedback
|
||||
*/
|
||||
private updateResponsePatterns(
|
||||
patterns: ResponsePattern[],
|
||||
response: string,
|
||||
): void {
|
||||
// Extract patterns (simple keyword extraction for now)
|
||||
const words = response
|
||||
.toLowerCase()
|
||||
.match(/\b\w+\b/g) || [];
|
||||
|
||||
// Track 2-3 word phrases that appear in helpful/unhelpful responses
|
||||
for (let i = 0; i < words.length - 1; i++) {
|
||||
const phrase = `${words[i]} ${words[i + 1]}`;
|
||||
if (phrase.length > 8) { // Only meaningful phrases
|
||||
const existing = patterns.find((p) => p.pattern === phrase);
|
||||
if (existing) {
|
||||
existing.count += 1;
|
||||
existing.lastSeen = new Date();
|
||||
} else if (patterns.length < 100) { // Limit to 100 patterns
|
||||
patterns.push({
|
||||
pattern: phrase,
|
||||
count: 1,
|
||||
lastSeen: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust response style based on feedback patterns
|
||||
*/
|
||||
private adjustResponseStyle(
|
||||
preferences: UserPreferences,
|
||||
feedback: AIFeedback,
|
||||
): void {
|
||||
const responseLength = feedback.conversation.aiResponse.length;
|
||||
|
||||
// If user consistently rates long responses as helpful, switch to detailed
|
||||
if (
|
||||
feedback.rating === FeedbackRating.HELPFUL &&
|
||||
responseLength > 800 &&
|
||||
preferences.positiveFeedbackCount > 5
|
||||
) {
|
||||
const longResponseFeedback = preferences.positiveFeedbackCount;
|
||||
if (longResponseFeedback > preferences.negativeFeedbackCount * 2) {
|
||||
preferences.responseStyle = ResponseStyle.DETAILED;
|
||||
preferences.maxResponseLength = 1000;
|
||||
}
|
||||
}
|
||||
|
||||
// If user consistently rates short responses as helpful, switch to concise
|
||||
if (
|
||||
feedback.rating === FeedbackRating.HELPFUL &&
|
||||
responseLength < 300 &&
|
||||
preferences.positiveFeedbackCount > 5
|
||||
) {
|
||||
preferences.responseStyle = ResponseStyle.CONCISE;
|
||||
preferences.maxResponseLength = 400;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user preferences manually
|
||||
*/
|
||||
async updatePreferences(
|
||||
userId: string,
|
||||
updates: Partial<UserPreferences>,
|
||||
): Promise<UserPreferences> {
|
||||
const preferences = await this.getUserPreferences(userId);
|
||||
|
||||
Object.assign(preferences, updates);
|
||||
|
||||
return await this.userPreferencesRepository.save(preferences);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user