prompt-learning.js
1 /** 2 * Prompt Learning System 3 * Analyzes feedback patterns and suggests prompt improvements 4 */ 5 6 import { readFileSync } from 'fs'; 7 import { join } from 'path'; 8 import Logger from './logger.js'; 9 import { run, getOne, getAll } from './db.js'; 10 11 const logger = new Logger('PromptLearning'); 12 13 /** 14 * Log feedback for a prompt decision 15 * @param {Object} feedback 16 * @param {number} feedback.outreachId - Outreach ID 17 * @param {number} feedback.siteId - Site ID 18 * @param {string} feedback.promptFile - Which prompt file (PROPOSAL.md, etc.) 19 * @param {string} feedback.feedbackType - rework|rejected|approved|conversion|no_response 20 * @param {string} feedback.feedbackText - Operator's instructions/notes 21 * @returns {Promise<void>} 22 */ 23 export async function logPromptFeedback(feedback) { 24 // Auto-categorize feedback based on common patterns 25 const category = categorizeFeedback(feedback.feedbackText); 26 27 await run( 28 `INSERT INTO prompt_feedback ( 29 message_id, site_id, prompt_file, prompt_version, 30 feedback_type, feedback_text, feedback_category 31 ) VALUES ($1, $2, $3, $4, $5, $6, $7)`, 32 [ 33 feedback.outreachId || feedback.messageId, 34 feedback.siteId, 35 feedback.promptFile, 36 await getCurrentPromptVersion(feedback.promptFile), 37 feedback.feedbackType, 38 feedback.feedbackText, 39 category, 40 ] 41 ); 42 43 logger.info(`Logged ${feedback.feedbackType} feedback for ${feedback.promptFile}`); 44 } 45 46 /** 47 * Auto-categorize feedback text into themes 48 */ 49 function categorizeFeedback(text) { 50 if (!text) return null; 51 52 const lower = text.toLowerCase(); 53 54 // Pattern matching for common themes 55 const categories = { 56 tone: ['too salesy', 'pushy', 'aggressive', 'casual', 'formal', 'tone'], 57 length: ['too long', 'too short', 'verbose', 'brief', 'concise'], 58 urgency: ['urgent', 'scarcity', 'pressure', 'fomo'], 59 personalization: ['generic', 'personal', 'name', 'specific'], 60 value_prop: ['benefit', 'value', 'roi', 'offer'], 61 compliance: ['spam', 'gdpr', 'opt-out', 'unsubscribe'], 62 cta: ['call to action', 'cta', 'next step'], 63 subject_line: ['subject', 'headline', 'title'], 64 field_detection: [ 65 'textarea', 66 'message field', 67 'input', 68 'selector', 69 'field', 70 'wrong field', 71 'not the', 72 ], 73 }; 74 75 for (const [category, keywords] of Object.entries(categories)) { 76 if (keywords.some(kw => lower.includes(kw))) { 77 return category; 78 } 79 } 80 81 return 'other'; 82 } 83 84 /** 85 * Get current version of a prompt file 86 * @param {string} promptFile 87 * @returns {Promise<number>} 88 */ 89 async function getCurrentPromptVersion(promptFile) { 90 const result = await getOne( 91 `SELECT MAX(version) AS version 92 FROM prompt_versions 93 WHERE prompt_file = $1`, 94 [promptFile] 95 ); 96 97 return result?.version || 1; 98 } 99 100 /** 101 * Analyze feedback patterns and generate learning insights 102 * @param {string} promptFile - Which prompt to analyze (PROPOSAL.md, etc.) 103 * @param {number} days - Look back period (default: 30) 104 * @returns {Promise<object>} 105 */ 106 export async function analyzePromptFeedback(promptFile, days = 30) { 107 // Get feedback distribution 108 const distribution = await getAll( 109 `SELECT 110 feedback_type, 111 COUNT(*) AS count, 112 ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 1) AS percentage 113 FROM prompt_feedback 114 WHERE prompt_file = $1 115 AND created_at > NOW() - ($2 || ' days')::interval 116 GROUP BY feedback_type 117 ORDER BY count DESC`, 118 [promptFile, days] 119 ); 120 121 // Get category breakdown 122 const categories = await getAll( 123 `SELECT 124 feedback_category, 125 COUNT(*) AS count, 126 string_agg(feedback_text, ' | ') AS examples 127 FROM prompt_feedback 128 WHERE prompt_file = $1 129 AND created_at > NOW() - ($2 || ' days')::interval 130 AND feedback_type IN ('rework', 'rejected') 131 AND feedback_category IS NOT NULL 132 GROUP BY feedback_category 133 ORDER BY count DESC 134 LIMIT 10`, 135 [promptFile, days] 136 ); 137 138 // Get approval rate 139 const stats = await getOne( 140 `SELECT 141 COUNT(CASE WHEN feedback_type = 'approved' THEN 1 END) AS approved, 142 COUNT(CASE WHEN feedback_type = 'rework' THEN 1 END) AS rework, 143 COUNT(CASE WHEN feedback_type = 'rejected' THEN 1 END) AS rejected, 144 COUNT(CASE WHEN feedback_type = 'conversion' THEN 1 END) AS conversions, 145 COUNT(*) AS total 146 FROM prompt_feedback 147 WHERE prompt_file = $1 148 AND created_at > NOW() - ($2 || ' days')::interval`, 149 [promptFile, days] 150 ); 151 152 const approvalRate = 153 stats.total > 0 ? (((stats.approved + stats.conversions) / stats.total) * 100).toFixed(1) : 0; 154 155 return { 156 promptFile, 157 period: `${days} days`, 158 stats: { 159 ...stats, 160 approvalRate: parseFloat(approvalRate), 161 }, 162 distribution, 163 categories, 164 topIssues: categories.slice(0, 5), 165 }; 166 } 167 168 /** 169 * Generate prompt improvement recommendations 170 * @param {string} promptFile 171 * @returns {Promise<object>} 172 */ 173 export async function generatePromptRecommendations(promptFile) { 174 const analysis = await analyzePromptFeedback(promptFile, 30); 175 const recommendations = []; 176 177 // Low approval rate 178 if (analysis.stats.approvalRate < 70) { 179 recommendations.push({ 180 priority: 'high', 181 issue: `Low approval rate (${analysis.stats.approvalRate}%)`, 182 recommendation: 'Review most common rework categories and adjust prompt guidelines', 183 }); 184 } 185 186 // Analyze top categories 187 for (const category of analysis.topIssues) { 188 if (category.count >= 3) { 189 recommendations.push({ 190 priority: 'medium', 191 issue: `Recurring ${category.feedback_category} issues (${category.count} instances)`, 192 recommendation: `Add specific guidance to prompt about ${category.feedback_category}`, 193 examples: category.examples.split(' | ').slice(0, 3), 194 }); 195 } 196 } 197 198 return { 199 ...analysis, 200 recommendations, 201 }; 202 } 203 204 /** 205 * Version a prompt file before making changes 206 * @param {string} promptFile 207 * @param {string} changeSummary 208 * @param {string} learningApplied 209 * @returns {Promise<number>} Next version number 210 */ 211 export async function versionPromptFile(promptFile, changeSummary, learningApplied) { 212 const projectRoot = process.cwd(); 213 const promptPath = join(projectRoot, 'prompts', promptFile); 214 215 // Read current prompt content 216 const content = readFileSync(promptPath, 'utf-8'); 217 218 // Get next version number 219 const currentVersion = await getCurrentPromptVersion(promptFile); 220 const nextVersion = currentVersion + 1; 221 222 // Store old version 223 await run( 224 `INSERT INTO prompt_versions (prompt_file, version, content, change_summary, learning_applied) 225 VALUES ($1, $2, $3, $4, $5)`, 226 [promptFile, currentVersion, content, changeSummary, learningApplied] 227 ); 228 229 logger.success(`Versioned ${promptFile} as v${currentVersion}`); 230 231 return nextVersion; 232 } 233 234 /** 235 * Get prompt version history 236 * @param {string} promptFile 237 * @param {number} limit 238 * @returns {Promise<object[]>} 239 */ 240 export async function getPromptHistory(promptFile, limit = 10) { 241 return await getAll( 242 `SELECT * FROM prompt_versions 243 WHERE prompt_file = $1 244 ORDER BY version DESC 245 LIMIT $2`, 246 [promptFile, limit] 247 ); 248 } 249 250 export default { 251 logPromptFeedback, 252 analyzePromptFeedback, 253 generatePromptRecommendations, 254 versionPromptFile, 255 getPromptHistory, 256 };