/ src / utils / prompt-learning.js
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  };