/** * Content Analyzer Service * Analyzes existing articles for tone, audience fit, themes, and publication suitability */ const ClaudeAPIService = require('./ClaudeAPI.service'); const publicationConfig = require('../config/publication-targets.config'); const logger = require('../utils/logger.util'); class ContentAnalyzerService { /** * Analyze article content for quality and publication fit * @param {Object} article - Article to analyze * @param {string} article.title - Article title * @param {string} article.content - Article content * @param {number} article.wordCount - Word count * @param {string} article.targetPublication - Publication ID (optional) * @returns {Promise} Analysis results */ async analyzeArticle(article) { logger.info(`Analyzing article: ${article.title}`); try { const claudeAPI = ClaudeAPIService; // Build system prompt for analysis const systemPrompt = `You are an expert content analyst evaluating articles about AI governance and the Tractatus framework. Your task is to analyze the provided article and return a comprehensive assessment in JSON format. Analyze for: 1. **Tone**: strategic, conversational, academic, investigative, persuasive 2. **Primary Audience**: leader, research, general, technical, policy 3. **Key Themes**: Extract 3-5 main themes discussed 4. **Strengths**: What works well (2-3 points) 5. **Weaknesses**: What could be improved (2-3 points) 6. **Publication Suitability**: If target publication specified, assess fit (1-10 score) Return JSON: { "tone": { "primary": "strategic|conversational|academic|investigative|persuasive", "secondary": "strategic|conversational|academic|investigative|persuasive", "confidence": 0.0-1.0 }, "audience": { "primary": "leader|research|general|technical|policy", "secondary": "leader|research|general|technical|policy", "readingLevel": "undergraduate|postgraduate|professional|general", "confidence": 0.0-1.0 }, "themes": [ { "theme": "Theme name", "prominence": 0.0-1.0, "description": "Brief description" } ], "strengths": [ "Strength description" ], "weaknesses": [ "Weakness description with specific suggestion" ], "publicationFit": { "score": 1-10, "reasoning": "Why this score", "recommendations": ["Specific changes to improve fit"] }, "tractatus": { "frameworkAlignment": 0.0-1.0, "quadrant": "STRATEGIC|OPERATIONAL|TACTICAL|MAINTENANCE", "valuesSensitive": boolean, "concerns": ["Any concerns about framework representation"] } }`; // Build user prompt with article details let userPrompt = `Analyze this article: **Title**: ${article.title} **Word Count**: ${article.wordCount} ${article.targetPublication ? `**Target Publication**: ${this._getPublicationName(article.targetPublication)}\n` : ''} **Content**: ${article.content} Provide comprehensive analysis in JSON format.`; // If target publication specified, include readership profile if (article.targetPublication) { const publication = publicationConfig.getPublicationById(article.targetPublication); if (publication && publication.readership) { userPrompt += `\n\n**Target Publication Profile**: - Name: ${publication.name} - Type: ${publication.type} - Primary Audience: ${publication.readership.primary} - Reader Interests: ${publication.readership.demographics.interests.join(', ')} - Editorial Tone: ${publication.editorial.tone.join(', ')} - Preferred Themes: ${publication.readership.contentPreferences.topicThemes.join(', ')} Assess how well this article fits this publication's readership.`; } } const messages = [ { role: 'user', content: userPrompt } ]; const response = await claudeAPI.sendMessage(messages, { system: systemPrompt, max_tokens: 2048, temperature: 0.3 // Lower temperature for consistent analysis }); const analysis = claudeAPI.extractJSON(response); logger.info(`Analysis complete for: ${article.title}`); return analysis; } catch (error) { logger.error('Content analysis error:', error); throw new Error(`Failed to analyze article: ${error.message}`); } } /** * Batch analyze multiple articles * @param {Array} articles - Articles to analyze * @returns {Promise>} Analysis results */ async batchAnalyze(articles) { logger.info(`Batch analyzing ${articles.length} articles`); const results = []; for (const article of articles) { try { const analysis = await this.analyzeArticle(article); results.push({ title: article.title, success: true, analysis }); } catch (error) { logger.error(`Failed to analyze ${article.title}:`, error); results.push({ title: article.title, success: false, error: error.message }); } } return results; } /** * Compare article against publication requirements * @param {Object} article - Article with content and metadata * @param {string} publicationId - Publication to compare against * @returns {Promise} Comparison results */ async compareToPublication(article, publicationId) { const publication = publicationConfig.getPublicationById(publicationId); if (!publication) { throw new Error(`Publication not found: ${publicationId}`); } logger.info(`Comparing "${article.title}" to ${publication.name}`); const analysis = await this.analyzeArticle({ ...article, targetPublication: publicationId }); // Build comparison report const comparison = { publication: { id: publication.id, name: publication.name, type: publication.type }, article: { title: article.title, wordCount: article.wordCount }, fit: { overall: analysis.publicationFit?.score || 0, reasoning: analysis.publicationFit?.reasoning || 'No fit assessment available' }, requirements: { wordCount: { required: publication.requirements?.wordCount || 'Not specified', actual: article.wordCount, meets: this._checkWordCount(article.wordCount, publication.requirements?.wordCount) }, tone: { preferred: publication.editorial?.tone || [], detected: analysis.tone.primary, matches: publication.editorial?.tone?.includes(analysis.tone.primary) }, audience: { target: publication.readership?.primary || 'Not specified', detected: analysis.audience.primary, matches: publication.readership?.primary === analysis.audience.primary } }, recommendations: analysis.publicationFit?.recommendations || [], analysis }; return comparison; } /** * Generate improvement suggestions based on analysis * @param {Object} analysis - Analysis results from analyzeArticle * @param {string} targetPublicationId - Optional target publication * @returns {Array} Prioritized improvement suggestions */ generateImprovements(analysis, targetPublicationId = null) { const improvements = []; // Check tone alignment if (targetPublicationId) { const publication = publicationConfig.getPublicationById(targetPublicationId); if (publication && publication.editorial) { const toneMatches = publication.editorial.tone.includes(analysis.tone.primary); if (!toneMatches) { improvements.push({ priority: 'high', category: 'tone', issue: `Tone mismatch: Article is ${analysis.tone.primary}, but ${publication.name} prefers ${publication.editorial.tone.join(' or ')}`, suggestion: `Revise to adopt a more ${publication.editorial.tone[0]} tone throughout` }); } } } // Check audience alignment if (analysis.audience.confidence < 0.7) { improvements.push({ priority: 'medium', category: 'audience', issue: 'Unclear target audience', suggestion: 'Clarify who this article is for and adjust language/examples accordingly' }); } // Add weaknesses as improvements if (analysis.weaknesses && analysis.weaknesses.length > 0) { analysis.weaknesses.forEach((weakness, index) => { improvements.push({ priority: index === 0 ? 'high' : 'medium', category: 'content', issue: weakness, suggestion: 'Address this weakness in revision' }); }); } // Check Tractatus alignment if (analysis.tractatus && analysis.tractatus.frameworkAlignment < 0.7) { improvements.push({ priority: 'high', category: 'framework', issue: 'Weak Tractatus framework alignment', suggestion: 'Strengthen connection to Tractatus principles and examples' }); } // Publication-specific recommendations if (analysis.publicationFit && analysis.publicationFit.recommendations) { analysis.publicationFit.recommendations.forEach(rec => { improvements.push({ priority: 'high', category: 'publication-fit', issue: 'Publication fit improvement', suggestion: rec }); }); } // Sort by priority const priorityOrder = { high: 0, medium: 1, low: 2 }; improvements.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]); return improvements; } /** * Helper: Get publication name by ID */ _getPublicationName(publicationId) { const publication = publicationConfig.getPublicationById(publicationId); return publication ? publication.name : publicationId; } /** * Helper: Check if word count meets requirements */ _checkWordCount(actual, required) { if (!required) return true; if (typeof required === 'string') { // Parse range like "750-1500" const match = required.match(/(\d+)-(\d+)/); if (match) { const min = parseInt(match[1]); const max = parseInt(match[2]); return actual >= min && actual <= max; } } return true; // Default to true if can't parse } } // Singleton instance let instance = null; module.exports = { getInstance: () => { if (!instance) { instance = new ContentAnalyzerService(); } return instance; }, ContentAnalyzerService };