UNPKG

story-weaver-ai

Version:

A narrative development system for AI-driven storytelling with Jungian psychology

1,321 lines (1,123 loc) 52.9 kB
/** * ai-services.js * AI service interactions for the Story Weaver CLI */ // NOTE/TODO: Include the beta header output-128k-2025-02-19 in your API request to increase the maximum output token length to 128k tokens for Claude 3.7 Sonnet. import { Anthropic } from '@anthropic-ai/sdk'; import OpenAI from 'openai'; import dotenv from 'dotenv'; import { CONFIG, log, sanitizePrompt } from './utils.js'; import { startLoadingIndicator, stopLoadingIndicator } from './ui.js'; import chalk from 'chalk'; // Load environment variables dotenv.config(); // Configure Anthropic client const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, // Add beta header for 128k token output defaultHeaders: { 'anthropic-beta': 'output-128k-2025-02-19' } }); // Lazy-loaded Perplexity client let perplexity = null; /** * Get or initialize the Perplexity client * @returns {OpenAI} Perplexity client */ function getPerplexityClient() { if (!perplexity) { if (!process.env.PERPLEXITY_API_KEY) { throw new Error("PERPLEXITY_API_KEY environment variable is missing. Set it to use research-backed features."); } perplexity = new OpenAI({ apiKey: process.env.PERPLEXITY_API_KEY, baseURL: 'https://api.perplexity.ai', }); } return perplexity; } /** * Handle Claude API errors with user-friendly messages * @param {Error} error - The error from Claude API * @returns {string} User-friendly error message */ function handleClaudeError(error) { // Check if it's a structured error response if (error.type === 'error' && error.error) { switch (error.error.type) { case 'overloaded_error': return 'Claude is currently experiencing high demand and is overloaded. Please wait a few minutes and try again.'; case 'rate_limit_error': return 'You have exceeded the rate limit. Please wait a few minutes before making more requests.'; case 'invalid_request_error': return 'There was an issue with the request format. If this persists, please report it as a bug.'; default: return `Claude API error: ${error.error.message}`; } } // Check for network/timeout errors if (error.message?.toLowerCase().includes('timeout')) { return 'The request to Claude timed out. Please try again.'; } if (error.message?.toLowerCase().includes('network')) { return 'There was a network error connecting to Claude. Please check your internet connection and try again.'; } // Default error message return `Error communicating with Claude: ${error.message}`; } /** * Analyze the complexity of a PRD to help determine optimal task count * @param {string} prdContent - The content of the PRD * @returns {Promise<Object>} Complexity metrics */ async function analyzePRDComplexity(prdContent) { try { log('info', 'Analyzing PRD complexity to determine optimal task count...'); const promptSystem = `You are a project complexity analysis expert. Analyze the provided PRD (Product Requirements Document) and determine its complexity metrics to help in planning tasks. Respond with a JSON object containing these metrics: { "overallComplexity": number (1-10 scale, 10 being extremely complex), "featureCount": number (count of distinct features/components), "technicalComplexity": number (1-10 scale), "integrationsCount": number (count of external systems/APIs to integrate), "domainComplexity": number (1-10 scale, how specialized is the domain knowledge), "recommendedTaskCount": number (your recommendation for task count), "reasoning": string (brief explanation of your assessment) }`; // Use a simpler prompt for complexity assessment to save tokens const response = await anthropic.messages.create({ model: CONFIG.model, max_tokens: 1000, // Smaller response needed temperature: 0.2, // More deterministic system: promptSystem, messages: [ { role: 'user', content: `Please analyze this PRD for complexity metrics to help determine task planning:\n\n${prdContent}` } ] }); // Parse the response const responseText = response.content[0].text; // Extract JSON from the response const jsonMatch = responseText.match(/({[\s\S]*})/); if (!jsonMatch) { throw new Error('Could not extract JSON from complexity analysis response'); } const complexityData = JSON.parse(jsonMatch[1]); log('info', `PRD complexity analysis: ${complexityData.overallComplexity}/10, recommended tasks: ${complexityData.recommendedTaskCount}`); return complexityData; } catch (error) { log('error', `Error analyzing PRD complexity: ${error.message}`); // Return default metrics if analysis fails return { overallComplexity: 5, featureCount: 5, technicalComplexity: 5, integrationsCount: 2, domainComplexity: 5, recommendedTaskCount: 10, reasoning: "Default metrics due to analysis failure" }; } } /** * Determine the optimal number of tasks based on PRD complexity or use explicit count * @param {string} prdContent - The content of the PRD * @param {number|null} explicitTaskCount - Optional explicit number of tasks * @returns {Promise<number>} The optimal task count */ async function determineOptimalTaskCount(prdContent, explicitTaskCount = null) { // If explicit count is provided, use it if (explicitTaskCount !== null) { log('info', `Using explicitly provided task count: ${explicitTaskCount}`); return explicitTaskCount; } // Otherwise analyze PRD complexity to determine optimal count try { const complexity = await analyzePRDComplexity(prdContent); // Calculate task count based on complexity metrics let baseTaskCount = 10; // Default moderate number of tasks if (complexity.wordCount > 2000) { baseTaskCount += Math.floor(complexity.wordCount / 500); } if (complexity.featureCount) { baseTaskCount += complexity.featureCount * 2; } // Add tasks based on technical complexity if (complexity.technicalComplexity === 'high') { baseTaskCount += 8; } else if (complexity.technicalComplexity === 'medium') { baseTaskCount += 4; } // Cap at reasonable limits const taskCount = Math.max(5, Math.min(30, baseTaskCount)); log('info', `Determined optimal task count: ${taskCount} based on complexity analysis`); return taskCount; } catch (error) { log('warn', `Error analyzing complexity: ${error.message}, using default task count of 12`); return 12; // Fallback to reasonable default } } /** * Call Claude to generate tasks based on a PRD * @param {string} prdContent - The content of the PRD * @param {string} prdPath - Path to the PRD file * @param {number|null} explicitTaskCount - Optional explicit number of tasks to generate * @returns {Object} Claude's response with generated tasks */ async function callClaudeForTasks(prdContent, prdPath, explicitTaskCount = null) { try { // Determine the optimal task count const taskCount = await determineOptimalTaskCount(prdContent, explicitTaskCount); log('info', `Generating ${taskCount} tasks from PRD: ${prdPath}`); // Construct the system prompt for Claude const systemPrompt = `You are an AI assistant specialized in breaking down Product Requirements Documents (PRDs) into specific, actionable development tasks. Your job is to analyze the PRD and create ${taskCount} well-defined tasks that would collectively implement all requirements. Each task should have this structure: { "id": number (1-indexed), "title": string (concise task name), "description": string (detailed explanation), "status": "todo", "dependencies": number[] (IDs of tasks this depends on), "complexity": "high" | "medium" | "low", "priority": "high" | "medium" | "low", "estimatedHours": number (reasonable estimate), "skills": string[] (technical skills needed), "implementation_notes": string (technical guidance) } Guidelines: 1. Create exactly ${taskCount} tasks, numbered from 1 to ${taskCount} 2. Start with foundational/architectural tasks 3. Break features into logical implementation steps 4. Set appropriate dependencies (a task can only depend on tasks with lower IDs) 5. Ensure comprehensive coverage of all requirements 6. Provide specific technical implementation guidance 7. Focus on clarity and actionability Expected output format: { "tasks": [ { "id": 1, "title": "Setup Project Structure", "description": "...", ... }, ... ], "metadata": { "projectName": "Implementation of PRD", "totalTasks": ${taskCount}, "sourceFile": "${prdPath}", "generatedAt": "YYYY-MM-DD" } } Important: Your response must be valid JSON only, with no additional explanation or comments.`; // Send request to Claude log('info', `Sending request to Claude to generate ${taskCount} tasks...`); const response = await anthropic.messages.create({ model: CONFIG.model, max_tokens: CONFIG.maxTokens, temperature: CONFIG.temperature, system: systemPrompt, messages: [ { role: 'user', content: `Here's the Product Requirements Document (PRD) to break down into ${taskCount} tasks:\n\n${prdContent}` } ] }); log('success', `Successfully received response from Claude API!`); // Extract content from response const content = response.content[0].text; // Process the response to extract valid JSON try { // Find JSON content boundaries const jsonStart = content.indexOf('{'); const jsonEnd = content.lastIndexOf('}'); if (jsonStart === -1 || jsonEnd === -1) { throw new Error("Could not find valid JSON in Claude's response"); } // Extract and parse JSON const jsonContent = content.substring(jsonStart, jsonEnd + 1); const parsedData = JSON.parse(jsonContent); // Validate the response structure if (!parsedData.tasks || !Array.isArray(parsedData.tasks)) { throw new Error("Claude's response does not contain a valid tasks array"); } // Log success and return the data log('info', `Successfully parsed ${parsedData.tasks.length} tasks from Claude's response`); return parsedData; } catch (error) { log('error', `Error processing Claude's response: ${error.message}`); throw new Error(`Failed to process Claude's response: ${error.message}`); } } catch (error) { log('error', `Error calling Claude: ${error.message}`); // Provide a more user-friendly error message if (error.status === 429) { throw new Error("Claude API rate limit exceeded. Please try again in a few minutes."); } else if (error.status >= 500) { throw new Error("Claude API is experiencing issues. Please try again later."); } else { throw new Error(`Error generating tasks: ${error.message}`); } } } /** * Call Claude to generate story elements from a concept * @param {string} conceptContent - Concept content * @param {string} conceptPath - Path to the concept file * @param {number} numElements - Number of elements to generate * @param {number} retryCount - Retry count * @returns {Object} Claude's response */ async function callClaude(conceptContent, conceptPath, numElements, retryCount = 0) { try { log('info', 'Calling Claude...'); // Build the system prompt const systemPrompt = `You are an AI assistant helping to break down a story concept into narrative elements using Jungian psychology principles. Your goal is to create ${numElements} well-structured, psychologically rich story elements based on the concept provided. Each element should follow this JSON structure: { "id": number, "title": string, "description": string, "status": "draft", "dependencies": number[] (IDs of elements this depends on), "archetypes": string[] (Jungian archetypes this element represents), "themes": string[] (psychological themes or motifs), "priority": "high" | "medium" | "low", "details": string (narrative development details), "criticalAnalysis": string (Jungian psychological interpretation) } Guidelines: 1. Create exactly ${numElements} story elements, numbered from 1 to ${numElements} 2. Each element should focus on a significant narrative component (character, setting, plot point, theme, etc.) 3. Order elements logically - consider narrative progression and psychological development 4. Incorporate Jungian archetypes (Hero, Shadow, Anima/Animus, Wise Old Man/Woman, Trickster, etc.) 5. Include Jungian psychological concepts (individuation, shadow integration, collective unconscious, etc.) 6. Set appropriate dependency IDs (an element can only depend on elements with lower IDs) 7. Assign priority (high/medium/low) based on narrative importance 8. Include detailed narrative development guidance in the "details" field 9. Provide psychological interpretation in the "criticalAnalysis" field Expected output format: { "elements": [ { "id": 1, "title": "The Protagonist's Call to Adventure", "description": "...", "archetypes": ["Hero", "Child"], "themes": ["Separation", "Awakening"], ... }, ... ], "metadata": { "storyName": "Story from Concept", "totalElements": ${numElements}, "sourceFile": "${conceptPath}", "generatedAt": "YYYY-MM-DD" } } Important: Your response must be valid JSON only, with no additional explanation or comments.`; // Use streaming request to handle large responses and show progress return await handleStreamingRequest(conceptContent, conceptPath, numElements, CONFIG.maxTokens, systemPrompt); } catch (error) { // Get user-friendly error message const userMessage = handleClaudeError(error); log('error', userMessage); // Retry logic for certain errors if (retryCount < 2 && ( error.error?.type === 'overloaded_error' || error.error?.type === 'rate_limit_error' || error.message?.toLowerCase().includes('timeout') || error.message?.toLowerCase().includes('network') )) { const waitTime = (retryCount + 1) * 5000; // 5s, then 10s log('info', `Waiting ${waitTime/1000} seconds before retry ${retryCount + 1}/2...`); await new Promise(resolve => setTimeout(resolve, waitTime)); return await callClaude(conceptContent, conceptPath, numElements, retryCount + 1); } else { console.error(chalk.red(userMessage)); if (CONFIG.debug) { log('debug', 'Full error:', error); } throw new Error(userMessage); } } } /** * Handle streaming request to Claude * @param {string} prdContent - PRD content * @param {string} prdPath - Path to the PRD file * @param {number} numTasks - Number of tasks to generate * @param {number} maxTokens - Maximum tokens * @param {string} systemPrompt - System prompt * @returns {Object} Claude's response */ async function handleStreamingRequest(prdContent, prdPath, numTasks, maxTokens, systemPrompt) { const loadingIndicator = startLoadingIndicator('Generating tasks from PRD...'); let responseText = ''; let streamingInterval = null; try { // Use streaming for handling large responses const stream = await anthropic.messages.create({ model: CONFIG.model, max_tokens: maxTokens, temperature: CONFIG.temperature, system: systemPrompt, messages: [ { role: 'user', content: `Here's the Product Requirements Document (PRD) to break down into ${numTasks} tasks:\n\n${prdContent}` } ], stream: true }); // Update loading indicator to show streaming progress let dotCount = 0; const readline = await import('readline'); streamingInterval = setInterval(() => { readline.cursorTo(process.stdout, 0); process.stdout.write(`Receiving streaming response from Claude${'.'.repeat(dotCount)}`); dotCount = (dotCount + 1) % 4; }, 500); // Process the stream for await (const chunk of stream) { if (chunk.type === 'content_block_delta' && chunk.delta.text) { responseText += chunk.delta.text; } } if (streamingInterval) clearInterval(streamingInterval); stopLoadingIndicator(loadingIndicator); log('info', "Completed streaming response from Claude API!"); return processClaudeResponse(responseText, numTasks, 0, prdContent, prdPath); } catch (error) { if (streamingInterval) clearInterval(streamingInterval); stopLoadingIndicator(loadingIndicator); // Get user-friendly error message const userMessage = handleClaudeError(error); log('error', userMessage); console.error(chalk.red(userMessage)); if (CONFIG.debug) { log('debug', 'Full error:', error); } throw new Error(userMessage); } } /** * Process Claude's response * @param {string} textContent - Text content from Claude * @param {number} numTasks - Number of tasks * @param {number} retryCount - Retry count * @param {string} prdContent - PRD content * @param {string} prdPath - Path to the PRD file * @returns {Object} Processed response */ function processClaudeResponse(textContent, numTasks, retryCount, prdContent, prdPath) { try { // Attempt to parse the JSON response let jsonStart = textContent.indexOf('{'); let jsonEnd = textContent.lastIndexOf('}'); if (jsonStart === -1 || jsonEnd === -1) { throw new Error("Could not find valid JSON in Claude's response"); } let jsonContent = textContent.substring(jsonStart, jsonEnd + 1); let parsedData = JSON.parse(jsonContent); // Validate the structure of the generated tasks if (!parsedData.tasks || !Array.isArray(parsedData.tasks)) { throw new Error("Claude's response does not contain a valid tasks array"); } // Ensure we have the correct number of tasks if (parsedData.tasks.length !== numTasks) { log('warn', `Expected ${numTasks} tasks, but received ${parsedData.tasks.length}`); } // Add metadata if missing if (!parsedData.metadata) { parsedData.metadata = { projectName: "PRD Implementation", totalTasks: parsedData.tasks.length, sourceFile: prdPath, generatedAt: new Date().toISOString().split('T')[0] }; } return parsedData; } catch (error) { log('error', "Error processing Claude's response:", error.message); // Retry logic if (retryCount < 2) { log('info', `Retrying to parse response (${retryCount + 1}/2)...`); // Try again with Claude for a cleaner response if (retryCount === 1) { log('info', "Calling Claude again for a cleaner response..."); return callClaude(prdContent, prdPath, numTasks, retryCount + 1); } return processClaudeResponse(textContent, numTasks, retryCount + 1, prdContent, prdPath); } else { throw error; } } } /** * Generate subelements for a story element * @param {Object} element - The story element to expand * @param {number} numSubelements - Number of subelements to generate * @param {number} nextSubelementId - ID to start numbering from * @param {string} additionalContext - Additional context for generation * @returns {Object} Generated subelements */ async function generateSubelements(element, numSubelements, nextSubelementId, additionalContext = '') { try { // Build the system prompt const systemPrompt = `You are an AI assistant helping to break down a story element into more detailed subelements using Jungian psychology principles. Main element: Title: ${element.title} Description: ${element.description} Archetypes: ${element.archetypes?.join(', ') || 'None specified'} Themes: ${element.themes?.join(', ') || 'None specified'} Details: ${element.details || 'None provided'} Please create ${numSubelements} specific subelements that together would enrich this story element. Each subelement should explore a facet of the main element through a Jungian psychological lens. For each subelement, provide: 1. A clear, evocative title 2. A concise description that connects to the main element 3. Any dependencies on other subelements (can be empty if no dependencies) 4. The specific archetype relation - how this subelement connects to Jungian archetypes Additional context to consider: ${additionalContext} Your response should be in JSON format with an array of subelements: { "subelements": [ { "id": ${nextSubelementId}, "title": "Subelement Title", "description": "Subelement description", "status": "draft", "dependencies": [], "archetypeRelation": "Specific archetype relation" }, ... ] }`; log('info', `Generating ${numSubelements} subelements for element: ${element.title}`); // Call Claude to generate the subelements const response = await anthropic.messages.create({ model: CONFIG.model, max_tokens: CONFIG.maxTokens, temperature: CONFIG.temperature, system: systemPrompt, messages: [ { role: 'user', content: `Please generate ${numSubelements} subelements for the story element described above. Focus on Jungian psychological depth and narrative richness.` } ] }); // Parse the response const responseContent = response.content[0].text; // Try to extract JSON from the response try { // Find JSON in the response (it might be surrounded by markdown code blocks or other text) const jsonMatch = responseContent.match(/```(?:json)?\s*([\s\S]*?)\s*```/) || responseContent.match(/({[\s\S]*})/); const jsonContent = jsonMatch ? jsonMatch[1] : responseContent; const result = JSON.parse(jsonContent); // Validate the result if (!result.subelements || !Array.isArray(result.subelements)) { throw new Error('Invalid response format: missing subelements array'); } // Ensure IDs are sequential and start from nextSubelementId result.subelements.forEach((subelement, index) => { subelement.id = nextSubelementId + index; }); return result; } catch (parseError) { log('error', `Failed to parse subelements from Claude's response: ${parseError.message}`); // Fallback: Try to extract subelements using regex if JSON parsing failed return parseSubelementsFromText(responseContent, nextSubelementId, numSubelements, element.id); } } catch (error) { log('error', `Error generating subelements: ${error.message}`); throw error; } } /** * Generate subelements with Perplexity for research-backed narrative development * @param {Object} element - The story element to expand * @param {number} numSubelements - Number of subelements to generate * @param {number} nextSubelementId - ID to start numbering from * @param {string} additionalContext - Additional context for generation * @returns {Object} Generated subelements */ async function generateSubelementsWithPerplexity(element, numSubelements = 3, nextSubelementId = 1, additionalContext = '') { try { // Get or initialize the Perplexity client const client = getPerplexityClient(); // Build the prompt const prompt = `As a literary analyst with expertise in Jungian psychology, help me break down this story element into ${numSubelements} well-researched subelements: STORY ELEMENT: Title: ${element.title} Description: ${element.description} Archetypes: ${element.archetypes?.join(', ') || 'None specified'} Themes: ${element.themes?.join(', ') || 'None specified'} Details: ${element.details || 'None provided'} ${additionalContext ? `ADDITIONAL CONTEXT: ${additionalContext}\n\n` : ''} For each subelement: 1. Create a title that connects to both the main element and Jungian psychology 2. Write a description that incorporates historical, literary, or mythological references 3. Identify how it relates to specific Jungian archetypes or psychological concepts 4. Explain its narrative significance within the larger story Return your response as a JSON object with the following structure: { "subelements": [ { "id": ${nextSubelementId}, "title": "Researched subelement title", "description": "Detailed description with research backing", "status": "draft", "dependencies": [], "archetypeRelation": "Detailed explanation of archetype connection with Jungian theory" }, ... ] }`; log('info', `Generating ${numSubelements} research-backed subelements for element: ${element.title}`); // Call Perplexity to generate the subelements const response = await client.chat.completions.create({ model: process.env.PERPLEXITY_MODEL || 'sonar-medium-online', messages: [ { role: 'system', content: 'You are a literary expert with deep knowledge of Jungian psychology, mythology, and narrative structure. You provide well-researched, psychologically nuanced analysis of story elements.' }, { role: 'user', content: prompt } ], temperature: 0.7, max_tokens: 2048 }); // Parse the response const responseContent = response.choices[0].message.content; // Try to extract JSON from the response try { // Find JSON in the response (it might be surrounded by markdown code blocks or other text) const jsonMatch = responseContent.match(/```(?:json)?\s*([\s\S]*?)\s*```/) || responseContent.match(/({[\s\S]*})/); const jsonContent = jsonMatch ? jsonMatch[1] : responseContent; const result = JSON.parse(jsonContent); // Validate the result if (!result.subelements || !Array.isArray(result.subelements)) { throw new Error('Invalid response format: missing subelements array'); } // Ensure IDs are sequential and start from nextSubelementId result.subelements.forEach((subelement, index) => { subelement.id = nextSubelementId + index; }); return result; } catch (parseError) { log('error', `Failed to parse research-backed subelements: ${parseError.message}`); // Fallback: Use Claude if Perplexity parsing fails log('info', 'Falling back to Claude for subelement generation'); return generateSubelements(element, numSubelements, nextSubelementId, additionalContext); } } catch (error) { log('error', `Error generating research-backed subelements: ${error.message}`); log('info', 'Falling back to Claude for subelement generation'); return generateSubelements(element, numSubelements, nextSubelementId, additionalContext); } } /** * Parse subtasks from Claude's response text * @param {string} text - Response text * @param {number} startId - Starting subtask ID * @param {number} expectedCount - Expected number of subtasks * @param {number} parentTaskId - Parent task ID * @returns {Array} Parsed subtasks */ function parseSubtasksFromText(text, startId, expectedCount, parentTaskId) { try { // Locate JSON array in the text const jsonStartIndex = text.indexOf('['); const jsonEndIndex = text.lastIndexOf(']'); if (jsonStartIndex === -1 || jsonEndIndex === -1 || jsonEndIndex < jsonStartIndex) { throw new Error("Could not locate valid JSON array in the response"); } // Extract and parse the JSON const jsonText = text.substring(jsonStartIndex, jsonEndIndex + 1); let subtasks = JSON.parse(jsonText); // Validate if (!Array.isArray(subtasks)) { throw new Error("Parsed content is not an array"); } // Log warning if count doesn't match expected if (subtasks.length !== expectedCount) { log('warn', `Expected ${expectedCount} subtasks, but parsed ${subtasks.length}`); } // Normalize subtask IDs if they don't match subtasks = subtasks.map((subtask, index) => { // Assign the correct ID if it doesn't match if (subtask.id !== startId + index) { log('warn', `Correcting subtask ID from ${subtask.id} to ${startId + index}`); subtask.id = startId + index; } // Convert dependencies to numbers if they are strings if (subtask.dependencies && Array.isArray(subtask.dependencies)) { subtask.dependencies = subtask.dependencies.map(dep => { return typeof dep === 'string' ? parseInt(dep, 10) : dep; }); } else { subtask.dependencies = []; } // Ensure status is 'pending' subtask.status = 'pending'; // Add parentTaskId subtask.parentTaskId = parentTaskId; return subtask; }); return subtasks; } catch (error) { log('error', `Error parsing subtasks: ${error.message}`); // Create a fallback array of empty subtasks if parsing fails log('warn', 'Creating fallback subtasks'); const fallbackSubtasks = []; for (let i = 0; i < expectedCount; i++) { fallbackSubtasks.push({ id: startId + i, title: `Subtask ${startId + i}`, description: "Auto-generated fallback subtask", dependencies: [], details: "This is a fallback subtask created because parsing failed. Please update with real details.", status: 'pending', parentTaskId: parentTaskId }); } return fallbackSubtasks; } } /** * Generate a prompt for complexity analysis * @param {Object} tasksData - Tasks data object containing tasks array * @returns {string} Generated prompt */ function generateComplexityAnalysisPrompt(tasksData) { return `Analyze the complexity of the following tasks and provide recommendations for subtask breakdown: ${tasksData.tasks.map(task => ` Task ID: ${task.id} Title: ${task.title} Description: ${task.description} Details: ${task.details} Dependencies: ${JSON.stringify(task.dependencies || [])} Priority: ${task.priority || 'medium'} `).join('\n---\n')} Analyze each task and return a JSON array with the following structure for each task: [ { "taskId": number, "taskTitle": string, "complexityScore": number (1-10), "recommendedSubtasks": number (${Math.max(3, CONFIG.defaultSubtasks - 1)}-${Math.min(8, CONFIG.defaultSubtasks + 2)}), "expansionPrompt": string (a specific prompt for generating good subtasks), "reasoning": string (brief explanation of your assessment) }, ... ] IMPORTANT: Make sure to include an analysis for EVERY task listed above, with the correct taskId matching each task's ID. `; } /** * Analyze story elements for plot holes and inconsistencies * @param {Object} storyData - Story data containing elements * @param {Array} elementIds - Specific element IDs to analyze, or empty for all * @returns {Object} Analysis result with plot holes and suggestions */ async function analyzePlotHoles(storyData, elementIds = []) { try { log('info', 'Analyzing story for plot holes and character inconsistencies'); // Filter elements if specific IDs are provided const elementsToAnalyze = elementIds.length > 0 ? storyData.elements.filter(element => elementIds.includes(element.id)) : storyData.elements; if (elementsToAnalyze.length === 0) { throw new Error('No elements found to analyze'); } // Extract key characters from the elements const characters = extractCharacters(elementsToAnalyze); // Build the system prompt const systemPrompt = `You are an expert literary analyst and editor specializing in narrative consistency and character development. Your task is to analyze a set of story elements for plot holes, timeline inconsistencies, character contradictions, and logical errors. Focus on: 1. Plot continuity issues - events that contradict each other or couldn't logically occur in sequence 2. Character consistency - actions, motivations, or traits that contradict established character behaviors 3. World-building inconsistencies - aspects of the story world that contradict each other 4. Timeline problems - events that don't make chronological sense 5. Logical fallacies - story elements that defy internal logic or require impossible circumstances For each issue you find, provide: 1. A clear description of the inconsistency 2. Which specific story elements are affected (by ID) 3. A suggested fix that would resolve the issue while preserving the narrative intent Consider the Jungian psychology framework of the story, especially how character actions align with their archetypal roles. Return your analysis as JSON in this structure: { "plotHoles": [ { "description": "Description of the inconsistency", "affectedElements": [element_ids], "suggestedFix": "Detailed suggestion for resolving the issue" } ], "characterConsistency": "Analysis of character consistency across the narrative", "suggestions": ["General suggestions for improving narrative coherence"] } If no significant issues are found, return an empty plotHoles array but still provide character consistency analysis and general suggestions.`; // Build the user prompt with the story elements const userPrompt = `Please analyze these story elements for plot holes, character inconsistencies, and logical errors: ${elementsToAnalyze.map(element => ` Element ${element.id}: ${element.title} Status: ${element.status} Description: ${element.description} Archetypes: ${element.archetypes ? element.archetypes.join(', ') : 'None'} Themes: ${element.themes ? element.themes.join(', ') : 'None'} Details: ${element.details || 'None provided'} ${element.subelements && element.subelements.length > 0 ? `Subelements:\n${element.subelements.map(sub => `- ${sub.title}: ${sub.description}`).join('\n')}` : 'No subelements'} `).join('\n---\n')} ${characters.length > 0 ? `\nKey Characters:\n${characters.map(char => `- ${char.name}: ${char.description}`).join('\n')}` : ''} Analyze thoroughly for any inconsistencies, focusing on plot holes, character actions that contradict established traits, timeline issues, and logical problems in the story world. `; // Call Claude to analyze const response = await anthropic.messages.create({ model: CONFIG.model, max_tokens: CONFIG.maxTokens, temperature: 0.5, // Lower temperature for more analytical response system: systemPrompt, messages: [ { role: 'user', content: userPrompt } ] }); // Extract and parse JSON from response const responseContent = response.content[0].text; try { // Find JSON in the response const jsonMatch = responseContent.match(/```(?:json)?\s*([\s\S]*?)\s*```/) || responseContent.match(/({[\s\S]*})/); const jsonContent = jsonMatch ? jsonMatch[1] : responseContent; const result = JSON.parse(jsonContent); // Ensure the result has the expected structure if (!result.plotHoles) result.plotHoles = []; if (!result.suggestions) result.suggestions = []; return result; } catch (parseError) { log('error', `Failed to parse analysis response: ${parseError.message}`); // Create a fallback result return { plotHoles: [], characterConsistency: "Analysis could not be parsed properly. Please try again.", suggestions: ["Try analyzing a smaller subset of elements for better results."] }; } } catch (error) { log('error', `Error analyzing plot holes: ${error.message}`); throw error; } } /** * Extract character information from story elements * @param {Array} elements - Story elements to analyze * @returns {Array} Extracted character information */ function extractCharacters(elements) { const characters = []; const characterNames = new Set(); // Regular expressions to identify character elements const characterTitleRegex = /character|protagonist|antagonist|hero|villain|mentor|ally|sidekick/i; const nameExtractRegex = /^([A-Z][a-z]+(?: [A-Z][a-z]+)?)/; // First pass: look for elements that are explicitly about characters for (const element of elements) { // Check if the element title suggests it's about a character if (characterTitleRegex.test(element.title)) { const nameMatch = element.title.match(nameExtractRegex); const name = nameMatch ? nameMatch[1] : element.title; if (!characterNames.has(name)) { characterNames.add(name); characters.push({ name, description: element.description, archetypes: element.archetypes || [], details: element.details || '' }); } continue; } // Check if the element has "character" as an archetype if (element.archetypes && element.archetypes.some(a => characterTitleRegex.test(a))) { const nameMatch = element.title.match(nameExtractRegex); const name = nameMatch ? nameMatch[1] : element.title; if (!characterNames.has(name)) { characterNames.add(name); characters.push({ name, description: element.description, archetypes: element.archetypes, details: element.details || '' }); } } } // Second pass: extract characters from descriptions and details for (const element of elements) { const combinedText = `${element.description} ${element.details || ''}`; // Look for character names in format "Name (archetype)" or similar patterns const characterPatterns = [ /([A-Z][a-z]+(?: [A-Z][a-z]+)?)\s+\((?:the |a |an )?(?:protagonist|hero|main character|antagonist|villain|mentor|supporting character)/gi, /(?:protagonist|hero|main character|antagonist|villain|mentor|supporting character)(?:\s+named|\s+called|\s+is)?\s+([A-Z][a-z]+(?: [A-Z][a-z]+)?)/gi ]; for (const pattern of characterPatterns) { let match; while ((match = pattern.exec(combinedText)) !== null) { const name = match[1]; if (!characterNames.has(name)) { characterNames.add(name); // Try to find a description near the name mention const contextWindow = combinedText.substring(Math.max(0, match.index - 100), Math.min(combinedText.length, match.index + 200)); const descriptionGuess = contextWindow.split(/[.!?]/).find(s => s.includes(name)) || ''; characters.push({ name, description: descriptionGuess.trim(), archetypes: [], details: '' }); } } } } return characters; } /** * Refine a specific phase of story development * @param {Object} storyData - Story data containing concept and/or elements * @param {string} phase - Refinement phase (world, characters, plot, themes) * @param {Object} options - Additional options * @returns {Object} Refinement result */ async function refineStoryPhase(storyData, phase, options = {}) { try { log('info', `Refining story phase: ${phase}`); // Build system prompt based on the refinement phase const systemPrompt = buildRefinementPrompt(phase); // Prepare the content for Claude let userPrompt; if (storyData.concept) { userPrompt = `Here's my story concept:\n\n${storyData.concept}\n\n`; } else { userPrompt = "Here's my story structure:\n\n"; } if (storyData.elements && storyData.elements.length > 0) { userPrompt += "Current story elements:\n\n"; // Add filtered elements based on phase const relevantElements = filterElementsByPhase(storyData.elements, phase); userPrompt += relevantElements.map(element => ` Element ${element.id}: ${element.title} Status: ${element.status} Description: ${element.description} Archetypes: ${element.archetypes ? element.archetypes.join(', ') : 'None'} Themes: ${element.themes ? element.themes.join(', ') : 'None'} ${element.details ? `Details: ${element.details}` : ''} ${element.criticalAnalysis ? `Critical Analysis: ${element.criticalAnalysis}` : ''} ${element.subelements && element.subelements.length > 0 ? `Subelements:\n${element.subelements.map(sub => `- ${sub.title}: ${sub.description}`).join('\n')}` : ''} `).join('\n---\n'); } // Add any specific focus from options if (options.focus) { userPrompt += `\n\nPlease focus specifically on: ${options.focus}\n`; } // Add the phase-specific request userPrompt += `\nPlease refine the ${phase} aspects of my story, following the guidelines in your instructions.`; // Call Claude to refine the story const response = await anthropic.messages.create({ model: CONFIG.model, max_tokens: CONFIG.maxTokens, temperature: 0.7, system: systemPrompt, messages: [ { role: 'user', content: userPrompt } ] }); // Process the response based on the phase return processRefinementResponse(response.content[0].text, phase, storyData); } catch (error) { log('error', `Error refining story phase ${phase}: ${error.message}`); throw error; } } /** * Build the system prompt for a specific refinement phase * @param {string} phase - Refinement phase * @returns {string} System prompt */ function buildRefinementPrompt(phase) { // Base prompt elements const basePrompt = `You are a master storyteller specializing in narrative development with Jungian psychology, guiding writers through a structured refinement process. Your goal is to help enhance the story while maintaining the writer's vision. Consider multiple dimensions: - Narrative coherence and depth - Character psychology and development - Jungian archetypes and their psychological significance - Symbolic systems and motifs - Thematic resonance across the story Return your analysis in a structured format with these sections: 1. REFINED_CONCEPT: An improved version of the story concept with more detail and coherence 2. SUMMARY: A brief explanation of the key improvements made 3. UPDATED_ELEMENTS: (Only if needed) JSON representing updated or new story elements 4. SUGGESTIONS: 3-5 actionable suggestions for further development All content should deeply incorporate Jungian psychology and work well with multiple characters.`; // Phase-specific prompts switch (phase) { case 'world': return `${basePrompt} For the WORLD phase, focus on: - Creating a psychologically rich setting that serves as external manifestation of the collective unconscious - Developing the rules and systems of the story world - Ensuring settings have symbolic significance in Jungian terms - Creating a world that allows for multiple character arcs and psychological journeys - Incorporating locations that represent different aspects of the psyche - Examining how the world reflects underlying collective archetypes This phase establishes the foundation upon which characters will interact and psychological dramas will unfold. In the REFINED_CONCEPT, elaborate the world details while preserving the core essence. In UPDATED_ELEMENTS, focus on enhancing or creating elements related to setting, rules, and symbolic systems.`; case 'characters': return `${basePrompt} For the CHARACTERS phase, focus on: - Developing multiple characters with psychological depth and complexity - Establishing clear Jungian archetypes for each character while avoiding stereotypes - Creating internal conflicts and psychological motivations - Ensuring character flaws that can evolve throughout the narrative - Developing meaningful relationships and psychological dynamics between characters - Establishing how each character relates to the collective unconscious of the story world This phase creates the psychological agents whose journeys will drive the narrative. In the REFINED_CONCEPT, enhance character descriptions with psychological detail. In UPDATED_ELEMENTS, focus on character-centric elements, including relationships, internal conflicts, and growth arcs.`; case 'plot': return `${basePrompt} For the PLOT phase, focus on: - Creating a cohesive narrative structure with psychological significance - Identifying and resolving potential plot holes - Designing plot points that support character development and psychological transformation - Establishing cause-and-effect relationships that feel natural and inevitable - Creating rising action that intensifies psychological stakes - Ensuring dramatic moments align with Jungian psychological processes - Balancing multiple character arcs within the overall narrative This phase structures the psychological journey and ensures narrative coherence. In the REFINED_CONCEPT, clarify the narrative progression and key plot points. In UPDATED_ELEMENTS, focus on plot-centric elements, including conflicts, turning points, and resolutions. Pay special attention to identifying and fixing potential plot inconsistencies.`; case 'themes': return `${basePrompt} For the THEMES phase, focus on: - Articulating core thematic questions explored in the story - Finding ways to express themes through multiple characters and situations - Establishing symbolic systems that reinforce themes - Connecting themes to Jungian concepts like individuation, shadow integration, and the collective unconscious - Creating motifs and recurring imagery that subtly reinforce themes - Ensuring thematic consistency while avoiding heavy-handedness This phase deepens the psychological significance and resonance of the story. In the REFINED_CONCEPT, bring thematic elements to the foreground. In UPDATED_ELEMENTS, enhance elements with thematic connections and symbolic significance.`; default: return basePrompt; } } /** * Filter elements based on the refinement phase * @param {Array} elements - All story elements * @param {string} phase - Refinement phase * @returns {Array} Filtered elements relevant to the phase */ function filterElementsByPhase(elements, phase) { // Keywords associated with each phase const phaseKeywords = { world: [ 'world', 'setting', 'place', 'location', 'environment', 'realm', 'universe', 'society', 'culture', 'history', 'background', 'rules', 'system', 'magic' ], characters: [ 'character', 'protagonist', 'antagonist', 'hero', 'villain', 'mentor', 'ally', 'person', 'figure', 'relationship', 'motivation', 'goal', 'backstory', 'trait' ], plot: [ 'plot', 'story', 'event', 'action', 'conflict', 'challenge', 'problem', 'resolution', 'arc', 'journey', 'quest', 'mission', 'scene', 'sequence', 'incident' ], themes: [ 'theme', 'motif', 'symbol', 'meaning', 'message', 'philosophy', 'idea', 'concept', 'metaphor', 'representation', 'archetype', 'pattern', 'image', 'dream' ] }; // If no specific phase or "all", return all elements if (!phase || phase === 'all') { return elements; } const keywords = phaseKeywords[phase] || []; const keywordRegex = new RegExp(keywords.join('|'), 'i'); // Filter elements based on keywords in title, description, or archetypes return elements.filter(element => { const titleMatches = keywordRegex.test(element.title); const descriptionMatches = keywordRegex.test(element.description); const archetypesMatch = element.archetypes && element.archetypes.some(archetype => keywordRegex.test(archetype)); const themesMatch = element.themes && element.themes.some(theme => keywordRegex.test(theme)); return titleMatches || descriptionMatches || archetypesMatch || themesMatch; }); } /** * Process refinement response from Claude * @param {string} responseText - Claude's response text * @param {string} phase - Refinement phase * @param {Object} storyData - Original story data * @returns {Object} Processed refinement result */ function processRefinementResponse(responseText, phase, storyData) { // Default result structure const result = { summary: null, refinedConcept: null, updatedElements: null, suggestions: [] }; // Extract sections from the response const refinedConceptMatch = responseText.match(/REFINED_CONCEPT:([^]*?)(?=SUMMARY:|UPDATED_ELEMENTS:|SUGGESTIONS:|$)/s); if (refinedConceptMatch) { result.refinedConcept = refinedConceptMatch[1].trim(); } const summaryMatch = responseText.match(/SUMMARY:([^]*?)(?=REFINED_CONCEPT:|UPDATED_ELEMENTS:|SUGGESTIONS:|$)/s); if (summaryMatch) { result.summary = summaryMatch[1].trim(); } const suggestionsMatch = responseText.match(/SUGGESTIONS:([^]*?)(?=REFINED_CONCEPT:|SUMMARY:|UPDATED_ELEMENTS:|$)/s); if (suggestionsMatch) { // Split suggestions by numbered items or bullet points const suggestionsText = suggestionsMatch[1].trim(); const suggestionsLines = suggestionsText.split(/\n+/); // Filter out lines that look like suggestions (numbered items or bullet points) result.suggestions = suggestionsLines .filter(line => /^(\d+[\.\):]|\*|\-)\s+/.test(line)) .map(line => line.replace(/^(\d+[\.\):]|\*|\-)\s+/, '').trim()); } // Extract updated elements if available const elementsMatch = responseText.match(/UPDATED_ELEMENTS:([^]*?)(?=REFINED_CONCEPT:|SUMMARY:|SUGGESTIONS:|$)/s); if (elementsMatch) { try { // Try to parse JSON from the response const elementsJson = elementsMatch[1].trim(); // Find JSON in the text (might be wrapped in code blocks)