diff --git a/backend/src/services/claudeClient.js b/backend/src/services/claudeClient.js new file mode 100644 index 0000000..5aa192c --- /dev/null +++ b/backend/src/services/claudeClient.js @@ -0,0 +1,144 @@ +import dotenv from 'dotenv' + +dotenv.config() + +const CLAUDE_API_KEY = process.env.CLAUDE_API_KEY +const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages' + +/** + * Generic AI prompt for collaborative document editing + */ +function getAgentPrompt(agentName) { + return `You are an AI assistant named ${agentName} collaborating on a technical document design. + +Your responsibilities: +1. Review the current document structure +2. Either: + a) Modify ONE existing section (identified by #, ##, ###, #### headers), OR + b) Create a NEW section if you think it's needed, OR + c) Delete a section if you think it's redundant or not useful +3. Provide your thinking process and reasoning +4. Return ONLY the section (modified or new) with its header, or command to delete, or confirm it's good as-is + +CRITICAL RULES - FOLLOW THESE EXACTLY: +- Work on exactly ONE section only +- NEVER return the entire document +- NEVER return multiple sections +- Return ONLY the section you're working on, not the whole document +- You CAN create a new section if document is missing important content +- You CAN delete a section if it's redundant, duplicate, or not useful +- To delete a section, respond: "DELETE: ## Section Name" (with exact header) +- If section is good, respond: "Section is good, no changes needed" +- Think step-by-step about what could be improved or removed +- Share your reasoning process + +RESPONSE FORMAT - FOLLOW THIS EXACTLY: +THINKING: [Your analysis and reasoning about the current document] +DECISION: [Exactly what you will do: modify section X, create new section Y, delete section Z, or keep as-is] +SECTION: +[ONLY ONE: Either a markdown section starting with # or ##, a DELETE command, or the text "Section is good, no changes needed"] + +EXAMPLE OF CORRECT RESPONSE: +THINKING: The Overview section is too brief and doesn't explain the main purpose. +DECISION: I will modify the Overview section to be more comprehensive. +SECTION: +## Overview +This is a technical document for designing system architecture... + +EXAMPLE OF INCORRECT RESPONSE (DO NOT DO THIS): +[The entire document repeated here] <- WRONG!` +} + +/** + * Call Claude API + */ +async function callClaudeAPI(messages) { + const response = await fetch(CLAUDE_API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': CLAUDE_API_KEY, + 'anthropic-version': '2023-06-01' + }, + body: JSON.stringify({ + model: 'claude-3-5-sonnet-20241022', + max_tokens: 2048, + system: messages[0].content, + messages: messages.slice(1) + }) + }) + + if (!response.ok) { + const error = await response.text() + throw new Error(`Claude API error: ${error}`) + } + + return response +} + +/** + * Generate agent response using Claude + */ +export async function generateAgentResponseSync(agentName, prompt, currentDocument = '') { + const systemPrompt = getAgentPrompt(agentName) + + const messages = [ + { role: 'user', content: systemPrompt }, + { role: 'user', content: `Project description: ${prompt}\n\nCurrent document:\n${currentDocument}` } + ] + + try { + const response = await callClaudeAPI(messages) + const data = await response.json() + + if (!data.content?.[0]?.text) { + throw new Error('Invalid response from Claude API') + } + + return data.content[0].text + } catch (error) { + console.error(`Error generating response from ${agentName}:`, error) + return `Error: ${error.message}` + } +} + +/** + * Extract section from AI response + * Validates that we get a proper section, not the entire document + */ +export function extractSection(aiResponse) { + const sectionMatch = aiResponse.match(/SECTION:\s*([\s\S]*?)(?:$)/) + if (sectionMatch) { + const extracted = sectionMatch[1].trim() + + // Validate: extracted should be either: + // 1. A markdown section starting with # (DELETE: or "Section is good...") + // 2. OR a single section with < 5000 chars (not entire document) + const isMarkdownSection = /^(#|DELETE:|Section is good)/.test(extracted) + const isShortEnough = extracted.length < 5000 // Single section should be < 5KB + + if (isMarkdownSection || isShortEnough) { + return extracted + } + } + + // Fallback: if no SECTION: tag found, treat entire response as section + // but only if it starts with markdown header or is a command + if (/^(#|DELETE:|Section is good)/.test(aiResponse)) { + return aiResponse.trim() + } + + // If none of the above, return error indicator + return "ERROR: Response does not contain a valid section format" +} + +/** + * Extract thinking from AI response + */ +export function extractThinking(aiResponse) { + const thinkingMatch = aiResponse.match(/THINKING:\s*([\s\S]*?)(?:DECISION:|SECTION:)/) + if (thinkingMatch) { + return thinkingMatch[1].trim() + } + return '' +}