From cc8d6a3f23ea79817a4c0864985b1396a75d25e2 Mon Sep 17 00:00:00 2001 From: andrei Date: Wed, 21 May 2025 19:28:57 +0200 Subject: [PATCH 01/16] Changed the Email Writer to support several forms that get submitted individually. This version is incomplete, as the backend logic has not yet been updated. --- src/routes/write/+page.svelte | 305 +++++++++++++++++++++++++++------- 1 file changed, 244 insertions(+), 61 deletions(-) diff --git a/src/routes/write/+page.svelte b/src/routes/write/+page.svelte index 476b9a3f2..f88cfb87a 100644 --- a/src/routes/write/+page.svelte +++ b/src/routes/write/+page.svelte @@ -34,7 +34,7 @@ const maxMessages = 20 // Organizing the form questions into sections and subsections - const formSections: FieldSection[] = [ + const formSections_Target: FieldSection[] = [ { title: 'Personal Context', subsections: [ @@ -92,27 +92,47 @@ ] } ] - }, + } + ] + + // Flatten the questions array for accessing by index + const paragraphText_Target: string[] = [] + formSections_Target.forEach((section) => { + section.subsections.forEach((subsection) => { + subsection.questions.forEach((question) => { + paragraphText_Target.push(question) + }) + }) + }) + + const formSections_Research: FieldSection[] = [ { - title: 'Information Needed About the Message', + title: 'Finding a target', subsections: [ { - title: 'Content Requirements', - questions: ['Specific outcome desired', 'Concrete action requested'] /* + title: "Specify what sort of target you're looking for, and where.", questions: [ - 'Clear, singular objective', - 'Specific outcome desired', - 'Concrete action requested' - ]*/ - }, - { - title: 'Supporting Evidence', - questions: [ - 'Relevant facts', - 'Context for the request', - 'Potential impact or consequences' + 'If you have certain institutions in mind, mention those. Otherwise, your local representative could be a good place to start.' ] - }, + } + ] + } + ] + + // Flatten the questions array for accessing by index + const paragraphText_Research: string[] = [] + formSections_Research.forEach((section) => { + section.subsections.forEach((subsection) => { + subsection.questions.forEach((question) => { + paragraphText_Research.push(question) + }) + }) + }) + + const formSections_MessageDetails: FieldSection[] = [ + { + title: 'The Message', + subsections: [ { title: 'Logical Structure', questions: [ @@ -165,11 +185,46 @@ ] // Flatten the questions array for accessing by index - const paragraphText: string[] = [] - formSections.forEach((section) => { + const paragraphText_MessageDetails: string[] = [] + formSections_Research.forEach((section) => { + section.subsections.forEach((subsection) => { + subsection.questions.forEach((question) => { + paragraphText_MessageDetails.push(question) + }) + }) + }) + + const formSections_Message: FieldSection[] = [ + { + title: 'What is your Message?', + subsections: [ + { + title: 'Content Requirements', + questions: ['Specific outcome desired', 'Concrete action requested'] /* + questions: [ + 'Clear, singular objective', + 'Specific outcome desired', + 'Concrete action requested' + ]*/ + }, + { + title: 'Supporting Evidence', + questions: [ + 'Relevant facts', + 'Context for the request', + 'Potential impact or consequences' + ] + } + ] + } + ] + + // Flatten the questions array for accessing by index + const paragraphText_Message: string[] = [] + formSections_Message.forEach((section) => { section.subsections.forEach((subsection) => { subsection.questions.forEach((question) => { - paragraphText.push(question) + paragraphText_Message.push(question) }) }) }) @@ -203,11 +258,11 @@ clear_arr(input_arr) // Find index for specific fields based on their question text - const roleAuthorityIndex = paragraphText.findIndex( + const roleAuthorityIndex = paragraphText_Target.findIndex( (q) => q === 'Understanding their role and potential authority' ) - const objectiveIndex = paragraphText.findIndex((q) => q === 'Concrete action requested') - const outcomeIndex = paragraphText.findIndex((q) => q === 'Specific outcome desired') + const objectiveIndex = paragraphText_Target.findIndex((q) => q === 'Concrete action requested') + const outcomeIndex = paragraphText_Target.findIndex((q) => q === 'Specific outcome desired') input_arr[roleAuthorityIndex] = 'You are writing for a child of about thirteen years old.' input_arr[objectiveIndex] = @@ -220,8 +275,8 @@ async function sendMessage() { let input = '' - for (var i in paragraphText) { - input = input + paragraphText[i] + ':\n' + input_arr[i] + '\n\n' + for (var i in paragraphText_Target) { + input = input + paragraphText_Target[i] + ':\n' + input_arr[i] + '\n\n' } messages = [...messages, { content: input, role: 'user' }] @@ -339,8 +394,10 @@ let currentField = -1 for (let line of lines) { - // Look for field headers matching our paragraphText array - const fieldIndex = paragraphText.findIndex((text) => line.trim().startsWith(text + ':')) + // Look for field headers matching our paragraphText_Target array + const fieldIndex = paragraphText_Target.findIndex((text) => + line.trim().startsWith(text + ':') + ) if (fieldIndex >= 0) { currentField = fieldIndex @@ -399,9 +456,33 @@ // Function to get the index of a question across all sections function getQuestionIndex(question: string): number { - return paragraphText.findIndex((text) => text === question) + return paragraphText_Target.findIndex((text) => text === question) } + // FORM FUNCTIONS // + + // Add these variables for form toggling + let activeForm = 'form1' // Default active form + + // Function to set the active form + function setActiveForm(formId) { + activeForm = formId + console.log(formId) + } + + // UNTESTED AND GENERATED BY AI + // Create separate arrays for each form's inputs + //let input_arr = Array(formSections_Target.flatMap(s => s.subsections.flatMap(ss => ss.questions)).length).fill(''); + let form2_input_arr = Array( + formSections_Target.flatMap((s) => s.subsections.flatMap((ss) => ss.questions)).length + ).fill('') + let form3_input_arr = Array( + formSections_Target.flatMap((s) => s.subsections.flatMap((ss) => ss.questions)).length + ).fill('') + let form4_input_arr = Array( + formSections_Target.flatMap((s) => s.subsections.flatMap((ss) => ss.questions)).length + ).fill('') + // Top of the page const title = `Write Email Content` const description = `This (beta!) webpage lets you write email content (with LLM assistance.)` @@ -453,6 +534,34 @@ particular hardcoded values, and starts writing content.

+ +
+ + + + +
+
{:else} -
- - {#each formSections as section, sectionIndex} -

{section.title}

- - {#each section.subsections as subsection, subsectionIndex} -

{subsection.title}

- - - {#if subsection.title === 'Content Requirements'} -

Precise Purpose

- {/if} - - {#each subsection.questions as question, questionIndex} - - {@const globalIndex = getQuestionIndex(question)} - - - {#if subsection.title === 'Supporting Evidence' && questionIndex === 0} -

Supporting Evidence

- {:else if subsection.title === 'Logical Structure' && questionIndex === 0} -

Logical Structure

- {/if} - -

{question}

- + +
+ + {#if activeForm === 'form1'} + + {#each formSections_Research as section, sectionIndex} +

{section.title}

+ {#each section.subsections as subsection, subsectionIndex} +

{subsection.title}

+ + {#each subsection.questions as question, questionIndex} + {@const globalIndex = getQuestionIndex(question)} + +

{question}

+ + {/each} + {/each} + {/each} + + {/if} + + + {#if activeForm === 'form2'} +
+ + {#each formSections_Target as section, sectionIndex} +

{section.title}

+ {#each section.subsections as subsection, subsectionIndex} +

{subsection.title}

+ + + {#if subsection.title === 'Content Requirements'} +

Precise Purpose

+ {/if} + + {#each subsection.questions as question, questionIndex} + + {@const globalIndex = getQuestionIndex(question)} + + + {#if subsection.title === 'Supporting Evidence' && questionIndex === 0} +

Supporting Evidence

+ {:else if subsection.title === 'Logical Structure' && questionIndex === 0} +

Logical Structure

+ {/if} + +

{question}

+ + {/each} + {/each} + {/each} +
+ {/if} + + + {#if activeForm === 'form3'} +
+ {#each formSections_Message as section, sectionIndex} +

{section.title}

+ {#each section.subsections as subsection, subsectionIndex} +

{subsection.title}

+ + {#each subsection.questions as question, questionIndex} + {@const globalIndex = getQuestionIndex(question)} + +

{question}

+ + {/each} + {/each} + {/each} +
+ {/if} + + + {#if activeForm === 'form4'} +
+ {#each formSections_MessageDetails as section, sectionIndex} +

{section.title}

+ {#each section.subsections as subsection, subsectionIndex} +

{subsection.title}

+ + {#each subsection.questions as question, questionIndex} + {@const globalIndex = getQuestionIndex(question)} + +

{question}

+ + {/each} + {/each} {/each} - {/each} - {/each} -
+ + {/if} +
{/if} From 73c59246845a572ac9a86136a12d58daba19f077 Mon Sep 17 00:00:00 2001 From: andrei Date: Wed, 4 Jun 2025 16:12:52 +0200 Subject: [PATCH 02/16] Fixed API to now allow tool use. --- src/routes/api/write/+server.ts | 453 +++++++++++++++++++++++++++----- 1 file changed, 390 insertions(+), 63 deletions(-) diff --git a/src/routes/api/write/+server.ts b/src/routes/api/write/+server.ts index 1fd2f9c92..2424df656 100644 --- a/src/routes/api/write/+server.ts +++ b/src/routes/api/write/+server.ts @@ -4,6 +4,10 @@ import Anthropic from '@anthropic-ai/sdk' // Safely access the API key, will be undefined if not set const ANTHROPIC_API_KEY_FOR_WRITE = env.ANTHROPIC_API_KEY_FOR_WRITE || undefined +// NEW: Add global toggle for web search functionality +const ENABLE_WEB_SEARCH = env.ENABLE_WEB_SEARCH !== 'false' // Default to true unless explicitly disabled +// NEW: Add configurable rate limiting for tool calls per step +const MAX_TOOL_CALLS_PER_STEP = parseInt(env.MAX_TOOL_CALLS_PER_STEP || '3') // Flag to track if API is available const IS_API_AVAILABLE = !!ANTHROPIC_API_KEY_FOR_WRITE @@ -16,12 +20,86 @@ if (!IS_API_AVAILABLE) { } // Define step types for server-side use -type StepName = 'research' | 'firstDraft' | 'firstCut' | 'firstEdit' | 'toneEdit' | 'finalEdit' +type StepName = + | 'findTarget' + | 'webSearch' + | 'research' + | 'firstDraft' + | 'firstCut' + | 'firstEdit' + | 'toneEdit' + | 'finalEdit' + +// Define workflow types +type WorkflowType = '1' | '2' | '3' | '4' + +// NEW: Define step configuration interface for tool usage +interface StepConfig { + toolsEnabled?: boolean // Whether this step can use tools + maxToolCalls?: number // Maximum tool calls for this step (overrides global) + description?: string // Enhanced description when tools are used +} + +// ENHANCED: Extend workflow configuration to support step configs +type WorkflowConfig = { + steps: StepName[] + description: string + stepConfigs?: Record // NEW: Optional step-level configuration +} + +// NEW: Define step-level tool configurations +const stepConfigs: Record = { + // Research-focused steps that benefit from web search + findTarget: { + toolsEnabled: true, + maxToolCalls: 5, + description: 'Find possible targets (using web search)' + }, + webSearch: { + toolsEnabled: true, + maxToolCalls: 3, + description: 'Research the target (using web search)' + }, + research: { + toolsEnabled: true, + maxToolCalls: 2, + description: 'Auto-fill missing user inputs (using web search)' + }, + // Text processing steps remain tool-free for performance + firstDraft: { toolsEnabled: false }, + firstCut: { toolsEnabled: false }, + firstEdit: { toolsEnabled: false }, + toneEdit: { toolsEnabled: false }, + finalEdit: { toolsEnabled: false } +} +const workflowConfigs: Record = { + '1': { + steps: ['findTarget'], + description: 'Find Target Only', + stepConfigs // NEW: Include step configurations + }, + '2': { + steps: ['webSearch'], + description: 'Web Search Only', + stepConfigs // NEW: Include step configurations + }, + '3': { + steps: ['research'], + description: 'Research Only', + stepConfigs // NEW: Include step configurations + }, + '4': { + steps: ['firstDraft', 'firstCut', 'firstEdit', 'toneEdit', 'finalEdit'], + description: 'Full Email Generation', + stepConfigs // NEW: Include step configurations + } +} // Server-side state management interface (not exposed to client) interface WriteState { step: StepName | 'complete' | 'start' // Current/completed step - userInput: string // Original input from form + workflowType: WorkflowType // Type of workflow being executed + userInput: string // Original input from form (cleaned, without prefix) email: string // Current email content information?: string // Processed information after research completedSteps: Array<{ @@ -52,6 +130,7 @@ const System_Prompts: { [id: string]: string } = {} System_Prompts['Basic'] = `You are a helpful AI assistant. Note: Some fields in the information may begin with a robot emoji (πŸ€–). This indicates the field was automatically generated during research. You can use this information normally, just ignore the emoji marker.` + System_Prompts['Mail'] = ` What follows is the anatomy of a good email, a set of guidelines and criteria for writing a good mail. Each paragraph, except the last represents @@ -177,9 +256,39 @@ Example: Original: "Preferred communication style: undefined" Your output: "Preferred communication style: πŸ€– Formal but approachable" +Please remember that you are addressing this person, and try to make all inferences based on the information provided and your own knowledge. Err on the side of caution: if you are unsure, be polite and neutral. + Output the full information, including your edits. Output nothing else. ` +System_Prompts['Target'] = ` +Please use your internet search capability to find individuals involved with AI safety who match the following description. + +For each person you find (aim for 3-5 people), please provide: +1. Name and current position +2. Why they're relevant to AI safety +3. Their organization +4. Brief note on their public stance on AI safety + +Please cite your sources for each person. +` + +//Preface with '[Person's Name] = John Doe' etc. +System_Prompts['webSearch'] = ` +Please use your internet search capability to research [Person's Name] who is [current role] at [organization/affiliation]. I plan to contact them about AI safety concerns. + +Search for and provide: +1. Professional background (education, career history, notable positions) +2. Their involvement with AI issues (policy positions, public statements, initiatives, articles, interviews) +3. Their public views on AI development and safety (with direct quotes where possible) +4. Recent activities related to technology policy or AI (last 6-12 months) +5. Communication style and key terms they use when discussing technology issues +6. Notable connections (organizations, committees, coalitions, or influential individuals they work with) +7. Contact information (professional email or official channels if publicly available) + +Please cite all sources you use and only include information you can verify through your internet search. If you encounter conflicting information, note this and provide the most reliable source. +` + // Only initialize the client if we have an API key const anthropic = IS_API_AVAILABLE ? new Anthropic({ @@ -191,13 +300,46 @@ export async function GET() { return json({ apiAvailable: IS_API_AVAILABLE }) } -// Helper function to call Claude API with timing +// Helper function to parse workflow type from user input +function parseWorkflowType(userInput: string): { + workflowType: WorkflowType + cleanedInput: string +} { + const workflowMatch = userInput.match(/^\[([1-4])\](.*)$/s) + + if (workflowMatch) { + const workflowType = workflowMatch[1] as WorkflowType + const cleanedInput = workflowMatch[2].trim() + return { workflowType, cleanedInput } + } + + // Default to workflow 4 if no prefix is found + return { workflowType: '4', cleanedInput: userInput } +} +// NEW: Interface for tool use response content +interface ToolUseContent { + type: 'tool_use' + id: string + name: string + input: any +} + +// NEW: Interface for tool result content +interface ToolResultContent { + type: 'tool_result' + tool_use_id: string + content: string +} + +// ENHANCED: Modified function signature to support optional tool usage async function callClaude( stepName: string, promptNames: string[], - userContent: string + userContent: string, + toolsEnabled: boolean = false // NEW: Optional parameter for tool usage ): Promise<{ text: string; durationSec: number }> { const pencil = '✏️' + const search = 'πŸ”' // NEW: Icon for tool usage const logPrefix = `${pencil} write:${stepName}` const startTime = Date.now() @@ -212,73 +354,209 @@ async function callClaude( // Combine all the specified prompts const systemPrompt = promptNames.map((name) => System_Prompts[name]).join('') - const response = await anthropic.messages.create({ + // NEW: Determine if tools should be included in this call + const shouldUseTools = toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE + + // NEW: Log tool usage status + if (shouldUseTools) { + console.log(`${search} ${logPrefix}: Tools enabled for this step`) + } + + // FIXED: Use correct web search tool definition matching API documentation + const tools = shouldUseTools + ? [ + { + type: 'web_search_20250305', // CHANGED: Use correct tool type from API docs + name: 'web_search', + max_uses: 5 // ADDED: Limit searches per request + } + ] + : undefined + + // ENHANCED: Create API request with conditional tool support + const requestParams: any = { model: 'claude-3-7-sonnet-20250219', - max_tokens: 4096, // Increased to handle all fields + max_tokens: 4096, system: systemPrompt, messages: [{ role: 'user', content: userContent }] - }) + } + + // NEW: Add tools to request if enabled + if (tools) { + requestParams.tools = tools + } - // Log the request ID at debug level - console.debug(`${logPrefix} requestId: ${response.id}`) + // FIXED: Implement proper tool execution loop + let currentMessages = [...requestParams.messages] + let finalText = '' + let toolCallCount = 0 + const maxCalls = Math.min( + MAX_TOOL_CALLS_PER_STEP, + stepConfigs[stepName as StepName]?.maxToolCalls || MAX_TOOL_CALLS_PER_STEP + ) + + while (toolCallCount < maxCalls) { + // Create request with current message history + const currentRequest = { + ...requestParams, + messages: currentMessages + } - // Ensure the response content is text - if (response.content[0].type !== 'text') { - throw new Error(`Unexpected content type from API: ${response.content[0].type}`) + const response = await anthropic.messages.create(currentRequest) + + // Log the request ID at debug level + console.debug(`${logPrefix} requestId: ${response.id}`) + + // FIXED: Process response content properly + let hasToolUse = false + let textContent = '' + + for (const content of response.content) { + if (content.type === 'text') { + textContent += content.text + } else if (content.type === 'server_tool_use' && shouldUseTools) { + // FIXED: Handle server-side tool use (web search is executed automatically) + hasToolUse = true + toolCallCount++ + console.log(`${search} ${logPrefix}: Web search executed - ${content.name}`) + } else if (content.type === 'web_search_tool_result') { + // FIXED: Handle web search results (automatically provided by API) + console.log(`${search} ${logPrefix}: Received web search results`) + } + } + + // FIXED: Add assistant's response to conversation history + currentMessages.push({ + role: 'assistant', + content: response.content + }) + + // FIXED: Accumulate text content + finalText += textContent + + // FIXED: Break if no tool use or if we've hit limits + if (!hasToolUse || toolCallCount >= maxCalls) { + break + } + + // FIXED: If there was tool use, Claude might continue in the same turn + // Check if response has pause_turn stop reason + if (response.stop_reason === 'pause_turn') { + // Continue the conversation to let Claude finish its turn + continue + } else { + // Tool use complete, break the loop + break + } + } + + // FIXED: Ensure we have text content + if (!finalText) { + throw new Error('No text content received from Claude') } - const result = response.content[0].text const elapsed = (Date.now() - startTime) / 1000 // seconds + // ENHANCED: Log tool usage statistics + if (shouldUseTools && toolCallCount > 0) { + console.log(`${search} ${logPrefix}: Used ${toolCallCount} web searches`) + } + // Log the full response text at debug level - console.debug(`${logPrefix} full response:\n---\n${result}\n---`) - return { text: result, durationSec: elapsed } + console.debug(`${logPrefix} full response:\n---\n${finalText}\n---`) + return { text: finalText, durationSec: elapsed } + } catch (error) { + // ENHANCED: Better error handling for tool-related failures + if (toolsEnabled && (error.message?.includes('tool') || error.message?.includes('search'))) { + console.warn( + `${search} ${logPrefix}: Tool error, falling back to text-only mode:`, + error.message + ) + // Retry without tools on tool-related errors + return callClaude(stepName, promptNames, userContent, false) + } + throw error // Re-throw non-tool errors } finally { console.timeEnd(`${logPrefix}`) } } +// NEW: Function to get step description with tool awareness +function getStepDescription(stepName: StepName): string { + const stepConfig = stepConfigs[stepName] + const toolsWillBeUsed = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE + + // Return enhanced description if tools are enabled and available + if (toolsWillBeUsed && stepConfig?.description) { + return stepConfig.description + } + + // Fallback to standard descriptions + const stepDescriptions: Record = { + findTarget: 'Find possible targets', + webSearch: 'Research the target', + research: 'Auto-fill missing user inputs', + firstDraft: 'Create initial draft', + firstCut: 'Remove unnecessary content', + firstEdit: 'Improve text flow', + toneEdit: 'Adjust tone and style', + finalEdit: 'Final polish' + } -// Define user-friendly step descriptions -const stepDescriptions: Record = { - research: 'Auto-fill missing user inputs', - firstDraft: 'Create initial draft', - firstCut: 'Remove unnecessary content', - firstEdit: 'Improve text flow', - toneEdit: 'Adjust tone and style', - finalEdit: 'Final polish' + return stepDescriptions[stepName] } // Function to generate a progress string from the state + function generateProgressString(state: WriteState): string { const pencil = '✏️' const checkmark = 'βœ“' + const search = 'πŸ”' // NEW: Icon for tool-enabled steps + + // Get workflow description + const workflowDescription = workflowConfigs[state.workflowType].description // Generate the progress string let lis = [] - // Completed steps - use descriptions instead of raw step names + // ENHANCED: Completed steps with tool usage indicators for (const step of state.completedSteps) { + const stepConfig = stepConfigs[step.name] + const usedTools = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE + const icon = usedTools ? `${search}${checkmark}` : checkmark + lis.push( - `
  • ${stepDescriptions[step.name]} (${step.durationSec.toFixed(1)}s) ${checkmark}
  • ` + `
  • ${getStepDescription(step.name)} (${step.durationSec.toFixed(1)}s) ${icon}
  • ` ) } - // Current step - only add if not already completed and state isn't complete + // ENHANCED: Current step with tool usage indicator if ( state.currentStep && state.step !== 'complete' && !state.completedSteps.some((s) => s.name === state.currentStep) ) { - lis.push(`
  • ${stepDescriptions[state.currentStep]} ${pencil}
  • `) + const stepConfig = stepConfigs[state.currentStep] + const willUseTools = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE + const icon = willUseTools ? `${search}${pencil}` : pencil + + lis.push(`
  • ${getStepDescription(state.currentStep)} ${icon}
  • `) } - // Remaining steps - filter out any that are in completed steps or current step + // ENHANCED: Remaining steps with tool usage preview const completedAndCurrentSteps = [...state.completedSteps.map((s) => s.name), state.currentStep] const filteredRemainingSteps = state.remainingSteps.filter( (step) => !completedAndCurrentSteps.includes(step) ) + lis = lis.concat( - filteredRemainingSteps.map((step) => `
  • ${stepDescriptions[step]}
  • `) + filteredRemainingSteps.map((step) => { + const stepConfig = stepConfigs[step] + const willUseTools = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE + const description = getStepDescription(step) + const indicator = willUseTools ? ` ${search}` : '' + + return `
  • ${description}${indicator}
  • ` + }) ) const listItems = lis.join('') @@ -286,31 +564,29 @@ function generateProgressString(state: WriteState): string { if (state.nextStep === null) { // Process is complete - return `Done (${totalTime.toFixed(1)}s):
      ${listItems}
    ` + return `${workflowDescription} - Done (${totalTime.toFixed(1)}s):
      ${listItems}
    ` } else { - return `Progress:
      ${listItems}
    ` + return `${workflowDescription} - Progress:
      ${listItems}
    ` } } // Initialize a new state for the step-by-step process function initializeState(userInput: string): WriteState { - const allSteps: StepName[] = [ - 'research', - 'firstDraft', - 'firstCut', - 'firstEdit', - 'toneEdit', - 'finalEdit' - ] + const { workflowType, cleanedInput } = parseWorkflowType(userInput) + const workflowSteps = workflowConfigs[workflowType].steps + + const firstStep = workflowSteps[0] + const remainingSteps = workflowSteps.slice(1) return { step: 'start', - userInput, + workflowType, + userInput: cleanedInput, email: '', completedSteps: [], - currentStep: 'research', // First step - remainingSteps: allSteps.slice(1), // All steps except research (which is current) - nextStep: 'research' + currentStep: firstStep, + remainingSteps, + nextStep: firstStep } } @@ -336,13 +612,61 @@ const stepHandlers: Record< StepName, (state: WriteState) => Promise<{ text: string; durationSec: number }> > = { + // ENHANCED: Enable tools for target finding + findTarget: async (state) => { + System_Prompts['Information'] = state.userInput + + // NEW: Check if tools should be enabled for this step + const stepConfig = stepConfigs.findTarget + const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH + + const result = await callClaude( + 'findTarget', + ['Basic', 'Target', 'Information'], + 'Hello! Please help me find a person to contact!', + toolsEnabled // NEW: Pass tool enablement flag + ) + + state.information = result.text + System_Prompts['Information'] = result.text + + return result + }, + + // ENHANCED: Enable tools for web search (this step is inherently search-based) + webSearch: async (state) => { + System_Prompts['Information'] = state.userInput + + // NEW: Check if tools should be enabled for this step + const stepConfig = stepConfigs.webSearch + const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH + + const result = await callClaude( + 'webSearch', + ['Basic', 'webSearch', 'Information', 'Results'], + 'Hello! Please research this person!', + toolsEnabled // NEW: Pass tool enablement flag + ) + + state.information = result.text + System_Prompts['Information'] = result.text + + return result + }, + + // ENHANCED: Enable tools for research step research: async (state) => { System_Prompts['Information'] = state.userInput + // NEW: Check if tools should be enabled for this step + const stepConfig = stepConfigs.research + const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH + const result = await callClaude( 'research', ['Basic', 'Mail', 'Information', 'Research'], - "Hello! Please update the list of information by replacing all instances of 'undefined' with something that belongs under their respective header based on the rest of the information provided. Thank you!" + "Hello! Please update the list of information by replacing all instances of 'undefined' with something that belongs under their respective header based on the rest of the information provided. Thank you!", + toolsEnabled // NEW: Pass tool enablement flag ) state.information = result.text @@ -351,11 +675,13 @@ const stepHandlers: Record< return result }, + // UNCHANGED: Text processing steps remain without tools for performance firstDraft: async (state) => { return await callClaude( 'firstDraft', ['Basic', 'Mail', 'First_Draft', 'Results'], 'Hello! Please write an email draft using the following information. \n' + state.information + // NOTE: No toolsEnabled parameter = defaults to false ) }, @@ -364,6 +690,7 @@ const stepHandlers: Record< 'firstCut', ['Basic', 'Mail', 'Information', 'First_Cut', 'Results'], 'Hello! Please cut the following email draft. \n \n' + state.email + // NOTE: No toolsEnabled parameter = defaults to false ) }, @@ -372,6 +699,7 @@ const stepHandlers: Record< 'firstEdit', ['Basic', 'Mail', 'Information', 'First_Edit', 'Results'], 'Hello! Please edit the following email draft. \n \n' + state.email + // NOTE: No toolsEnabled parameter = defaults to false ) }, @@ -380,6 +708,7 @@ const stepHandlers: Record< 'toneEdit', ['Basic', 'Mail', 'Information', 'Tone_Edit', 'Results'], 'Hello! Please edit the tone of the following email draft. \n \n' + state.email + // NOTE: No toolsEnabled parameter = defaults to false ) }, @@ -388,10 +717,10 @@ const stepHandlers: Record< 'finalEdit', ['Basic', 'Mail', 'Information', 'Final_Edit', 'Checklist', 'Results'], 'Hello! Please edit the following email draft. \n \n' + state.email + // NOTE: No toolsEnabled parameter = defaults to false ) } } - // Process a specific step async function processStep(state: WriteState): Promise { if (!state.nextStep) { @@ -416,8 +745,8 @@ async function processStep(state: WriteState): Promise { const result = await stepHandler(state) - // Update email content (except for research step which updates information) - if (currentStep !== 'research') { + // Update email content (except for research-like steps which update information) + if (/*!['research', 'findTarget', 'webSearch']*/ !['research'].includes(currentStep)) { state.email = result.text } @@ -428,20 +757,13 @@ async function processStep(state: WriteState): Promise { durationSec: result.durationSec }) - // Set next step - const allSteps: StepName[] = [ - 'research', - 'firstDraft', - 'firstCut', - 'firstEdit', - 'toneEdit', - 'finalEdit' - ] - const currentIndex = allSteps.indexOf(currentStep) - - if (currentIndex !== -1 && currentIndex < allSteps.length - 1) { - state.nextStep = allSteps[currentIndex + 1] - state.currentStep = allSteps[currentIndex + 1] + // Set next step based on workflow configuration + const workflowSteps = workflowConfigs[state.workflowType].steps + const currentIndex = workflowSteps.indexOf(currentStep) + + if (currentIndex !== -1 && currentIndex < workflowSteps.length - 1) { + state.nextStep = workflowSteps[currentIndex + 1] + state.currentStep = workflowSteps[currentIndex + 1] } else { // Last step completed, mark as complete state.nextStep = null @@ -474,7 +796,9 @@ export async function POST({ fetch, request }) { // Continue an existing process try { state = JSON.parse(requestData.stateToken) as WriteState - console.log(`${pencil} write: Continuing from step ${state.step}`) + console.log( + `${pencil} write: Continuing from step ${state.step} (workflow ${state.workflowType})` + ) } catch (error) { console.error('Error parsing state token:', error) return json({ @@ -497,8 +821,11 @@ export async function POST({ fetch, request }) { } as ChatResponse) } - // Initialize new state + // Initialize new state with workflow parsing state = initializeState(info) + console.log( + `${pencil} write: Detected workflow type ${state.workflowType}: ${workflowConfigs[state.workflowType].description}` + ) // For initial calls (no stateToken), return progress string without processing if (requestData.stateToken === undefined) { From 927525583f4aa7604ecb25ecd2c0f5d965d6e8b7 Mon Sep 17 00:00:00 2001 From: andrei Date: Mon, 16 Jun 2025 09:22:46 +0200 Subject: [PATCH 03/16] Added fields to the Email Writer for autofilling responses. --- src/routes/write/+page.svelte | 328 +++++++++++++++++++++++++++++----- 1 file changed, 283 insertions(+), 45 deletions(-) diff --git a/src/routes/write/+page.svelte b/src/routes/write/+page.svelte index b10fd9e7d..ff3849cc0 100644 --- a/src/routes/write/+page.svelte +++ b/src/routes/write/+page.svelte @@ -23,12 +23,13 @@ // Use a unique localStorage key to avoid conflicts with other pages const STORAGE_KEY = 'email_writer_messages' + // CLAUDE CHANGE: Added storage key for form data + const FORM_DATA_STORAGE_KEY = 'email_writer_form_data' let messages: Message[] = typeof localStorage !== 'undefined' ? JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]') : [] // Array for form - let input_arr = new Array(35) let loading = false let apiAvailable = true // Default to true, will be updated after first API call const maxMessages = 20 @@ -131,7 +132,7 @@ const formSections_MessageDetails: FieldSection[] = [ { - title: 'The Message', + title: 'Message Details', subsections: [ { title: 'Logical Structure', @@ -184,9 +185,9 @@ } ] - // Flatten the questions array for accessing by index + // CLAUDE CHANGE: Fixed the population of paragraphText_MessageDetails - was incorrectly using formSections_Research const paragraphText_MessageDetails: string[] = [] - formSections_Research.forEach((section) => { + formSections_MessageDetails.forEach((section) => { section.subsections.forEach((subsection) => { subsection.questions.forEach((question) => { paragraphText_MessageDetails.push(question) @@ -229,15 +230,56 @@ }) }) + // CLAUDE CHANGE: Added mapping functions to get correct arrays based on active form + function getCurrentInputArray(): string[] { + switch (activeForm) { + case 'form1': + return form1_input_arr + case 'form2': + return form2_input_arr + case 'form3': + return form3_input_arr + case 'form4': + return form4_input_arr + default: + return form2_input_arr + } + } + + function getCurrentQuestionArray(): string[] { + switch (activeForm) { + case 'form1': + return paragraphText_Research + case 'form2': + return paragraphText_Target + case 'form3': + return paragraphText_Message + case 'form4': + return paragraphText_MessageDetails + default: + return paragraphText_Target + } + } + function clear_arr(arr: string[]) { for (var i in arr) { arr[i] = '' } } + // CLAUDE CHANGE: Updated clear function to clear current form and reset to form1 function clear() { messages = [] localStorage.setItem(STORAGE_KEY, JSON.stringify(messages)) + + // Clear all form arrays + clear_arr(form1_input_arr) + clear_arr(form2_input_arr) + clear_arr(form3_input_arr) + clear_arr(form4_input_arr) + + // Clear form data from localStorage + localStorage.removeItem(FORM_DATA_STORAGE_KEY) } function copy() { @@ -249,38 +291,108 @@ ) } - // Magic test function that populates three specific fields and starts the process - for demo purposes + // CLAUDE CHANGE: Updated runTest to work with currently active form and provide appropriate test data function runTest() { // Clear any existing chat clear() - // Clear all input fields - clear_arr(input_arr) + // Get current form arrays + const currentInputArray = getCurrentInputArray() + const currentQuestionArray = getCurrentQuestionArray() - // Find index for specific fields based on their question text - const roleAuthorityIndex = paragraphText_Target.findIndex( - (q) => q === 'Understanding their role and potential authority' - ) - const objectiveIndex = paragraphText_Target.findIndex((q) => q === 'Concrete action requested') - const outcomeIndex = paragraphText_Target.findIndex((q) => q === 'Specific outcome desired') + // Clear current input fields + clear_arr(currentInputArray) - input_arr[roleAuthorityIndex] = 'You are writing for a child of about thirteen years old.' - input_arr[objectiveIndex] = - 'Child understands that the current fast worldwide race to build AGI before we know how to make it safe could mean everybody dies. It sounds like science fiction, but experts are increasingly concerned. Of course the grown-ups should do something about this, but because it can be hard for them to admit to themselves that those they love could be in so much danger, then even once they hear about the problem they find reasons not to believe or act. Being sincere, kind and persistent with them that you need protecting can help everybody in the world.' - input_arr[outcomeIndex] = - "The child isn't too scared, but thinks this is a very important problem all their friends, family and teachers should know about and be actively working to solve." + // Provide test data based on active form + switch (activeForm) { + case 'form1': + // Test data for research form + if (currentInputArray.length > 0) { + currentInputArray[0] = + 'Local city council member or school board representative who handles education policy and child safety issues. Location: California, Los Angeles.' + } + break + + case 'form2': + // Test data for target/personal context form + const roleAuthorityIndex = currentQuestionArray.findIndex( + (q) => q === 'Understanding their role and potential authority' + ) + if (roleAuthorityIndex >= 0) { + currentInputArray[roleAuthorityIndex] = + 'You are writing for a local government official with decision-making authority over education and safety policies.' + } + break + + case 'form3': + // Test data for message form + const objectiveIndex = currentQuestionArray.findIndex( + (q) => q === 'Concrete action requested' + ) + const outcomeIndex = currentQuestionArray.findIndex((q) => q === 'Specific outcome desired') + + if (objectiveIndex >= 0) { + currentInputArray[objectiveIndex] = + 'Official takes action to ensure AI safety education and policies are implemented in local institutions.' + } + if (outcomeIndex >= 0) { + currentInputArray[outcomeIndex] = + 'Local community becomes informed about AI risks and appropriate safety measures are put in place.' + } + break + + case 'form4': + // Test data for message details form + const urgencyIndex = currentQuestionArray.findIndex((q) => q === 'Urgency of the request') + const toneIndex = currentQuestionArray.findIndex( + (q) => q === 'Balancing professionalism and approachability' + ) + + if (urgencyIndex >= 0) { + currentInputArray[urgencyIndex] = + 'High urgency due to rapidly advancing AI development timeline.' + } + if (toneIndex >= 0) { + currentInputArray[toneIndex] = + 'Professional but accessible tone that conveys seriousness without being alarmist.' + } + break + } sendMessage() } + // CLAUDE CHANGE: Updated sendMessage to work with currently active form only async function sendMessage() { + const currentInputArray = getCurrentInputArray() + const currentQuestionArray = getCurrentQuestionArray() + let input = '' - for (var i in paragraphText_Target) { - input = input + paragraphText_Target[i] + ':\n' + input_arr[i] + '\n\n' + switch (activeForm) { + case 'form1': + input = input + '[1]' + break + + case 'form2': + input = input + '[2]' + break + + case 'form3': + input = input + '[3]' + break + + case 'form4': + input = input + '[4]' + break + } + for (let i = 0; i < currentQuestionArray.length; i++) { + input = input + currentQuestionArray[i] + ':\n' + (currentInputArray[i] || '') + '\n\n' } + messages = [...messages, { content: input, role: 'user' }] - clear_arr(input_arr) + // Clear current form fields + clear_arr(currentInputArray) loading = true try { @@ -387,15 +499,18 @@ } } - // Update form fields if we have information from the research step + // CLAUDE CHANGE: Updated auto-fill logic to work with currently active form if (data.information) { + const currentInputArray = getCurrentInputArray() + const currentQuestionArray = getCurrentQuestionArray() + // Parse the information string to extract field values const lines = data.information.split('\n') let currentField = -1 for (let line of lines) { - // Look for field headers matching our paragraphText_Target array - const fieldIndex = paragraphText_Target.findIndex((text) => + // Look for field headers matching our current question array + const fieldIndex = currentQuestionArray.findIndex((text) => line.trim().startsWith(text + ':') ) @@ -404,8 +519,11 @@ } else if (currentField >= 0 && line.trim()) { // Only update when there's actual content and the field is empty const lineContent = line.trim() - if (lineContent && (!input_arr[currentField] || input_arr[currentField] === '')) { - input_arr[currentField] = lineContent + if ( + lineContent && + (!currentInputArray[currentField] || currentInputArray[currentField] === '') + ) { + currentInputArray[currentField] = lineContent } } } @@ -413,6 +531,8 @@ // Save messages to localStorage localStorage.setItem(STORAGE_KEY, JSON.stringify(messages)) + // CLAUDE CHANGE: Also save form data + saveFormData() // If not complete, continue with the next step if (!data.complete && data.stateToken) { @@ -432,7 +552,35 @@ } } + // CLAUDE CHANGE: Added form data persistence functions + function saveFormData() { + const formData = { + form1_input_arr, + form2_input_arr, + form3_input_arr, + form4_input_arr, + activeForm + } + localStorage.setItem(FORM_DATA_STORAGE_KEY, JSON.stringify(formData)) + } + + function loadFormData() { + const saved = localStorage.getItem(FORM_DATA_STORAGE_KEY) + if (saved) { + const formData = JSON.parse(saved) + form1_input_arr = formData.form1_input_arr || new Array(paragraphText_Research.length) + form2_input_arr = formData.form2_input_arr || new Array(paragraphText_Target.length) + form3_input_arr = formData.form3_input_arr || new Array(paragraphText_Message.length) + form4_input_arr = + formData.form4_input_arr || new Array(paragraphText_MessageDetails.length) + activeForm = formData.activeForm || 'form1' + } + } + onMount(async () => { + // CLAUDE CHANGE: Load form data on mount + loadFormData() + // Check API availability on component mount try { const response = await fetch('api/write') @@ -464,24 +612,22 @@ // Add these variables for form toggling let activeForm = 'form1' // Default active form - // Function to set the active form + // CLAUDE CHANGE: Updated setActiveForm to save form data when switching function setActiveForm(formId) { + saveFormData() // Save current state before switching activeForm = formId + saveFormData() // Save current state after switching to save activeForm console.log(formId) } // UNTESTED AND GENERATED BY AI // Create separate arrays for each form's inputs //let input_arr = Array(formSections_Target.flatMap(s => s.subsections.flatMap(ss => ss.questions)).length).fill(''); - let form2_input_arr = Array( - formSections_Target.flatMap((s) => s.subsections.flatMap((ss) => ss.questions)).length - ).fill('') - let form3_input_arr = Array( - formSections_Target.flatMap((s) => s.subsections.flatMap((ss) => ss.questions)).length - ).fill('') - let form4_input_arr = Array( - formSections_Target.flatMap((s) => s.subsections.flatMap((ss) => ss.questions)).length - ).fill('') + // CLAUDE CHANGE: Fixed form array initialization to use correct lengths + let form1_input_arr = Array(paragraphText_Research.length).fill('') + let form2_input_arr = Array(paragraphText_Target.length).fill('') + let form3_input_arr = Array(paragraphText_Message.length).fill('') + let form4_input_arr = Array(paragraphText_MessageDetails.length).fill('') // Top of the page const title = `Write Email Content` @@ -606,8 +752,9 @@ {:else} +
    - + {#if activeForm === 'form1'}
    {#each formSections_Research as section, sectionIndex} @@ -616,12 +763,12 @@

    {subsection.title}

    {#each subsection.questions as question, questionIndex} - {@const globalIndex = getQuestionIndex(question)} + {@const globalIndex = paragraphText_Research.findIndex((text) => text === question)}

    {question}

    {/each} @@ -630,7 +777,96 @@
    {/if} - + + + + + {#if activeForm === 'form2'} +
    +

    Finding A Target (For Personal Context)

    + {#each formSections_Research as section, sectionIndex} + {#each section.subsections as subsection, subsectionIndex} +

    {subsection.title}

    + + {#each subsection.questions as question, questionIndex} + {@const globalIndex = paragraphText_Research.findIndex((text) => text === question)} + +

    {question}

    + + {/each} + {/each} + {/each} +
    + + + + +
    + {/if} + + + {#if activeForm === 'form3'} +
    +

    Finding A Target (For Message Content)

    + {#each formSections_Research as section, sectionIndex} + {#each section.subsections as subsection, subsectionIndex} +

    {subsection.title}

    + + {#each subsection.questions as question, questionIndex} + {@const globalIndex = paragraphText_Research.findIndex((text) => text === question)} + +

    {question}

    + + {/each} + {/each} + {/each} +
    + + +
    + {/if} + + + {#if activeForm === 'form4'} +
    +

    Finding A Target (For Message Details)

    + {#each formSections_Research as section, sectionIndex} + {#each section.subsections as subsection, subsectionIndex} +

    {subsection.title}

    + + {#each subsection.questions as question, questionIndex} + {@const globalIndex = paragraphText_Research.findIndex((text) => text === question)} + +

    {question}

    + + {/each} + {/each} + {/each} +
    + + +
    + {/if} + + {#if activeForm === 'form2'}
    @@ -658,7 +894,7 @@

    {question}

    {/each} @@ -667,7 +903,7 @@
    {/if} - + {#if activeForm === 'form3'}
    {#each formSections_Message as section, sectionIndex} @@ -676,7 +912,7 @@

    {subsection.title}

    {#each subsection.questions as question, questionIndex} - {@const globalIndex = getQuestionIndex(question)} + {@const globalIndex = paragraphText_Message.findIndex((text) => text === question)}

    {question}

    - {/each} - {/each} - {/each} -
    - - -
    - {/if} - - - - - - {#if activeForm === 'form4'} -
    -

    Finding A Target (For Message Details)

    - {#each formSections_Research as section, sectionIndex} - {#each section.subsections as subsection, subsectionIndex} -

    {subsection.title}

    - - {#each subsection.questions as question, questionIndex} - {@const globalIndex = paragraphText_Research.findIndex((text) => text === question)} - -

    {question}

    - - {/each} - {/each} - {/each} -
    - - -
    - {/if} - - + {#if activeForm === 'form2'}
    @@ -888,7 +821,7 @@ {#each subsection.questions as question, questionIndex} - {@const globalIndex = getQuestionIndex(question)} + {@const globalIndex = paragraphText_Target.findIndex((text) => text === question)} {#if subsection.title === 'Supporting Evidence' && questionIndex === 0} @@ -909,7 +842,7 @@
    {/if} - + {#if activeForm === 'form3'}
    {#each formSections_Message as section, sectionIndex} @@ -932,7 +865,7 @@
    {/if} - + {#if activeForm === 'form4'}
    {#each formSections_MessageDetails as section, sectionIndex} From 8a73a997c996f0afe5f66a577609b3b54d7031f7 Mon Sep 17 00:00:00 2001 From: andrei Date: Thu, 19 Jun 2025 18:29:51 +0200 Subject: [PATCH 06/16] Fixed minor pnpm check errors in src/routes/write and src/routes/api/write, mostly type declarations, checking that the errors still present are, in fact, result of outdated check files. --- src/routes/write/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/write/+page.svelte b/src/routes/write/+page.svelte index 16a190afc..f42bb0b57 100644 --- a/src/routes/write/+page.svelte +++ b/src/routes/write/+page.svelte @@ -629,7 +629,7 @@ let activeForm = 'form1' // Default active form // CLAUDE CHANGE: Updated setActiveForm to save form data when switching - function setActiveForm(formId) { + function setActiveForm(formId: string) { saveFormData() // Save current state before switching activeForm = formId saveFormData() // Save current state after switching to save activeForm From aaa9c99761f2e2fe815b90254a24aa5f791bd89e Mon Sep 17 00:00:00 2001 From: andrei Date: Mon, 23 Jun 2025 14:10:13 +0200 Subject: [PATCH 07/16] Incomplete: Split api/write/+server.ts in twain. --- src/routes/api/write/+server.ts | 668 ++---------------- .../api/write/process-claude-background.ts | 513 ++++++++++++++ 2 files changed, 583 insertions(+), 598 deletions(-) create mode 100644 src/routes/api/write/process-claude-background.ts diff --git a/src/routes/api/write/+server.ts b/src/routes/api/write/+server.ts index cc66f4f59..984932479 100644 --- a/src/routes/api/write/+server.ts +++ b/src/routes/api/write/+server.ts @@ -1,26 +1,9 @@ import { error, json } from '@sveltejs/kit' import { env } from '$env/dynamic/private' -import Anthropic from '@anthropic-ai/sdk' +import { processStep, stepConfigs } from './process-claude-background' -// Safely access the API key, will be undefined if not set -const ANTHROPIC_API_KEY_FOR_WRITE = env.ANTHROPIC_API_KEY_FOR_WRITE || undefined -// NEW: Add global toggle for web search functionality -const ENABLE_WEB_SEARCH = env.ENABLE_WEB_SEARCH !== 'false' // Default to true unless explicitly disabled -// NEW: Add configurable rate limiting for tool calls per step -const MAX_TOOL_CALLS_PER_STEP = parseInt(env.MAX_TOOL_CALLS_PER_STEP || '3') - -// Flag to track if API is available -const IS_API_AVAILABLE = !!ANTHROPIC_API_KEY_FOR_WRITE - -// Log warning during build if API key is missing -if (!IS_API_AVAILABLE) { - console.warn( - '⚠️ ANTHROPIC_API_KEY_FOR_WRITE is not set. The /write page will operate in limited mode.' - ) -} - -// Define step types for server-side use -type StepName = +// Type definitions +export type StepName = | 'findTarget' | 'webSearch' | 'research' @@ -30,276 +13,92 @@ type StepName = | 'toneEdit' | 'finalEdit' -// Define workflow types -type WorkflowType = '1' | '2' | '3' | '4' +export type WorkflowType = '1' | '2' | '3' | '4' -// NEW: Define step configuration interface for tool usage -interface StepConfig { - toolsEnabled?: boolean // Whether this step can use tools - maxToolCalls?: number // Maximum tool calls for this step (overrides global) - description?: string // Enhanced description when tools are used +export interface StepConfig { + toolsEnabled?: boolean + maxToolCalls?: number + description?: string } -// ENHANCED: Extend workflow configuration to support step configs -type WorkflowConfig = { +export type WorkflowConfig = { steps: StepName[] description: string - stepConfigs?: Record // NEW: Optional step-level configuration + stepConfigs?: Record } -// NEW: Define step-level tool configurations -const stepConfigs: Record = { - // Research-focused steps that benefit from web search - findTarget: { - toolsEnabled: true, - maxToolCalls: 5, - description: 'Find possible targets (using web search)' - }, - webSearch: { - toolsEnabled: true, - maxToolCalls: 3, - description: 'Research the target (using web search)' - }, - research: { - toolsEnabled: false, - maxToolCalls: 2, - description: 'Auto-fill missing user inputs' - }, - // Text processing steps remain tool-free for performance - firstDraft: { toolsEnabled: false }, - firstCut: { toolsEnabled: false }, - firstEdit: { toolsEnabled: false }, - toneEdit: { toolsEnabled: false }, - finalEdit: { toolsEnabled: false } +export interface WriteState { + step: StepName | 'complete' | 'start' + workflowType: WorkflowType + userInput: string + email: string + information?: string + completedSteps: Array<{ + name: StepName + durationSec: number + }> + currentStep: StepName | null + remainingSteps: StepName[] + nextStep: StepName | null } -const workflowConfigs: Record = { +export type ChatResponse = { + response: string + apiAvailable?: boolean + stateToken?: string + progressString?: string + complete?: boolean + information?: string +} + +export type Message = { + role: 'user' | 'assistant' | 'system' + content: string +} + +export interface JobStorage { + jobId: string + status: 'pending' | 'processing' | 'completed' | 'failed' + writeState: WriteState // Your existing state object + error?: string + createdAt: Date + completedAt?: Date +} + +// Environment configuration +const ANTHROPIC_API_KEY_FOR_WRITE = env.ANTHROPIC_API_KEY_FOR_WRITE || undefined +const ENABLE_WEB_SEARCH = env.ENABLE_WEB_SEARCH !== 'false' +const IS_API_AVAILABLE = !!ANTHROPIC_API_KEY_FOR_WRITE + +// Workflow configurations +export const workflowConfigs: Record = { '1': { steps: ['findTarget'], description: 'Find Target Only', - stepConfigs // NEW: Include step configurations + stepConfigs }, '2': { steps: ['webSearch', 'research'], description: 'Web Search + Autofill', - stepConfigs // NEW: Include step configurations + stepConfigs }, '3': { steps: ['research'], description: 'Autofill only', - stepConfigs // NEW: Include step configurations + stepConfigs }, '4': { steps: ['firstDraft', 'firstCut', 'firstEdit', 'toneEdit', 'finalEdit'], description: 'Full Email Generation', - stepConfigs // NEW: Include step configurations + stepConfigs } } -// Server-side state management interface (not exposed to client) -interface WriteState { - step: StepName | 'complete' | 'start' // Current/completed step - workflowType: WorkflowType // Type of workflow being executed - userInput: string // Original input from form (cleaned, without prefix) - email: string // Current email content - information?: string // Processed information after research - completedSteps: Array<{ - name: StepName - durationSec: number - }> - currentStep: StepName | null // Step being processed - remainingSteps: StepName[] // Steps still to do - nextStep: StepName | null // Next step to run (null if complete) -} -// Client-facing response type -export type ChatResponse = { - response: string // Email content to display - apiAvailable?: boolean // Is API available - stateToken?: string // Opaque state token to pass to next request - progressString?: string // Human-readable progress string - complete?: boolean // Is the process complete - information?: string // Processed information for form fields -} - -export type Message = { - role: 'user' | 'assistant' | 'system' - content: string -} - -const System_Prompts: { [id: string]: string } = {} -System_Prompts['Basic'] = `You are a helpful AI assistant. - -Note: Some fields in the information may begin with a robot emoji (πŸ€–). This indicates the field was automatically generated during research. You can use this information normally, just ignore the emoji marker.` - -System_Prompts['Mail'] = ` -What follows is the anatomy of a good email, a set of guidelines and -criteria for writing a good mail. Each paragraph, except the last represents -a distinct part of the email. - -Subject line: -The subject line should be short, informative and clearly communicate -the goal of the mail. It must grab the attention and capture the -interest of the recipient. Avoid clichΓ© language. - -Greeting: -The greeting must match the tone of the mail. If possible, address the -recipient by the appropriate title. Keep it short, and mention the reason -for the mail. Establish a strong connection with the recipient: Are they -a politician meant to represent you? Is it regarding something they've -recently done? Make the recipient feel like they owe you an answer. - -First paragraph: -Explain what the purpose of the email is. It must be concise and captivating, -most people who receive many emails learn to quickly dismiss many. Make -sure the relation is established and they have a reason to read on. - -Body paragraph: -The main body of the email should be informative and contain the information -of the mail. Take great care not to overwhelm the reader: it must be -logically structured and not too full of facts. The message should remain -clear and the relation to the greeting and first paragraph must remain clear. -It should not be too long, otherwise it might get skimmed. Links to further -information can be provided. - -Conclusion: -Keep this short and sweet. Make sure it has a CLEAR CALL TO ACTION! -Restate the reason the recipient should feel the need to act. Thank them -for their time and/or your ask. - -General: -Make sure the formatting isn't too boring. Write in a manner the recipient -would respond well to: Do not argue with them, do not mention views they -probably won't share. Try to play to things they said before and that fit -their persona. Keep the tone consistent and not too emotional. Do not sound -crazy. -` -System_Prompts['Checklist'] = ` -Checklist Before Sending -Message Verification - Is the purpose crystal clear? - Have I provided necessary context? - Is there a specific, achievable call to action? - Have I proofread for tone and clarity? -` - -System_Prompts['First_Draft'] = ` -Using the information that will be provided by the user, write the mail -according to the criteria. Get all the information into the mail. -Don't worry about it being too long. Keep the message powerful. -` - -System_Prompts['First_Cut'] = ` -You will be provided with an email by the user. -Remove redundant information and clean up the structure. The point of this pass is -to have the structure clear and the mail slightly longer than needed. The message -should be clear, the information still mostly present, with only what is -absolutely necessary being removed. -` - -System_Prompts['First_Edit'] = ` -You will be provided with an email by the user. The following points are paramount: -Make sure the flow of information is natural. All paragraphs should be -connected in a sensical manner. Remove odd, unfitting or overly emotional -language. Make sure the paragraphs fulfill their roles. -` - -System_Prompts['Tone_Edit'] = ` -You will be provided with an email by the user. The following points are paramount: -Adjust the language to match recipient's communication style. Remove potentially -offensive or dismissive language. Ensure the tone matches the relationship and -purpose. Make sure the points and information is relevant for the recipient. -Assume the recipient's position: How would they react to the mail? What information -would resonate with them? What wouldn't? Do not compromise on the message. -` - -System_Prompts['Final_Edit'] = ` -You will be provided with an email by the user. Make sure the email matches the -criteria initially described. Check spelling, grammar and tone. -` - -System_Prompts['Making_Template'] = ` -Making a template out of an email requires a good email as a base, then punching massive -holes into the email to allow for the fitting of new information, specifically in tone -and style as well as personal connection. The information should be kept, as well as the -structural flow of the email and especially between the paragraphs. Provide clearly -denoted comments on what was removed and by what it should be replaced. - -The user will provide an email for you to turn into a template using the method described before. -` - -System_Prompts['Improving_Template'] = ` -Assume the role of someone filling in the email template. How much do you have to -rewrite text to make you contributions fit? Can you keep the email brief? Are you restricted -by any word choices and sentence structures? Can you instert your own personality into the -template without too much effort? With these considerations, improve the template. - -The user will provide an email template for you to improve using the method described before. -` - -System_Prompts['Explain'] = ` -When making choices, provide a clearly labeled rationale for why you chose as you did -and what informed those decisions. -` - -System_Prompts['Results'] = ` -Only reply with the final results, a.k.a. the final email, and absolutely nothing else. -` - -System_Prompts['Research'] = ` -Please replace all mentions of 'undefined' with the apropriate information that should -go in that space, derived from the rest of the information. - -Important: For any field you fill in that was originally 'undefined' or empty, prefix -your answer with a robot emoji (πŸ€–) to indicate it was automatically generated. - -Example: -Original: "Preferred communication style: undefined" -Your output: "Preferred communication style: πŸ€– Formal but approachable" - -Please remember that you are addressing this person, and try to make all inferences based on the information provided and your own knowledge. Err on the side of caution: if you are unsure, be polite and neutral. - -Output the full information, including your edits. Output nothing else. -` - -System_Prompts['Target'] = ` -Please use your internet search capability to find individuals involved with AI safety who match the following description. - -For each person you find (aim for 3-5 people), please provide: -1. Name and current position -2. Why they're relevant to AI safety -3. Their organization -4. Brief note on their public stance on AI safety - -Please cite your sources for each person. -` - -//Preface with '[Person's Name] = John Doe' etc. -System_Prompts['webSearch'] = ` -Please use your internet search capability to research [Person's Name] who is [current role] at [organization/affiliation]. I plan to contact them about AI safety concerns. - -Search for and provide: -1. Professional background (education, career history, notable positions) -2. Their involvement with AI issues (policy positions, public statements, initiatives, articles, interviews) -3. Their public views on AI development and safety (with direct quotes where possible) -4. Recent activities related to technology policy or AI (last 6-12 months) -5. Communication style and key terms they use when discussing technology issues -6. Notable connections (organizations, committees, coalitions, or influential individuals they work with) -7. Contact information (professional email or official channels if publicly available) - -Please cite all sources you use and only include information you can verify through your internet search. If you encounter conflicting information, note this and provide the most reliable source. - -BE BRIEF! This is extremely important. Try to output only a few lines of text for each questions. -` - -// Only initialize the client if we have an API key -const anthropic = IS_API_AVAILABLE - ? new Anthropic({ - apiKey: ANTHROPIC_API_KEY_FOR_WRITE - }) - : null - -export async function GET() { - return json({ apiAvailable: IS_API_AVAILABLE }) +// Log warning during build if API key is missing +if (!IS_API_AVAILABLE) { + console.warn( + '⚠️ ANTHROPIC_API_KEY_FOR_WRITE is not set. The /write page will operate in limited mode.' + ) } // Helper function to parse workflow type from user input @@ -318,171 +117,8 @@ function parseWorkflowType(userInput: string): { // Default to workflow 4 if no prefix is found return { workflowType: '4', cleanedInput: userInput } } -// NEW: Interface for tool use response content -interface ToolUseContent { - type: 'tool_use' - id: string - name: string - input: any -} - -// NEW: Interface for tool result content -interface ToolResultContent { - type: 'tool_result' - tool_use_id: string - content: string -} - -// ENHANCED: Modified function signature to support optional tool usage -async function callClaude( - stepName: string, - promptNames: string[], - userContent: string, - toolsEnabled: boolean = false // NEW: Optional parameter for tool usage -): Promise<{ text: string; durationSec: number }> { - const pencil = '✏️' - const search = 'πŸ”' // NEW: Icon for tool usage - const logPrefix = `${pencil} write:${stepName}` - const startTime = Date.now() - - console.time(`${logPrefix}`) - - try { - // Check if the API client is available - if (!anthropic) { - throw new Error('Anthropic API client is not initialized. API key is missing.') - } - - // Combine all the specified prompts - const systemPrompt = promptNames.map((name) => System_Prompts[name]).join('') - - // NEW: Determine if tools should be included in this call - const shouldUseTools = toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE - - // NEW: Log tool usage status - if (shouldUseTools) { - console.log(`${search} ${logPrefix}: Tools enabled for this step`) - } - - // FIXED: Use correct web search tool definition matching API documentation - const tools = shouldUseTools - ? [ - { - type: 'web_search_20250305', // CHANGED: Use correct tool type from API docs - name: 'web_search', - max_uses: 3 // ADDED: Limit searches per request - } - ] - : undefined - - // ENHANCED: Create API request with conditional tool support - const requestParams: any = { - model: 'claude-3-7-sonnet-20250219', - max_tokens: 4096, - system: systemPrompt, - messages: [{ role: 'user', content: userContent }] - } - - // NEW: Add tools to request if enabled - if (tools) { - requestParams.tools = tools - } - - // FIXED: Implement proper tool execution loop - let currentMessages = [...requestParams.messages] - let finalText = '' - let toolCallCount = 0 - const maxCalls = Math.min( - MAX_TOOL_CALLS_PER_STEP, - stepConfigs[stepName as StepName]?.maxToolCalls || MAX_TOOL_CALLS_PER_STEP - ) - - while (toolCallCount < maxCalls) { - // Create request with current message history - const currentRequest = { - ...requestParams, - messages: currentMessages - } - - const response = await anthropic.messages.create(currentRequest) - - // Log the request ID at debug level - console.debug(`${logPrefix} requestId: ${response.id}`) - - // FIXED: Process response content properly - let hasToolUse = false - let textContent = '' - - for (const content of response.content) { - if (content.type === 'text') { - textContent += content.text - } else if (content.type === 'server_tool_use' && shouldUseTools) { - // FIXED: Handle server-side tool use (web search is executed automatically) - hasToolUse = true - toolCallCount++ - console.log(`${search} ${logPrefix}: Web search executed - ${content.name}`) - } else if (content.type === 'web_search_tool_result') { - // FIXED: Handle web search results (automatically provided by API) - console.log(`${search} ${logPrefix}: Received web search results`) - } - } - // FIXED: Add assistant's response to conversation history - currentMessages.push({ - role: 'assistant', - content: response.content - }) - - // FIXED: Accumulate text content - finalText += textContent - - // FIXED: Break if no tool use or if we've hit limits - if (!hasToolUse || toolCallCount >= maxCalls) { - break - } - - // FIXED: If there was tool use, Claude might continue in the same turn - // Check if response has pause_turn stop reason - if (response.stop_reason === 'pause_turn') { - // Continue the conversation to let Claude finish its turn - continue - } else { - // Tool use complete, break the loop - break - } - } - - // FIXED: Ensure we have text content - if (!finalText) { - throw new Error('No text content received from Claude') - } - - const elapsed = (Date.now() - startTime) / 1000 // seconds - - // ENHANCED: Log tool usage statistics - if (shouldUseTools && toolCallCount > 0) { - console.log(`${search} ${logPrefix}: Used ${toolCallCount} web searches`) - } - - // Log the full response text at debug level - console.debug(`${logPrefix} full response:\n---\n${finalText}\n---`) - return { text: finalText, durationSec: elapsed } - } catch (error) { - // ENHANCED: Better error handling for tool-related failures - if (toolsEnabled && (error.message?.includes('tool') || error.message?.includes('search'))) { - console.warn( - `${search} ${logPrefix}: Tool error, falling back to text-only mode:`, - error.message - ) - // Retry without tools on tool-related errors - return callClaude(stepName, promptNames, userContent, false) - } - throw error // Re-throw non-tool errors - } finally { - console.timeEnd(`${logPrefix}`) - } -} -// NEW: Function to get step description with tool awareness +// Function to get step description with tool awareness function getStepDescription(stepName: StepName): string { const stepConfig = stepConfigs[stepName] const toolsWillBeUsed = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE @@ -508,11 +144,10 @@ function getStepDescription(stepName: StepName): string { } // Function to generate a progress string from the state - function generateProgressString(state: WriteState): string { const pencil = '✏️' const checkmark = 'βœ“' - const search = 'πŸ”' // NEW: Icon for tool-enabled steps + const search = 'πŸ”' // Get workflow description const workflowDescription = workflowConfigs[state.workflowType].description @@ -520,7 +155,7 @@ function generateProgressString(state: WriteState): string { // Generate the progress string let lis = [] - // ENHANCED: Completed steps with tool usage indicators + // Completed steps with tool usage indicators for (const step of state.completedSteps) { const stepConfig = stepConfigs[step.name] const usedTools = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE @@ -531,7 +166,7 @@ function generateProgressString(state: WriteState): string { ) } - // ENHANCED: Current step with tool usage indicator + // Current step with tool usage indicator if ( state.currentStep && state.step !== 'complete' && @@ -544,7 +179,7 @@ function generateProgressString(state: WriteState): string { lis.push(`
  • ${getStepDescription(state.currentStep)} ${icon}
  • `) } - // ENHANCED: Remaining steps with tool usage preview + // Remaining steps with tool usage preview const completedAndCurrentSteps = [...state.completedSteps.map((s) => s.name), state.currentStep] const filteredRemainingSteps = state.remainingSteps.filter( (step) => !completedAndCurrentSteps.includes(step) @@ -609,171 +244,9 @@ function prepareResponse(state: WriteState): ChatResponse { } } -// Define step handlers in a map for easy lookup -const stepHandlers: Record< - StepName, - (state: WriteState) => Promise<{ text: string; durationSec: number }> -> = { - // ENHANCED: Enable tools for target finding - findTarget: async (state) => { - System_Prompts['Information'] = state.userInput - - // NEW: Check if tools should be enabled for this step - const stepConfig = stepConfigs.findTarget - const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH - - const result = await callClaude( - 'findTarget', - ['Basic', 'Target', 'Information'], - 'Hello! Please help me find a person to contact!', - toolsEnabled // NEW: Pass tool enablement flag - ) - - state.information = result.text - System_Prompts['Information'] = result.text - - return result - }, - - // ENHANCED: Enable tools for web search (this step is inherently search-based) - webSearch: async (state) => { - System_Prompts['Information'] = state.userInput - - // NEW: Check if tools should be enabled for this step - const stepConfig = stepConfigs.webSearch - const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH - - const result = await callClaude( - 'webSearch', - ['Basic', 'webSearch', 'Information', 'Results'], - 'Hello! Please research this person!', - toolsEnabled // NEW: Pass tool enablement flag - ) - - state.information = result.text - System_Prompts['Information'] = System_Prompts['Information'] + '\n\n' + result.text - - return result - }, - - // ENHANCED: Enable tools for research step - research: async (state) => { - System_Prompts['Information'] = state.userInput - - // NEW: Check if tools should be enabled for this step - const stepConfig = stepConfigs.research - const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH - - const result = await callClaude( - 'research', - ['Basic', 'Mail', 'Information', 'Research'], - "Hello! Please update the list of information by replacing all instances of 'undefined' with something that belongs under their respective header based on the rest of the information provided. Thank you!", - toolsEnabled // NEW: Pass tool enablement flag - ) - - state.information = result.text - System_Prompts['Information'] = result.text - - return result - }, - - // UNCHANGED: Text processing steps remain without tools for performance - firstDraft: async (state) => { - return await callClaude( - 'firstDraft', - ['Basic', 'Mail', 'First_Draft', 'Results'], - 'Hello! Please write an email draft using the following information. \n' + state.userInput - // NOTE: No toolsEnabled parameter = defaults to false - ) - }, - - firstCut: async (state) => { - return await callClaude( - 'firstCut', - ['Basic', 'Mail', 'Information', 'First_Cut', 'Results'], - 'Hello! Please cut the following email draft. \n \n' + state.email - // NOTE: No toolsEnabled parameter = defaults to false - ) - }, - - firstEdit: async (state) => { - return await callClaude( - 'firstEdit', - ['Basic', 'Mail', 'Information', 'First_Edit', 'Results'], - 'Hello! Please edit the following email draft. \n \n' + state.email - // NOTE: No toolsEnabled parameter = defaults to false - ) - }, - - toneEdit: async (state) => { - return await callClaude( - 'toneEdit', - ['Basic', 'Mail', 'Information', 'Tone_Edit', 'Results'], - 'Hello! Please edit the tone of the following email draft. \n \n' + state.email - // NOTE: No toolsEnabled parameter = defaults to false - ) - }, - - finalEdit: async (state) => { - return await callClaude( - 'finalEdit', - ['Basic', 'Mail', 'Information', 'Final_Edit', 'Checklist', 'Results'], - 'Hello! Please edit the following email draft. \n \n' + state.email - // NOTE: No toolsEnabled parameter = defaults to false - ) - } -} -// Process a specific step -async function processStep(state: WriteState): Promise { - if (!state.nextStep) { - return { - ...state, - step: 'complete', - currentStep: null - } - } - - const currentStep = state.nextStep as StepName - state.currentStep = currentStep - - // Update remaining steps - remove current step from remaining - state.remainingSteps = state.remainingSteps.filter((step) => step !== currentStep) - - // Execute the step using the step handler from the map - const stepHandler = stepHandlers[currentStep] - if (!stepHandler) { - throw new Error(`Unknown step: ${currentStep}`) - } - - const result = await stepHandler(state) - - // Update email content (except for research-like steps which update information) - if (/*!['research', 'findTarget', 'webSearch']*/ !['research'].includes(currentStep)) { - state.email = result.text - } - - // Update state with results - current step is now completed - state.step = currentStep - state.completedSteps.push({ - name: currentStep, - durationSec: result.durationSec - }) - - // Set next step based on workflow configuration - const workflowSteps = workflowConfigs[state.workflowType].steps - const currentIndex = workflowSteps.indexOf(currentStep) - - if (currentIndex !== -1 && currentIndex < workflowSteps.length - 1) { - state.nextStep = workflowSteps[currentIndex + 1] - state.currentStep = workflowSteps[currentIndex + 1] - } else { - // Last step completed, mark as complete - state.nextStep = null - state.currentStep = null - state.step = 'complete' - } - - return state +// API endpoint handlers +export async function GET() { + return json({ apiAvailable: IS_API_AVAILABLE }) } export async function POST({ fetch, request }) { @@ -792,7 +265,6 @@ export async function POST({ fetch, request }) { // Check if this is a continuation of an existing process let state: WriteState - let stateToken = null if (requestData.stateToken) { // Continue an existing process diff --git a/src/routes/api/write/process-claude-background.ts b/src/routes/api/write/process-claude-background.ts new file mode 100644 index 000000000..4d633dcf5 --- /dev/null +++ b/src/routes/api/write/process-claude-background.ts @@ -0,0 +1,513 @@ +import { env } from '$env/dynamic/private' +import Anthropic from '@anthropic-ai/sdk' + +// Import types and configurations from server +import type { StepName, WriteState, StepConfig, JobStorage } from './+server' +import { workflowConfigs } from './+server' + +// Environment variables for step processing +const ANTHROPIC_API_KEY_FOR_WRITE = env.ANTHROPIC_API_KEY_FOR_WRITE || undefined +const ENABLE_WEB_SEARCH = env.ENABLE_WEB_SEARCH !== 'false' +const MAX_TOOL_CALLS_PER_STEP = parseInt(env.MAX_TOOL_CALLS_PER_STEP || '3') +const IS_API_AVAILABLE = !!ANTHROPIC_API_KEY_FOR_WRITE + +// Initialize Anthropic client +const anthropic = IS_API_AVAILABLE + ? new Anthropic({ + apiKey: ANTHROPIC_API_KEY_FOR_WRITE + }) + : null + +// System prompts for step processing +const System_Prompts: { [id: string]: string } = { + Basic: `You are a helpful AI assistant. + +Note: Some fields in the information may begin with a robot emoji (πŸ€–). This indicates the field was automatically generated during research. You can use this information normally, just ignore the emoji marker.`, + + Mail: ` +What follows is the anatomy of a good email, a set of guidelines and +criteria for writing a good mail. Each paragraph, except the last represents +a distinct part of the email. + +Subject line: +The subject line should be short, informative and clearly communicate +the goal of the mail. It must grab the attention and capture the +interest of the recipient. Avoid clichΓ© language. + +Greeting: +The greeting must match the tone of the mail. If possible, address the +recipient by the appropriate title. Keep it short, and mention the reason +for the mail. Establish a strong connection with the recipient: Are they +a politician meant to represent you? Is it regarding something they've +recently done? Make the recipient feel like they owe you an answer. + +First paragraph: +Explain what the purpose of the email is. It must be concise and captivating, +most people who receive many emails learn to quickly dismiss many. Make +sure the relation is established and they have a reason to read on. + +Body paragraph: +The main body of the email should be informative and contain the information +of the mail. Take great care not to overwhelm the reader: it must be +logically structured and not too full of facts. The message should remain +clear and the relation to the greeting and first paragraph must remain clear. +It should not be too long, otherwise it might get skimmed. Links to further +information can be provided. + +Conclusion: +Keep this short and sweet. Make sure it has a CLEAR CALL TO ACTION! +Restate the reason the recipient should feel the need to act. Thank them +for their time and/or your ask. + +General: +Make sure the formatting isn't too boring. Write in a manner the recipient +would respond well to: Do not argue with them, do not mention views they +probably won't share. Try to play to things they said before and that fit +their persona. Keep the tone consistent and not too emotional. Do not sound +crazy. +`, + + Checklist: ` +Checklist Before Sending +Message Verification + Is the purpose crystal clear? + Have I provided necessary context? + Is there a specific, achievable call to action? + Have I proofread for tone and clarity? +`, + + First_Draft: ` +Using the information that will be provided by the user, write the mail +according to the criteria. Get all the information into the mail. +Don't worry about it being too long. Keep the message powerful. +`, + + First_Cut: ` +You will be provided with an email by the user. +Remove redundant information and clean up the structure. The point of this pass is +to have the structure clear and the mail slightly longer than needed. The message +should be clear, the information still mostly present, with only what is +absolutely necessary being removed. +`, + + First_Edit: ` +You will be provided with an email by the user. The following points are paramount: +Make sure the flow of information is natural. All paragraphs should be +connected in a sensical manner. Remove odd, unfitting or overly emotional +language. Make sure the paragraphs fulfill their roles. +`, + + Tone_Edit: ` +You will be provided with an email by the user. The following points are paramount: +Adjust the language to match recipient's communication style. Remove potentially +offensive or dismissive language. Ensure the tone matches the relationship and +purpose. Make sure the points and information is relevant for the recipient. +Assume the recipient's position: How would they react to the mail? What information +would resonate with them? What wouldn't? Do not compromise on the message. +`, + + Final_Edit: ` +You will be provided with an email by the user. Make sure the email matches the +criteria initially described. Check spelling, grammar and tone. +`, + + Research: ` +Please replace all mentions of 'undefined' with the apropriate information that should +go in that space, derived from the rest of the information. + +Important: For any field you fill in that was originally 'undefined' or empty, prefix +your answer with a robot emoji (πŸ€–) to indicate it was automatically generated. + +Example: +Original: "Preferred communication style: undefined" +Your output: "Preferred communication style: πŸ€– Formal but approachable" + +Please remember that you are addressing this person, and try to make all inferences based on the information provided and your own knowledge. Err on the side of caution: if you are unsure, be polite and neutral. + +Output the full information, including your edits. Output nothing else. +`, + + Target: ` +Please use your internet search capability to find individuals involved with AI safety who match the following description. + +For each person you find (aim for 3-5 people), please provide: +1. Name and current position +2. Why they're relevant to AI safety +3. Their organization +4. Brief note on their public stance on AI safety + +Please cite your sources for each person. +`, + + webSearch: ` +Please use your internet search capability to research [Person's Name] who is [current role] at [organization/affiliation]. I plan to contact them about AI safety concerns. + +Search for and provide: +1. Professional background (education, career history, notable positions) +2. Their involvement with AI issues (policy positions, public statements, initiatives, articles, interviews) +3. Their public views on AI development and safety (with direct quotes where possible) +4. Recent activities related to technology policy or AI (last 6-12 months) +5. Communication style and key terms they use when discussing technology issues +6. Notable connections (organizations, committees, coalitions, or influential individuals they work with) +7. Contact information (professional email or official channels if publicly available) + +Please cite all sources you use and only include information you can verify through your internet search. If you encounter conflicting information, note this and provide the most reliable source. + +BE BRIEF! This is extremely important. Try to output only a few lines of text for each questions. +`, + + Results: ` +Only reply with the final results, a.k.a. the final email, and absolutely nothing else. +` +} + +// Step configurations +export const stepConfigs: Record = { + findTarget: { + toolsEnabled: true, + maxToolCalls: 5, + description: 'Find possible targets (using web search)' + }, + webSearch: { + toolsEnabled: true, + maxToolCalls: 3, + description: 'Research the target (using web search)' + }, + research: { + toolsEnabled: false, + maxToolCalls: 2, + description: 'Auto-fill missing user inputs' + }, + firstDraft: { toolsEnabled: false }, + firstCut: { toolsEnabled: false }, + firstEdit: { toolsEnabled: false }, + toneEdit: { toolsEnabled: false }, + finalEdit: { toolsEnabled: false } +} + +// Enhanced callClaude function with tool support +export async function callClaude( + stepName: string, + promptNames: string[], + userContent: string, + toolsEnabled: boolean = false +): Promise<{ text: string; durationSec: number }> { + const pencil = '✏️' + const search = 'πŸ”' + const logPrefix = `${pencil} write:${stepName}` + const startTime = Date.now() + + console.time(`${logPrefix}`) + + try { + // Check if the API client is available + if (!anthropic) { + throw new Error('Anthropic API client is not initialized. API key is missing.') + } + + // Combine all the specified prompts + const systemPrompt = promptNames.map((name) => System_Prompts[name]).join('') + + // Determine if tools should be included in this call + const shouldUseTools = toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE + + // Log tool usage status + if (shouldUseTools) { + console.log(`${search} ${logPrefix}: Tools enabled for this step`) + } + + // Use correct web search tool definition matching API documentation + const tools = shouldUseTools + ? [ + { + type: 'web_search_20250305', + name: 'web_search', + max_uses: 3 + } + ] + : undefined + + // Create API request with conditional tool support + const requestParams: any = { + model: 'claude-3-7-sonnet-20250219', + max_tokens: 4096, + system: systemPrompt, + messages: [{ role: 'user', content: userContent }] + } + + // Add tools to request if enabled + if (tools) { + requestParams.tools = tools + } + + // Implement proper tool execution loop + let currentMessages = [...requestParams.messages] + let finalText = '' + let toolCallCount = 0 + const maxCalls = Math.min( + MAX_TOOL_CALLS_PER_STEP, + stepConfigs[stepName as StepName]?.maxToolCalls || MAX_TOOL_CALLS_PER_STEP + ) + + while (toolCallCount < maxCalls) { + // Create request with current message history + const currentRequest = { + ...requestParams, + messages: currentMessages + } + + const response = await anthropic.messages.create(currentRequest) + + // Log the request ID at debug level + console.debug(`${logPrefix} requestId: ${response.id}`) + + // Process response content properly + let hasToolUse = false + let textContent = '' + + for (const content of response.content) { + if (content.type === 'text') { + textContent += content.text + } else if (content.type === 'server_tool_use' && shouldUseTools) { + // Handle server-side tool use (web search is executed automatically) + hasToolUse = true + toolCallCount++ + console.log(`${search} ${logPrefix}: Web search executed - ${content.name}`) + } else if (content.type === 'web_search_tool_result') { + // Handle web search results (automatically provided by API) + console.log(`${search} ${logPrefix}: Received web search results`) + } + } + + // Add assistant's response to conversation history + currentMessages.push({ + role: 'assistant', + content: response.content + }) + + // Accumulate text content + finalText += textContent + + // Break if no tool use or if we've hit limits + if (!hasToolUse || toolCallCount >= maxCalls) { + break + } + + // If there was tool use, Claude might continue in the same turn + // Check if response has pause_turn stop reason + if (response.stop_reason === 'pause_turn') { + // Continue the conversation to let Claude finish its turn + continue + } else { + // Tool use complete, break the loop + break + } + } + + // Ensure we have text content + if (!finalText) { + throw new Error('No text content received from Claude') + } + + const elapsed = (Date.now() - startTime) / 1000 // seconds + + // Log tool usage statistics + if (shouldUseTools && toolCallCount > 0) { + console.log(`${search} ${logPrefix}: Used ${toolCallCount} web searches`) + } + + // Log the full response text at debug level + console.debug(`${logPrefix} full response:\n---\n${finalText}\n---`) + return { text: finalText, durationSec: elapsed } + } catch (error) { + // Better error handling for tool-related failures + if (toolsEnabled && (error.message?.includes('tool') || error.message?.includes('search'))) { + console.warn( + `${search} ${logPrefix}: Tool error, falling back to text-only mode:`, + error.message + ) + // Retry without tools on tool-related errors + return callClaude(stepName, promptNames, userContent, false) + } + throw error // Re-throw non-tool errors + } finally { + console.timeEnd(`${logPrefix}`) + } +} + +// Define step handlers in a map for easy lookup +const stepHandlers: Record< + StepName, + (state: WriteState) => Promise<{ text: string; durationSec: number }> +> = { + // Enable tools for target finding + findTarget: async (state) => { + System_Prompts['Information'] = state.userInput + + // Check if tools should be enabled for this step + const stepConfig = stepConfigs.findTarget + const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH + + const result = await callClaude( + 'findTarget', + ['Basic', 'Target', 'Information'], + 'Hello! Please help me find a person to contact!', + toolsEnabled + ) + + state.information = result.text + System_Prompts['Information'] = result.text + + return result + }, + + // Enable tools for web search + webSearch: async (state) => { + System_Prompts['Information'] = state.userInput + + // Check if tools should be enabled for this step + const stepConfig = stepConfigs.webSearch + const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH + + const result = await callClaude( + 'webSearch', + ['Basic', 'webSearch', 'Information', 'Results'], + 'Hello! Please research this person!', + toolsEnabled + ) + + state.information = result.text + System_Prompts['Information'] = System_Prompts['Information'] + '\n\n' + result.text + + return result + }, + + // Enable tools for research step + research: async (state) => { + System_Prompts['Information'] = state.userInput + + // Check if tools should be enabled for this step + const stepConfig = stepConfigs.research + const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH + + const result = await callClaude( + 'research', + ['Basic', 'Mail', 'Information', 'Research'], + "Hello! Please update the list of information by replacing all instances of 'undefined' with something that belongs under their respective header based on the rest of the information provided. Thank you!", + toolsEnabled + ) + + state.information = result.text + System_Prompts['Information'] = result.text + + return result + }, + + // Text processing steps remain without tools for performance + firstDraft: async (state) => { + return await callClaude( + 'firstDraft', + ['Basic', 'Mail', 'First_Draft', 'Results'], + 'Hello! Please write an email draft using the following information. \n' + state.userInput + ) + }, + + firstCut: async (state) => { + return await callClaude( + 'firstCut', + ['Basic', 'Mail', 'Information', 'First_Cut', 'Results'], + 'Hello! Please cut the following email draft. \n \n' + state.email + ) + }, + + firstEdit: async (state) => { + return await callClaude( + 'firstEdit', + ['Basic', 'Mail', 'Information', 'First_Edit', 'Results'], + 'Hello! Please edit the following email draft. \n \n' + state.email + ) + }, + + toneEdit: async (state) => { + return await callClaude( + 'toneEdit', + ['Basic', 'Mail', 'Information', 'Tone_Edit', 'Results'], + 'Hello! Please edit the tone of the following email draft. \n \n' + state.email + ) + }, + + finalEdit: async (state) => { + return await callClaude( + 'finalEdit', + ['Basic', 'Mail', 'Information', 'Final_Edit', 'Checklist', 'Results'], + 'Hello! Please edit the following email draft. \n \n' + state.email + ) + } +} + +// Process a specific step +export async function processStep(state: WriteState): Promise { + if (!state.nextStep) { + return { + ...state, + step: 'complete', + currentStep: null + } + } + + const currentStep = state.nextStep as StepName + state.currentStep = currentStep + + // Update remaining steps - remove current step from remaining + state.remainingSteps = state.remainingSteps.filter((step) => step !== currentStep) + + // Execute the step using the step handler from the map + const stepHandler = stepHandlers[currentStep] + if (!stepHandler) { + throw new Error(`Unknown step: ${currentStep}`) + } + + const result = await stepHandler(state) + + // Update email content (except for research-like steps which update information) + if (!['research'].includes(currentStep)) { + state.email = result.text + } + + // Update state with results - current step is now completed + state.step = currentStep + state.completedSteps.push({ + name: currentStep, + durationSec: result.durationSec + }) + + // Set next step based on workflow configuration + // Note: This requires access to workflowConfigs which should be imported from the server file + const workflowSteps = getWorkflowSteps(state.workflowType) + const currentIndex = workflowSteps.indexOf(currentStep) + + if (currentIndex !== -1 && currentIndex < workflowSteps.length - 1) { + state.nextStep = workflowSteps[currentIndex + 1] + state.currentStep = workflowSteps[currentIndex + 1] + } else { + // Last step completed, mark as complete + state.nextStep = null + state.currentStep = null + state.step = 'complete' + } + + return state +} + +// Helper function to get workflow steps (this will need to be imported from server) +function getWorkflowSteps(workflowType: string): StepName[] { + // This should be imported from the server file or passed as a parameter + // For now, returning a placeholder + const workflowConfigs = { + '1': ['findTarget'], + '2': ['webSearch', 'research'], + '3': ['research'], + '4': ['firstDraft', 'firstCut', 'firstEdit', 'toneEdit', 'finalEdit'] + } + return workflowConfigs[workflowType] || [] +} From 56e936445015b24dae2b87e8775b5705a7957a70 Mon Sep 17 00:00:00 2001 From: andrei Date: Mon, 23 Jun 2025 14:33:53 +0200 Subject: [PATCH 08/16] Went back to the last locally working version. --- src/routes/api/write/+server.ts | 668 ++++++++++++++++++++++++++++---- 1 file changed, 598 insertions(+), 70 deletions(-) diff --git a/src/routes/api/write/+server.ts b/src/routes/api/write/+server.ts index 984932479..cc66f4f59 100644 --- a/src/routes/api/write/+server.ts +++ b/src/routes/api/write/+server.ts @@ -1,9 +1,26 @@ import { error, json } from '@sveltejs/kit' import { env } from '$env/dynamic/private' -import { processStep, stepConfigs } from './process-claude-background' +import Anthropic from '@anthropic-ai/sdk' -// Type definitions -export type StepName = +// Safely access the API key, will be undefined if not set +const ANTHROPIC_API_KEY_FOR_WRITE = env.ANTHROPIC_API_KEY_FOR_WRITE || undefined +// NEW: Add global toggle for web search functionality +const ENABLE_WEB_SEARCH = env.ENABLE_WEB_SEARCH !== 'false' // Default to true unless explicitly disabled +// NEW: Add configurable rate limiting for tool calls per step +const MAX_TOOL_CALLS_PER_STEP = parseInt(env.MAX_TOOL_CALLS_PER_STEP || '3') + +// Flag to track if API is available +const IS_API_AVAILABLE = !!ANTHROPIC_API_KEY_FOR_WRITE + +// Log warning during build if API key is missing +if (!IS_API_AVAILABLE) { + console.warn( + '⚠️ ANTHROPIC_API_KEY_FOR_WRITE is not set. The /write page will operate in limited mode.' + ) +} + +// Define step types for server-side use +type StepName = | 'findTarget' | 'webSearch' | 'research' @@ -13,92 +30,276 @@ export type StepName = | 'toneEdit' | 'finalEdit' -export type WorkflowType = '1' | '2' | '3' | '4' +// Define workflow types +type WorkflowType = '1' | '2' | '3' | '4' -export interface StepConfig { - toolsEnabled?: boolean - maxToolCalls?: number - description?: string +// NEW: Define step configuration interface for tool usage +interface StepConfig { + toolsEnabled?: boolean // Whether this step can use tools + maxToolCalls?: number // Maximum tool calls for this step (overrides global) + description?: string // Enhanced description when tools are used } -export type WorkflowConfig = { +// ENHANCED: Extend workflow configuration to support step configs +type WorkflowConfig = { steps: StepName[] description: string - stepConfigs?: Record + stepConfigs?: Record // NEW: Optional step-level configuration } -export interface WriteState { - step: StepName | 'complete' | 'start' - workflowType: WorkflowType - userInput: string - email: string - information?: string - completedSteps: Array<{ - name: StepName - durationSec: number - }> - currentStep: StepName | null - remainingSteps: StepName[] - nextStep: StepName | null -} - -export type ChatResponse = { - response: string - apiAvailable?: boolean - stateToken?: string - progressString?: string - complete?: boolean - information?: string -} - -export type Message = { - role: 'user' | 'assistant' | 'system' - content: string -} - -export interface JobStorage { - jobId: string - status: 'pending' | 'processing' | 'completed' | 'failed' - writeState: WriteState // Your existing state object - error?: string - createdAt: Date - completedAt?: Date +// NEW: Define step-level tool configurations +const stepConfigs: Record = { + // Research-focused steps that benefit from web search + findTarget: { + toolsEnabled: true, + maxToolCalls: 5, + description: 'Find possible targets (using web search)' + }, + webSearch: { + toolsEnabled: true, + maxToolCalls: 3, + description: 'Research the target (using web search)' + }, + research: { + toolsEnabled: false, + maxToolCalls: 2, + description: 'Auto-fill missing user inputs' + }, + // Text processing steps remain tool-free for performance + firstDraft: { toolsEnabled: false }, + firstCut: { toolsEnabled: false }, + firstEdit: { toolsEnabled: false }, + toneEdit: { toolsEnabled: false }, + finalEdit: { toolsEnabled: false } } -// Environment configuration -const ANTHROPIC_API_KEY_FOR_WRITE = env.ANTHROPIC_API_KEY_FOR_WRITE || undefined -const ENABLE_WEB_SEARCH = env.ENABLE_WEB_SEARCH !== 'false' -const IS_API_AVAILABLE = !!ANTHROPIC_API_KEY_FOR_WRITE - -// Workflow configurations -export const workflowConfigs: Record = { +const workflowConfigs: Record = { '1': { steps: ['findTarget'], description: 'Find Target Only', - stepConfigs + stepConfigs // NEW: Include step configurations }, '2': { steps: ['webSearch', 'research'], description: 'Web Search + Autofill', - stepConfigs + stepConfigs // NEW: Include step configurations }, '3': { steps: ['research'], description: 'Autofill only', - stepConfigs + stepConfigs // NEW: Include step configurations }, '4': { steps: ['firstDraft', 'firstCut', 'firstEdit', 'toneEdit', 'finalEdit'], description: 'Full Email Generation', - stepConfigs + stepConfigs // NEW: Include step configurations } } +// Server-side state management interface (not exposed to client) +interface WriteState { + step: StepName | 'complete' | 'start' // Current/completed step + workflowType: WorkflowType // Type of workflow being executed + userInput: string // Original input from form (cleaned, without prefix) + email: string // Current email content + information?: string // Processed information after research + completedSteps: Array<{ + name: StepName + durationSec: number + }> + currentStep: StepName | null // Step being processed + remainingSteps: StepName[] // Steps still to do + nextStep: StepName | null // Next step to run (null if complete) +} -// Log warning during build if API key is missing -if (!IS_API_AVAILABLE) { - console.warn( - '⚠️ ANTHROPIC_API_KEY_FOR_WRITE is not set. The /write page will operate in limited mode.' - ) +// Client-facing response type +export type ChatResponse = { + response: string // Email content to display + apiAvailable?: boolean // Is API available + stateToken?: string // Opaque state token to pass to next request + progressString?: string // Human-readable progress string + complete?: boolean // Is the process complete + information?: string // Processed information for form fields +} + +export type Message = { + role: 'user' | 'assistant' | 'system' + content: string +} + +const System_Prompts: { [id: string]: string } = {} +System_Prompts['Basic'] = `You are a helpful AI assistant. + +Note: Some fields in the information may begin with a robot emoji (πŸ€–). This indicates the field was automatically generated during research. You can use this information normally, just ignore the emoji marker.` + +System_Prompts['Mail'] = ` +What follows is the anatomy of a good email, a set of guidelines and +criteria for writing a good mail. Each paragraph, except the last represents +a distinct part of the email. + +Subject line: +The subject line should be short, informative and clearly communicate +the goal of the mail. It must grab the attention and capture the +interest of the recipient. Avoid clichΓ© language. + +Greeting: +The greeting must match the tone of the mail. If possible, address the +recipient by the appropriate title. Keep it short, and mention the reason +for the mail. Establish a strong connection with the recipient: Are they +a politician meant to represent you? Is it regarding something they've +recently done? Make the recipient feel like they owe you an answer. + +First paragraph: +Explain what the purpose of the email is. It must be concise and captivating, +most people who receive many emails learn to quickly dismiss many. Make +sure the relation is established and they have a reason to read on. + +Body paragraph: +The main body of the email should be informative and contain the information +of the mail. Take great care not to overwhelm the reader: it must be +logically structured and not too full of facts. The message should remain +clear and the relation to the greeting and first paragraph must remain clear. +It should not be too long, otherwise it might get skimmed. Links to further +information can be provided. + +Conclusion: +Keep this short and sweet. Make sure it has a CLEAR CALL TO ACTION! +Restate the reason the recipient should feel the need to act. Thank them +for their time and/or your ask. + +General: +Make sure the formatting isn't too boring. Write in a manner the recipient +would respond well to: Do not argue with them, do not mention views they +probably won't share. Try to play to things they said before and that fit +their persona. Keep the tone consistent and not too emotional. Do not sound +crazy. +` +System_Prompts['Checklist'] = ` +Checklist Before Sending +Message Verification + Is the purpose crystal clear? + Have I provided necessary context? + Is there a specific, achievable call to action? + Have I proofread for tone and clarity? +` + +System_Prompts['First_Draft'] = ` +Using the information that will be provided by the user, write the mail +according to the criteria. Get all the information into the mail. +Don't worry about it being too long. Keep the message powerful. +` + +System_Prompts['First_Cut'] = ` +You will be provided with an email by the user. +Remove redundant information and clean up the structure. The point of this pass is +to have the structure clear and the mail slightly longer than needed. The message +should be clear, the information still mostly present, with only what is +absolutely necessary being removed. +` + +System_Prompts['First_Edit'] = ` +You will be provided with an email by the user. The following points are paramount: +Make sure the flow of information is natural. All paragraphs should be +connected in a sensical manner. Remove odd, unfitting or overly emotional +language. Make sure the paragraphs fulfill their roles. +` + +System_Prompts['Tone_Edit'] = ` +You will be provided with an email by the user. The following points are paramount: +Adjust the language to match recipient's communication style. Remove potentially +offensive or dismissive language. Ensure the tone matches the relationship and +purpose. Make sure the points and information is relevant for the recipient. +Assume the recipient's position: How would they react to the mail? What information +would resonate with them? What wouldn't? Do not compromise on the message. +` + +System_Prompts['Final_Edit'] = ` +You will be provided with an email by the user. Make sure the email matches the +criteria initially described. Check spelling, grammar and tone. +` + +System_Prompts['Making_Template'] = ` +Making a template out of an email requires a good email as a base, then punching massive +holes into the email to allow for the fitting of new information, specifically in tone +and style as well as personal connection. The information should be kept, as well as the +structural flow of the email and especially between the paragraphs. Provide clearly +denoted comments on what was removed and by what it should be replaced. + +The user will provide an email for you to turn into a template using the method described before. +` + +System_Prompts['Improving_Template'] = ` +Assume the role of someone filling in the email template. How much do you have to +rewrite text to make you contributions fit? Can you keep the email brief? Are you restricted +by any word choices and sentence structures? Can you instert your own personality into the +template without too much effort? With these considerations, improve the template. + +The user will provide an email template for you to improve using the method described before. +` + +System_Prompts['Explain'] = ` +When making choices, provide a clearly labeled rationale for why you chose as you did +and what informed those decisions. +` + +System_Prompts['Results'] = ` +Only reply with the final results, a.k.a. the final email, and absolutely nothing else. +` + +System_Prompts['Research'] = ` +Please replace all mentions of 'undefined' with the apropriate information that should +go in that space, derived from the rest of the information. + +Important: For any field you fill in that was originally 'undefined' or empty, prefix +your answer with a robot emoji (πŸ€–) to indicate it was automatically generated. + +Example: +Original: "Preferred communication style: undefined" +Your output: "Preferred communication style: πŸ€– Formal but approachable" + +Please remember that you are addressing this person, and try to make all inferences based on the information provided and your own knowledge. Err on the side of caution: if you are unsure, be polite and neutral. + +Output the full information, including your edits. Output nothing else. +` + +System_Prompts['Target'] = ` +Please use your internet search capability to find individuals involved with AI safety who match the following description. + +For each person you find (aim for 3-5 people), please provide: +1. Name and current position +2. Why they're relevant to AI safety +3. Their organization +4. Brief note on their public stance on AI safety + +Please cite your sources for each person. +` + +//Preface with '[Person's Name] = John Doe' etc. +System_Prompts['webSearch'] = ` +Please use your internet search capability to research [Person's Name] who is [current role] at [organization/affiliation]. I plan to contact them about AI safety concerns. + +Search for and provide: +1. Professional background (education, career history, notable positions) +2. Their involvement with AI issues (policy positions, public statements, initiatives, articles, interviews) +3. Their public views on AI development and safety (with direct quotes where possible) +4. Recent activities related to technology policy or AI (last 6-12 months) +5. Communication style and key terms they use when discussing technology issues +6. Notable connections (organizations, committees, coalitions, or influential individuals they work with) +7. Contact information (professional email or official channels if publicly available) + +Please cite all sources you use and only include information you can verify through your internet search. If you encounter conflicting information, note this and provide the most reliable source. + +BE BRIEF! This is extremely important. Try to output only a few lines of text for each questions. +` + +// Only initialize the client if we have an API key +const anthropic = IS_API_AVAILABLE + ? new Anthropic({ + apiKey: ANTHROPIC_API_KEY_FOR_WRITE + }) + : null + +export async function GET() { + return json({ apiAvailable: IS_API_AVAILABLE }) } // Helper function to parse workflow type from user input @@ -117,8 +318,171 @@ function parseWorkflowType(userInput: string): { // Default to workflow 4 if no prefix is found return { workflowType: '4', cleanedInput: userInput } } +// NEW: Interface for tool use response content +interface ToolUseContent { + type: 'tool_use' + id: string + name: string + input: any +} + +// NEW: Interface for tool result content +interface ToolResultContent { + type: 'tool_result' + tool_use_id: string + content: string +} + +// ENHANCED: Modified function signature to support optional tool usage +async function callClaude( + stepName: string, + promptNames: string[], + userContent: string, + toolsEnabled: boolean = false // NEW: Optional parameter for tool usage +): Promise<{ text: string; durationSec: number }> { + const pencil = '✏️' + const search = 'πŸ”' // NEW: Icon for tool usage + const logPrefix = `${pencil} write:${stepName}` + const startTime = Date.now() + + console.time(`${logPrefix}`) + + try { + // Check if the API client is available + if (!anthropic) { + throw new Error('Anthropic API client is not initialized. API key is missing.') + } + + // Combine all the specified prompts + const systemPrompt = promptNames.map((name) => System_Prompts[name]).join('') + + // NEW: Determine if tools should be included in this call + const shouldUseTools = toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE + + // NEW: Log tool usage status + if (shouldUseTools) { + console.log(`${search} ${logPrefix}: Tools enabled for this step`) + } + + // FIXED: Use correct web search tool definition matching API documentation + const tools = shouldUseTools + ? [ + { + type: 'web_search_20250305', // CHANGED: Use correct tool type from API docs + name: 'web_search', + max_uses: 3 // ADDED: Limit searches per request + } + ] + : undefined + + // ENHANCED: Create API request with conditional tool support + const requestParams: any = { + model: 'claude-3-7-sonnet-20250219', + max_tokens: 4096, + system: systemPrompt, + messages: [{ role: 'user', content: userContent }] + } + + // NEW: Add tools to request if enabled + if (tools) { + requestParams.tools = tools + } + + // FIXED: Implement proper tool execution loop + let currentMessages = [...requestParams.messages] + let finalText = '' + let toolCallCount = 0 + const maxCalls = Math.min( + MAX_TOOL_CALLS_PER_STEP, + stepConfigs[stepName as StepName]?.maxToolCalls || MAX_TOOL_CALLS_PER_STEP + ) + + while (toolCallCount < maxCalls) { + // Create request with current message history + const currentRequest = { + ...requestParams, + messages: currentMessages + } + + const response = await anthropic.messages.create(currentRequest) + + // Log the request ID at debug level + console.debug(`${logPrefix} requestId: ${response.id}`) + + // FIXED: Process response content properly + let hasToolUse = false + let textContent = '' + + for (const content of response.content) { + if (content.type === 'text') { + textContent += content.text + } else if (content.type === 'server_tool_use' && shouldUseTools) { + // FIXED: Handle server-side tool use (web search is executed automatically) + hasToolUse = true + toolCallCount++ + console.log(`${search} ${logPrefix}: Web search executed - ${content.name}`) + } else if (content.type === 'web_search_tool_result') { + // FIXED: Handle web search results (automatically provided by API) + console.log(`${search} ${logPrefix}: Received web search results`) + } + } -// Function to get step description with tool awareness + // FIXED: Add assistant's response to conversation history + currentMessages.push({ + role: 'assistant', + content: response.content + }) + + // FIXED: Accumulate text content + finalText += textContent + + // FIXED: Break if no tool use or if we've hit limits + if (!hasToolUse || toolCallCount >= maxCalls) { + break + } + + // FIXED: If there was tool use, Claude might continue in the same turn + // Check if response has pause_turn stop reason + if (response.stop_reason === 'pause_turn') { + // Continue the conversation to let Claude finish its turn + continue + } else { + // Tool use complete, break the loop + break + } + } + + // FIXED: Ensure we have text content + if (!finalText) { + throw new Error('No text content received from Claude') + } + + const elapsed = (Date.now() - startTime) / 1000 // seconds + + // ENHANCED: Log tool usage statistics + if (shouldUseTools && toolCallCount > 0) { + console.log(`${search} ${logPrefix}: Used ${toolCallCount} web searches`) + } + + // Log the full response text at debug level + console.debug(`${logPrefix} full response:\n---\n${finalText}\n---`) + return { text: finalText, durationSec: elapsed } + } catch (error) { + // ENHANCED: Better error handling for tool-related failures + if (toolsEnabled && (error.message?.includes('tool') || error.message?.includes('search'))) { + console.warn( + `${search} ${logPrefix}: Tool error, falling back to text-only mode:`, + error.message + ) + // Retry without tools on tool-related errors + return callClaude(stepName, promptNames, userContent, false) + } + throw error // Re-throw non-tool errors + } finally { + console.timeEnd(`${logPrefix}`) + } +} +// NEW: Function to get step description with tool awareness function getStepDescription(stepName: StepName): string { const stepConfig = stepConfigs[stepName] const toolsWillBeUsed = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE @@ -144,10 +508,11 @@ function getStepDescription(stepName: StepName): string { } // Function to generate a progress string from the state + function generateProgressString(state: WriteState): string { const pencil = '✏️' const checkmark = 'βœ“' - const search = 'πŸ”' + const search = 'πŸ”' // NEW: Icon for tool-enabled steps // Get workflow description const workflowDescription = workflowConfigs[state.workflowType].description @@ -155,7 +520,7 @@ function generateProgressString(state: WriteState): string { // Generate the progress string let lis = [] - // Completed steps with tool usage indicators + // ENHANCED: Completed steps with tool usage indicators for (const step of state.completedSteps) { const stepConfig = stepConfigs[step.name] const usedTools = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE @@ -166,7 +531,7 @@ function generateProgressString(state: WriteState): string { ) } - // Current step with tool usage indicator + // ENHANCED: Current step with tool usage indicator if ( state.currentStep && state.step !== 'complete' && @@ -179,7 +544,7 @@ function generateProgressString(state: WriteState): string { lis.push(`
  • ${getStepDescription(state.currentStep)} ${icon}
  • `) } - // Remaining steps with tool usage preview + // ENHANCED: Remaining steps with tool usage preview const completedAndCurrentSteps = [...state.completedSteps.map((s) => s.name), state.currentStep] const filteredRemainingSteps = state.remainingSteps.filter( (step) => !completedAndCurrentSteps.includes(step) @@ -244,9 +609,171 @@ function prepareResponse(state: WriteState): ChatResponse { } } -// API endpoint handlers -export async function GET() { - return json({ apiAvailable: IS_API_AVAILABLE }) +// Define step handlers in a map for easy lookup +const stepHandlers: Record< + StepName, + (state: WriteState) => Promise<{ text: string; durationSec: number }> +> = { + // ENHANCED: Enable tools for target finding + findTarget: async (state) => { + System_Prompts['Information'] = state.userInput + + // NEW: Check if tools should be enabled for this step + const stepConfig = stepConfigs.findTarget + const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH + + const result = await callClaude( + 'findTarget', + ['Basic', 'Target', 'Information'], + 'Hello! Please help me find a person to contact!', + toolsEnabled // NEW: Pass tool enablement flag + ) + + state.information = result.text + System_Prompts['Information'] = result.text + + return result + }, + + // ENHANCED: Enable tools for web search (this step is inherently search-based) + webSearch: async (state) => { + System_Prompts['Information'] = state.userInput + + // NEW: Check if tools should be enabled for this step + const stepConfig = stepConfigs.webSearch + const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH + + const result = await callClaude( + 'webSearch', + ['Basic', 'webSearch', 'Information', 'Results'], + 'Hello! Please research this person!', + toolsEnabled // NEW: Pass tool enablement flag + ) + + state.information = result.text + System_Prompts['Information'] = System_Prompts['Information'] + '\n\n' + result.text + + return result + }, + + // ENHANCED: Enable tools for research step + research: async (state) => { + System_Prompts['Information'] = state.userInput + + // NEW: Check if tools should be enabled for this step + const stepConfig = stepConfigs.research + const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH + + const result = await callClaude( + 'research', + ['Basic', 'Mail', 'Information', 'Research'], + "Hello! Please update the list of information by replacing all instances of 'undefined' with something that belongs under their respective header based on the rest of the information provided. Thank you!", + toolsEnabled // NEW: Pass tool enablement flag + ) + + state.information = result.text + System_Prompts['Information'] = result.text + + return result + }, + + // UNCHANGED: Text processing steps remain without tools for performance + firstDraft: async (state) => { + return await callClaude( + 'firstDraft', + ['Basic', 'Mail', 'First_Draft', 'Results'], + 'Hello! Please write an email draft using the following information. \n' + state.userInput + // NOTE: No toolsEnabled parameter = defaults to false + ) + }, + + firstCut: async (state) => { + return await callClaude( + 'firstCut', + ['Basic', 'Mail', 'Information', 'First_Cut', 'Results'], + 'Hello! Please cut the following email draft. \n \n' + state.email + // NOTE: No toolsEnabled parameter = defaults to false + ) + }, + + firstEdit: async (state) => { + return await callClaude( + 'firstEdit', + ['Basic', 'Mail', 'Information', 'First_Edit', 'Results'], + 'Hello! Please edit the following email draft. \n \n' + state.email + // NOTE: No toolsEnabled parameter = defaults to false + ) + }, + + toneEdit: async (state) => { + return await callClaude( + 'toneEdit', + ['Basic', 'Mail', 'Information', 'Tone_Edit', 'Results'], + 'Hello! Please edit the tone of the following email draft. \n \n' + state.email + // NOTE: No toolsEnabled parameter = defaults to false + ) + }, + + finalEdit: async (state) => { + return await callClaude( + 'finalEdit', + ['Basic', 'Mail', 'Information', 'Final_Edit', 'Checklist', 'Results'], + 'Hello! Please edit the following email draft. \n \n' + state.email + // NOTE: No toolsEnabled parameter = defaults to false + ) + } +} +// Process a specific step +async function processStep(state: WriteState): Promise { + if (!state.nextStep) { + return { + ...state, + step: 'complete', + currentStep: null + } + } + + const currentStep = state.nextStep as StepName + state.currentStep = currentStep + + // Update remaining steps - remove current step from remaining + state.remainingSteps = state.remainingSteps.filter((step) => step !== currentStep) + + // Execute the step using the step handler from the map + const stepHandler = stepHandlers[currentStep] + if (!stepHandler) { + throw new Error(`Unknown step: ${currentStep}`) + } + + const result = await stepHandler(state) + + // Update email content (except for research-like steps which update information) + if (/*!['research', 'findTarget', 'webSearch']*/ !['research'].includes(currentStep)) { + state.email = result.text + } + + // Update state with results - current step is now completed + state.step = currentStep + state.completedSteps.push({ + name: currentStep, + durationSec: result.durationSec + }) + + // Set next step based on workflow configuration + const workflowSteps = workflowConfigs[state.workflowType].steps + const currentIndex = workflowSteps.indexOf(currentStep) + + if (currentIndex !== -1 && currentIndex < workflowSteps.length - 1) { + state.nextStep = workflowSteps[currentIndex + 1] + state.currentStep = workflowSteps[currentIndex + 1] + } else { + // Last step completed, mark as complete + state.nextStep = null + state.currentStep = null + state.step = 'complete' + } + + return state } export async function POST({ fetch, request }) { @@ -265,6 +792,7 @@ export async function POST({ fetch, request }) { // Check if this is a continuation of an existing process let state: WriteState + let stateToken = null if (requestData.stateToken) { // Continue an existing process From 3c1e719945e154f7b9089be6b520a5e7f5de958e Mon Sep 17 00:00:00 2001 From: andrei Date: Mon, 23 Jun 2025 14:36:23 +0200 Subject: [PATCH 09/16] Split write/+server.ts in twain. --- src/routes/api/write/+server.ts | 668 ++---------------- .../api/write/process-step-background.ts | 513 ++++++++++++++ 2 files changed, 583 insertions(+), 598 deletions(-) create mode 100644 src/routes/api/write/process-step-background.ts diff --git a/src/routes/api/write/+server.ts b/src/routes/api/write/+server.ts index cc66f4f59..984932479 100644 --- a/src/routes/api/write/+server.ts +++ b/src/routes/api/write/+server.ts @@ -1,26 +1,9 @@ import { error, json } from '@sveltejs/kit' import { env } from '$env/dynamic/private' -import Anthropic from '@anthropic-ai/sdk' +import { processStep, stepConfigs } from './process-claude-background' -// Safely access the API key, will be undefined if not set -const ANTHROPIC_API_KEY_FOR_WRITE = env.ANTHROPIC_API_KEY_FOR_WRITE || undefined -// NEW: Add global toggle for web search functionality -const ENABLE_WEB_SEARCH = env.ENABLE_WEB_SEARCH !== 'false' // Default to true unless explicitly disabled -// NEW: Add configurable rate limiting for tool calls per step -const MAX_TOOL_CALLS_PER_STEP = parseInt(env.MAX_TOOL_CALLS_PER_STEP || '3') - -// Flag to track if API is available -const IS_API_AVAILABLE = !!ANTHROPIC_API_KEY_FOR_WRITE - -// Log warning during build if API key is missing -if (!IS_API_AVAILABLE) { - console.warn( - '⚠️ ANTHROPIC_API_KEY_FOR_WRITE is not set. The /write page will operate in limited mode.' - ) -} - -// Define step types for server-side use -type StepName = +// Type definitions +export type StepName = | 'findTarget' | 'webSearch' | 'research' @@ -30,276 +13,92 @@ type StepName = | 'toneEdit' | 'finalEdit' -// Define workflow types -type WorkflowType = '1' | '2' | '3' | '4' +export type WorkflowType = '1' | '2' | '3' | '4' -// NEW: Define step configuration interface for tool usage -interface StepConfig { - toolsEnabled?: boolean // Whether this step can use tools - maxToolCalls?: number // Maximum tool calls for this step (overrides global) - description?: string // Enhanced description when tools are used +export interface StepConfig { + toolsEnabled?: boolean + maxToolCalls?: number + description?: string } -// ENHANCED: Extend workflow configuration to support step configs -type WorkflowConfig = { +export type WorkflowConfig = { steps: StepName[] description: string - stepConfigs?: Record // NEW: Optional step-level configuration + stepConfigs?: Record } -// NEW: Define step-level tool configurations -const stepConfigs: Record = { - // Research-focused steps that benefit from web search - findTarget: { - toolsEnabled: true, - maxToolCalls: 5, - description: 'Find possible targets (using web search)' - }, - webSearch: { - toolsEnabled: true, - maxToolCalls: 3, - description: 'Research the target (using web search)' - }, - research: { - toolsEnabled: false, - maxToolCalls: 2, - description: 'Auto-fill missing user inputs' - }, - // Text processing steps remain tool-free for performance - firstDraft: { toolsEnabled: false }, - firstCut: { toolsEnabled: false }, - firstEdit: { toolsEnabled: false }, - toneEdit: { toolsEnabled: false }, - finalEdit: { toolsEnabled: false } +export interface WriteState { + step: StepName | 'complete' | 'start' + workflowType: WorkflowType + userInput: string + email: string + information?: string + completedSteps: Array<{ + name: StepName + durationSec: number + }> + currentStep: StepName | null + remainingSteps: StepName[] + nextStep: StepName | null } -const workflowConfigs: Record = { +export type ChatResponse = { + response: string + apiAvailable?: boolean + stateToken?: string + progressString?: string + complete?: boolean + information?: string +} + +export type Message = { + role: 'user' | 'assistant' | 'system' + content: string +} + +export interface JobStorage { + jobId: string + status: 'pending' | 'processing' | 'completed' | 'failed' + writeState: WriteState // Your existing state object + error?: string + createdAt: Date + completedAt?: Date +} + +// Environment configuration +const ANTHROPIC_API_KEY_FOR_WRITE = env.ANTHROPIC_API_KEY_FOR_WRITE || undefined +const ENABLE_WEB_SEARCH = env.ENABLE_WEB_SEARCH !== 'false' +const IS_API_AVAILABLE = !!ANTHROPIC_API_KEY_FOR_WRITE + +// Workflow configurations +export const workflowConfigs: Record = { '1': { steps: ['findTarget'], description: 'Find Target Only', - stepConfigs // NEW: Include step configurations + stepConfigs }, '2': { steps: ['webSearch', 'research'], description: 'Web Search + Autofill', - stepConfigs // NEW: Include step configurations + stepConfigs }, '3': { steps: ['research'], description: 'Autofill only', - stepConfigs // NEW: Include step configurations + stepConfigs }, '4': { steps: ['firstDraft', 'firstCut', 'firstEdit', 'toneEdit', 'finalEdit'], description: 'Full Email Generation', - stepConfigs // NEW: Include step configurations + stepConfigs } } -// Server-side state management interface (not exposed to client) -interface WriteState { - step: StepName | 'complete' | 'start' // Current/completed step - workflowType: WorkflowType // Type of workflow being executed - userInput: string // Original input from form (cleaned, without prefix) - email: string // Current email content - information?: string // Processed information after research - completedSteps: Array<{ - name: StepName - durationSec: number - }> - currentStep: StepName | null // Step being processed - remainingSteps: StepName[] // Steps still to do - nextStep: StepName | null // Next step to run (null if complete) -} -// Client-facing response type -export type ChatResponse = { - response: string // Email content to display - apiAvailable?: boolean // Is API available - stateToken?: string // Opaque state token to pass to next request - progressString?: string // Human-readable progress string - complete?: boolean // Is the process complete - information?: string // Processed information for form fields -} - -export type Message = { - role: 'user' | 'assistant' | 'system' - content: string -} - -const System_Prompts: { [id: string]: string } = {} -System_Prompts['Basic'] = `You are a helpful AI assistant. - -Note: Some fields in the information may begin with a robot emoji (πŸ€–). This indicates the field was automatically generated during research. You can use this information normally, just ignore the emoji marker.` - -System_Prompts['Mail'] = ` -What follows is the anatomy of a good email, a set of guidelines and -criteria for writing a good mail. Each paragraph, except the last represents -a distinct part of the email. - -Subject line: -The subject line should be short, informative and clearly communicate -the goal of the mail. It must grab the attention and capture the -interest of the recipient. Avoid clichΓ© language. - -Greeting: -The greeting must match the tone of the mail. If possible, address the -recipient by the appropriate title. Keep it short, and mention the reason -for the mail. Establish a strong connection with the recipient: Are they -a politician meant to represent you? Is it regarding something they've -recently done? Make the recipient feel like they owe you an answer. - -First paragraph: -Explain what the purpose of the email is. It must be concise and captivating, -most people who receive many emails learn to quickly dismiss many. Make -sure the relation is established and they have a reason to read on. - -Body paragraph: -The main body of the email should be informative and contain the information -of the mail. Take great care not to overwhelm the reader: it must be -logically structured and not too full of facts. The message should remain -clear and the relation to the greeting and first paragraph must remain clear. -It should not be too long, otherwise it might get skimmed. Links to further -information can be provided. - -Conclusion: -Keep this short and sweet. Make sure it has a CLEAR CALL TO ACTION! -Restate the reason the recipient should feel the need to act. Thank them -for their time and/or your ask. - -General: -Make sure the formatting isn't too boring. Write in a manner the recipient -would respond well to: Do not argue with them, do not mention views they -probably won't share. Try to play to things they said before and that fit -their persona. Keep the tone consistent and not too emotional. Do not sound -crazy. -` -System_Prompts['Checklist'] = ` -Checklist Before Sending -Message Verification - Is the purpose crystal clear? - Have I provided necessary context? - Is there a specific, achievable call to action? - Have I proofread for tone and clarity? -` - -System_Prompts['First_Draft'] = ` -Using the information that will be provided by the user, write the mail -according to the criteria. Get all the information into the mail. -Don't worry about it being too long. Keep the message powerful. -` - -System_Prompts['First_Cut'] = ` -You will be provided with an email by the user. -Remove redundant information and clean up the structure. The point of this pass is -to have the structure clear and the mail slightly longer than needed. The message -should be clear, the information still mostly present, with only what is -absolutely necessary being removed. -` - -System_Prompts['First_Edit'] = ` -You will be provided with an email by the user. The following points are paramount: -Make sure the flow of information is natural. All paragraphs should be -connected in a sensical manner. Remove odd, unfitting or overly emotional -language. Make sure the paragraphs fulfill their roles. -` - -System_Prompts['Tone_Edit'] = ` -You will be provided with an email by the user. The following points are paramount: -Adjust the language to match recipient's communication style. Remove potentially -offensive or dismissive language. Ensure the tone matches the relationship and -purpose. Make sure the points and information is relevant for the recipient. -Assume the recipient's position: How would they react to the mail? What information -would resonate with them? What wouldn't? Do not compromise on the message. -` - -System_Prompts['Final_Edit'] = ` -You will be provided with an email by the user. Make sure the email matches the -criteria initially described. Check spelling, grammar and tone. -` - -System_Prompts['Making_Template'] = ` -Making a template out of an email requires a good email as a base, then punching massive -holes into the email to allow for the fitting of new information, specifically in tone -and style as well as personal connection. The information should be kept, as well as the -structural flow of the email and especially between the paragraphs. Provide clearly -denoted comments on what was removed and by what it should be replaced. - -The user will provide an email for you to turn into a template using the method described before. -` - -System_Prompts['Improving_Template'] = ` -Assume the role of someone filling in the email template. How much do you have to -rewrite text to make you contributions fit? Can you keep the email brief? Are you restricted -by any word choices and sentence structures? Can you instert your own personality into the -template without too much effort? With these considerations, improve the template. - -The user will provide an email template for you to improve using the method described before. -` - -System_Prompts['Explain'] = ` -When making choices, provide a clearly labeled rationale for why you chose as you did -and what informed those decisions. -` - -System_Prompts['Results'] = ` -Only reply with the final results, a.k.a. the final email, and absolutely nothing else. -` - -System_Prompts['Research'] = ` -Please replace all mentions of 'undefined' with the apropriate information that should -go in that space, derived from the rest of the information. - -Important: For any field you fill in that was originally 'undefined' or empty, prefix -your answer with a robot emoji (πŸ€–) to indicate it was automatically generated. - -Example: -Original: "Preferred communication style: undefined" -Your output: "Preferred communication style: πŸ€– Formal but approachable" - -Please remember that you are addressing this person, and try to make all inferences based on the information provided and your own knowledge. Err on the side of caution: if you are unsure, be polite and neutral. - -Output the full information, including your edits. Output nothing else. -` - -System_Prompts['Target'] = ` -Please use your internet search capability to find individuals involved with AI safety who match the following description. - -For each person you find (aim for 3-5 people), please provide: -1. Name and current position -2. Why they're relevant to AI safety -3. Their organization -4. Brief note on their public stance on AI safety - -Please cite your sources for each person. -` - -//Preface with '[Person's Name] = John Doe' etc. -System_Prompts['webSearch'] = ` -Please use your internet search capability to research [Person's Name] who is [current role] at [organization/affiliation]. I plan to contact them about AI safety concerns. - -Search for and provide: -1. Professional background (education, career history, notable positions) -2. Their involvement with AI issues (policy positions, public statements, initiatives, articles, interviews) -3. Their public views on AI development and safety (with direct quotes where possible) -4. Recent activities related to technology policy or AI (last 6-12 months) -5. Communication style and key terms they use when discussing technology issues -6. Notable connections (organizations, committees, coalitions, or influential individuals they work with) -7. Contact information (professional email or official channels if publicly available) - -Please cite all sources you use and only include information you can verify through your internet search. If you encounter conflicting information, note this and provide the most reliable source. - -BE BRIEF! This is extremely important. Try to output only a few lines of text for each questions. -` - -// Only initialize the client if we have an API key -const anthropic = IS_API_AVAILABLE - ? new Anthropic({ - apiKey: ANTHROPIC_API_KEY_FOR_WRITE - }) - : null - -export async function GET() { - return json({ apiAvailable: IS_API_AVAILABLE }) +// Log warning during build if API key is missing +if (!IS_API_AVAILABLE) { + console.warn( + '⚠️ ANTHROPIC_API_KEY_FOR_WRITE is not set. The /write page will operate in limited mode.' + ) } // Helper function to parse workflow type from user input @@ -318,171 +117,8 @@ function parseWorkflowType(userInput: string): { // Default to workflow 4 if no prefix is found return { workflowType: '4', cleanedInput: userInput } } -// NEW: Interface for tool use response content -interface ToolUseContent { - type: 'tool_use' - id: string - name: string - input: any -} - -// NEW: Interface for tool result content -interface ToolResultContent { - type: 'tool_result' - tool_use_id: string - content: string -} - -// ENHANCED: Modified function signature to support optional tool usage -async function callClaude( - stepName: string, - promptNames: string[], - userContent: string, - toolsEnabled: boolean = false // NEW: Optional parameter for tool usage -): Promise<{ text: string; durationSec: number }> { - const pencil = '✏️' - const search = 'πŸ”' // NEW: Icon for tool usage - const logPrefix = `${pencil} write:${stepName}` - const startTime = Date.now() - - console.time(`${logPrefix}`) - - try { - // Check if the API client is available - if (!anthropic) { - throw new Error('Anthropic API client is not initialized. API key is missing.') - } - - // Combine all the specified prompts - const systemPrompt = promptNames.map((name) => System_Prompts[name]).join('') - - // NEW: Determine if tools should be included in this call - const shouldUseTools = toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE - - // NEW: Log tool usage status - if (shouldUseTools) { - console.log(`${search} ${logPrefix}: Tools enabled for this step`) - } - - // FIXED: Use correct web search tool definition matching API documentation - const tools = shouldUseTools - ? [ - { - type: 'web_search_20250305', // CHANGED: Use correct tool type from API docs - name: 'web_search', - max_uses: 3 // ADDED: Limit searches per request - } - ] - : undefined - - // ENHANCED: Create API request with conditional tool support - const requestParams: any = { - model: 'claude-3-7-sonnet-20250219', - max_tokens: 4096, - system: systemPrompt, - messages: [{ role: 'user', content: userContent }] - } - - // NEW: Add tools to request if enabled - if (tools) { - requestParams.tools = tools - } - - // FIXED: Implement proper tool execution loop - let currentMessages = [...requestParams.messages] - let finalText = '' - let toolCallCount = 0 - const maxCalls = Math.min( - MAX_TOOL_CALLS_PER_STEP, - stepConfigs[stepName as StepName]?.maxToolCalls || MAX_TOOL_CALLS_PER_STEP - ) - - while (toolCallCount < maxCalls) { - // Create request with current message history - const currentRequest = { - ...requestParams, - messages: currentMessages - } - - const response = await anthropic.messages.create(currentRequest) - - // Log the request ID at debug level - console.debug(`${logPrefix} requestId: ${response.id}`) - - // FIXED: Process response content properly - let hasToolUse = false - let textContent = '' - - for (const content of response.content) { - if (content.type === 'text') { - textContent += content.text - } else if (content.type === 'server_tool_use' && shouldUseTools) { - // FIXED: Handle server-side tool use (web search is executed automatically) - hasToolUse = true - toolCallCount++ - console.log(`${search} ${logPrefix}: Web search executed - ${content.name}`) - } else if (content.type === 'web_search_tool_result') { - // FIXED: Handle web search results (automatically provided by API) - console.log(`${search} ${logPrefix}: Received web search results`) - } - } - // FIXED: Add assistant's response to conversation history - currentMessages.push({ - role: 'assistant', - content: response.content - }) - - // FIXED: Accumulate text content - finalText += textContent - - // FIXED: Break if no tool use or if we've hit limits - if (!hasToolUse || toolCallCount >= maxCalls) { - break - } - - // FIXED: If there was tool use, Claude might continue in the same turn - // Check if response has pause_turn stop reason - if (response.stop_reason === 'pause_turn') { - // Continue the conversation to let Claude finish its turn - continue - } else { - // Tool use complete, break the loop - break - } - } - - // FIXED: Ensure we have text content - if (!finalText) { - throw new Error('No text content received from Claude') - } - - const elapsed = (Date.now() - startTime) / 1000 // seconds - - // ENHANCED: Log tool usage statistics - if (shouldUseTools && toolCallCount > 0) { - console.log(`${search} ${logPrefix}: Used ${toolCallCount} web searches`) - } - - // Log the full response text at debug level - console.debug(`${logPrefix} full response:\n---\n${finalText}\n---`) - return { text: finalText, durationSec: elapsed } - } catch (error) { - // ENHANCED: Better error handling for tool-related failures - if (toolsEnabled && (error.message?.includes('tool') || error.message?.includes('search'))) { - console.warn( - `${search} ${logPrefix}: Tool error, falling back to text-only mode:`, - error.message - ) - // Retry without tools on tool-related errors - return callClaude(stepName, promptNames, userContent, false) - } - throw error // Re-throw non-tool errors - } finally { - console.timeEnd(`${logPrefix}`) - } -} -// NEW: Function to get step description with tool awareness +// Function to get step description with tool awareness function getStepDescription(stepName: StepName): string { const stepConfig = stepConfigs[stepName] const toolsWillBeUsed = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE @@ -508,11 +144,10 @@ function getStepDescription(stepName: StepName): string { } // Function to generate a progress string from the state - function generateProgressString(state: WriteState): string { const pencil = '✏️' const checkmark = 'βœ“' - const search = 'πŸ”' // NEW: Icon for tool-enabled steps + const search = 'πŸ”' // Get workflow description const workflowDescription = workflowConfigs[state.workflowType].description @@ -520,7 +155,7 @@ function generateProgressString(state: WriteState): string { // Generate the progress string let lis = [] - // ENHANCED: Completed steps with tool usage indicators + // Completed steps with tool usage indicators for (const step of state.completedSteps) { const stepConfig = stepConfigs[step.name] const usedTools = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE @@ -531,7 +166,7 @@ function generateProgressString(state: WriteState): string { ) } - // ENHANCED: Current step with tool usage indicator + // Current step with tool usage indicator if ( state.currentStep && state.step !== 'complete' && @@ -544,7 +179,7 @@ function generateProgressString(state: WriteState): string { lis.push(`
  • ${getStepDescription(state.currentStep)} ${icon}
  • `) } - // ENHANCED: Remaining steps with tool usage preview + // Remaining steps with tool usage preview const completedAndCurrentSteps = [...state.completedSteps.map((s) => s.name), state.currentStep] const filteredRemainingSteps = state.remainingSteps.filter( (step) => !completedAndCurrentSteps.includes(step) @@ -609,171 +244,9 @@ function prepareResponse(state: WriteState): ChatResponse { } } -// Define step handlers in a map for easy lookup -const stepHandlers: Record< - StepName, - (state: WriteState) => Promise<{ text: string; durationSec: number }> -> = { - // ENHANCED: Enable tools for target finding - findTarget: async (state) => { - System_Prompts['Information'] = state.userInput - - // NEW: Check if tools should be enabled for this step - const stepConfig = stepConfigs.findTarget - const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH - - const result = await callClaude( - 'findTarget', - ['Basic', 'Target', 'Information'], - 'Hello! Please help me find a person to contact!', - toolsEnabled // NEW: Pass tool enablement flag - ) - - state.information = result.text - System_Prompts['Information'] = result.text - - return result - }, - - // ENHANCED: Enable tools for web search (this step is inherently search-based) - webSearch: async (state) => { - System_Prompts['Information'] = state.userInput - - // NEW: Check if tools should be enabled for this step - const stepConfig = stepConfigs.webSearch - const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH - - const result = await callClaude( - 'webSearch', - ['Basic', 'webSearch', 'Information', 'Results'], - 'Hello! Please research this person!', - toolsEnabled // NEW: Pass tool enablement flag - ) - - state.information = result.text - System_Prompts['Information'] = System_Prompts['Information'] + '\n\n' + result.text - - return result - }, - - // ENHANCED: Enable tools for research step - research: async (state) => { - System_Prompts['Information'] = state.userInput - - // NEW: Check if tools should be enabled for this step - const stepConfig = stepConfigs.research - const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH - - const result = await callClaude( - 'research', - ['Basic', 'Mail', 'Information', 'Research'], - "Hello! Please update the list of information by replacing all instances of 'undefined' with something that belongs under their respective header based on the rest of the information provided. Thank you!", - toolsEnabled // NEW: Pass tool enablement flag - ) - - state.information = result.text - System_Prompts['Information'] = result.text - - return result - }, - - // UNCHANGED: Text processing steps remain without tools for performance - firstDraft: async (state) => { - return await callClaude( - 'firstDraft', - ['Basic', 'Mail', 'First_Draft', 'Results'], - 'Hello! Please write an email draft using the following information. \n' + state.userInput - // NOTE: No toolsEnabled parameter = defaults to false - ) - }, - - firstCut: async (state) => { - return await callClaude( - 'firstCut', - ['Basic', 'Mail', 'Information', 'First_Cut', 'Results'], - 'Hello! Please cut the following email draft. \n \n' + state.email - // NOTE: No toolsEnabled parameter = defaults to false - ) - }, - - firstEdit: async (state) => { - return await callClaude( - 'firstEdit', - ['Basic', 'Mail', 'Information', 'First_Edit', 'Results'], - 'Hello! Please edit the following email draft. \n \n' + state.email - // NOTE: No toolsEnabled parameter = defaults to false - ) - }, - - toneEdit: async (state) => { - return await callClaude( - 'toneEdit', - ['Basic', 'Mail', 'Information', 'Tone_Edit', 'Results'], - 'Hello! Please edit the tone of the following email draft. \n \n' + state.email - // NOTE: No toolsEnabled parameter = defaults to false - ) - }, - - finalEdit: async (state) => { - return await callClaude( - 'finalEdit', - ['Basic', 'Mail', 'Information', 'Final_Edit', 'Checklist', 'Results'], - 'Hello! Please edit the following email draft. \n \n' + state.email - // NOTE: No toolsEnabled parameter = defaults to false - ) - } -} -// Process a specific step -async function processStep(state: WriteState): Promise { - if (!state.nextStep) { - return { - ...state, - step: 'complete', - currentStep: null - } - } - - const currentStep = state.nextStep as StepName - state.currentStep = currentStep - - // Update remaining steps - remove current step from remaining - state.remainingSteps = state.remainingSteps.filter((step) => step !== currentStep) - - // Execute the step using the step handler from the map - const stepHandler = stepHandlers[currentStep] - if (!stepHandler) { - throw new Error(`Unknown step: ${currentStep}`) - } - - const result = await stepHandler(state) - - // Update email content (except for research-like steps which update information) - if (/*!['research', 'findTarget', 'webSearch']*/ !['research'].includes(currentStep)) { - state.email = result.text - } - - // Update state with results - current step is now completed - state.step = currentStep - state.completedSteps.push({ - name: currentStep, - durationSec: result.durationSec - }) - - // Set next step based on workflow configuration - const workflowSteps = workflowConfigs[state.workflowType].steps - const currentIndex = workflowSteps.indexOf(currentStep) - - if (currentIndex !== -1 && currentIndex < workflowSteps.length - 1) { - state.nextStep = workflowSteps[currentIndex + 1] - state.currentStep = workflowSteps[currentIndex + 1] - } else { - // Last step completed, mark as complete - state.nextStep = null - state.currentStep = null - state.step = 'complete' - } - - return state +// API endpoint handlers +export async function GET() { + return json({ apiAvailable: IS_API_AVAILABLE }) } export async function POST({ fetch, request }) { @@ -792,7 +265,6 @@ export async function POST({ fetch, request }) { // Check if this is a continuation of an existing process let state: WriteState - let stateToken = null if (requestData.stateToken) { // Continue an existing process diff --git a/src/routes/api/write/process-step-background.ts b/src/routes/api/write/process-step-background.ts new file mode 100644 index 000000000..4d633dcf5 --- /dev/null +++ b/src/routes/api/write/process-step-background.ts @@ -0,0 +1,513 @@ +import { env } from '$env/dynamic/private' +import Anthropic from '@anthropic-ai/sdk' + +// Import types and configurations from server +import type { StepName, WriteState, StepConfig, JobStorage } from './+server' +import { workflowConfigs } from './+server' + +// Environment variables for step processing +const ANTHROPIC_API_KEY_FOR_WRITE = env.ANTHROPIC_API_KEY_FOR_WRITE || undefined +const ENABLE_WEB_SEARCH = env.ENABLE_WEB_SEARCH !== 'false' +const MAX_TOOL_CALLS_PER_STEP = parseInt(env.MAX_TOOL_CALLS_PER_STEP || '3') +const IS_API_AVAILABLE = !!ANTHROPIC_API_KEY_FOR_WRITE + +// Initialize Anthropic client +const anthropic = IS_API_AVAILABLE + ? new Anthropic({ + apiKey: ANTHROPIC_API_KEY_FOR_WRITE + }) + : null + +// System prompts for step processing +const System_Prompts: { [id: string]: string } = { + Basic: `You are a helpful AI assistant. + +Note: Some fields in the information may begin with a robot emoji (πŸ€–). This indicates the field was automatically generated during research. You can use this information normally, just ignore the emoji marker.`, + + Mail: ` +What follows is the anatomy of a good email, a set of guidelines and +criteria for writing a good mail. Each paragraph, except the last represents +a distinct part of the email. + +Subject line: +The subject line should be short, informative and clearly communicate +the goal of the mail. It must grab the attention and capture the +interest of the recipient. Avoid clichΓ© language. + +Greeting: +The greeting must match the tone of the mail. If possible, address the +recipient by the appropriate title. Keep it short, and mention the reason +for the mail. Establish a strong connection with the recipient: Are they +a politician meant to represent you? Is it regarding something they've +recently done? Make the recipient feel like they owe you an answer. + +First paragraph: +Explain what the purpose of the email is. It must be concise and captivating, +most people who receive many emails learn to quickly dismiss many. Make +sure the relation is established and they have a reason to read on. + +Body paragraph: +The main body of the email should be informative and contain the information +of the mail. Take great care not to overwhelm the reader: it must be +logically structured and not too full of facts. The message should remain +clear and the relation to the greeting and first paragraph must remain clear. +It should not be too long, otherwise it might get skimmed. Links to further +information can be provided. + +Conclusion: +Keep this short and sweet. Make sure it has a CLEAR CALL TO ACTION! +Restate the reason the recipient should feel the need to act. Thank them +for their time and/or your ask. + +General: +Make sure the formatting isn't too boring. Write in a manner the recipient +would respond well to: Do not argue with them, do not mention views they +probably won't share. Try to play to things they said before and that fit +their persona. Keep the tone consistent and not too emotional. Do not sound +crazy. +`, + + Checklist: ` +Checklist Before Sending +Message Verification + Is the purpose crystal clear? + Have I provided necessary context? + Is there a specific, achievable call to action? + Have I proofread for tone and clarity? +`, + + First_Draft: ` +Using the information that will be provided by the user, write the mail +according to the criteria. Get all the information into the mail. +Don't worry about it being too long. Keep the message powerful. +`, + + First_Cut: ` +You will be provided with an email by the user. +Remove redundant information and clean up the structure. The point of this pass is +to have the structure clear and the mail slightly longer than needed. The message +should be clear, the information still mostly present, with only what is +absolutely necessary being removed. +`, + + First_Edit: ` +You will be provided with an email by the user. The following points are paramount: +Make sure the flow of information is natural. All paragraphs should be +connected in a sensical manner. Remove odd, unfitting or overly emotional +language. Make sure the paragraphs fulfill their roles. +`, + + Tone_Edit: ` +You will be provided with an email by the user. The following points are paramount: +Adjust the language to match recipient's communication style. Remove potentially +offensive or dismissive language. Ensure the tone matches the relationship and +purpose. Make sure the points and information is relevant for the recipient. +Assume the recipient's position: How would they react to the mail? What information +would resonate with them? What wouldn't? Do not compromise on the message. +`, + + Final_Edit: ` +You will be provided with an email by the user. Make sure the email matches the +criteria initially described. Check spelling, grammar and tone. +`, + + Research: ` +Please replace all mentions of 'undefined' with the apropriate information that should +go in that space, derived from the rest of the information. + +Important: For any field you fill in that was originally 'undefined' or empty, prefix +your answer with a robot emoji (πŸ€–) to indicate it was automatically generated. + +Example: +Original: "Preferred communication style: undefined" +Your output: "Preferred communication style: πŸ€– Formal but approachable" + +Please remember that you are addressing this person, and try to make all inferences based on the information provided and your own knowledge. Err on the side of caution: if you are unsure, be polite and neutral. + +Output the full information, including your edits. Output nothing else. +`, + + Target: ` +Please use your internet search capability to find individuals involved with AI safety who match the following description. + +For each person you find (aim for 3-5 people), please provide: +1. Name and current position +2. Why they're relevant to AI safety +3. Their organization +4. Brief note on their public stance on AI safety + +Please cite your sources for each person. +`, + + webSearch: ` +Please use your internet search capability to research [Person's Name] who is [current role] at [organization/affiliation]. I plan to contact them about AI safety concerns. + +Search for and provide: +1. Professional background (education, career history, notable positions) +2. Their involvement with AI issues (policy positions, public statements, initiatives, articles, interviews) +3. Their public views on AI development and safety (with direct quotes where possible) +4. Recent activities related to technology policy or AI (last 6-12 months) +5. Communication style and key terms they use when discussing technology issues +6. Notable connections (organizations, committees, coalitions, or influential individuals they work with) +7. Contact information (professional email or official channels if publicly available) + +Please cite all sources you use and only include information you can verify through your internet search. If you encounter conflicting information, note this and provide the most reliable source. + +BE BRIEF! This is extremely important. Try to output only a few lines of text for each questions. +`, + + Results: ` +Only reply with the final results, a.k.a. the final email, and absolutely nothing else. +` +} + +// Step configurations +export const stepConfigs: Record = { + findTarget: { + toolsEnabled: true, + maxToolCalls: 5, + description: 'Find possible targets (using web search)' + }, + webSearch: { + toolsEnabled: true, + maxToolCalls: 3, + description: 'Research the target (using web search)' + }, + research: { + toolsEnabled: false, + maxToolCalls: 2, + description: 'Auto-fill missing user inputs' + }, + firstDraft: { toolsEnabled: false }, + firstCut: { toolsEnabled: false }, + firstEdit: { toolsEnabled: false }, + toneEdit: { toolsEnabled: false }, + finalEdit: { toolsEnabled: false } +} + +// Enhanced callClaude function with tool support +export async function callClaude( + stepName: string, + promptNames: string[], + userContent: string, + toolsEnabled: boolean = false +): Promise<{ text: string; durationSec: number }> { + const pencil = '✏️' + const search = 'πŸ”' + const logPrefix = `${pencil} write:${stepName}` + const startTime = Date.now() + + console.time(`${logPrefix}`) + + try { + // Check if the API client is available + if (!anthropic) { + throw new Error('Anthropic API client is not initialized. API key is missing.') + } + + // Combine all the specified prompts + const systemPrompt = promptNames.map((name) => System_Prompts[name]).join('') + + // Determine if tools should be included in this call + const shouldUseTools = toolsEnabled && ENABLE_WEB_SEARCH && IS_API_AVAILABLE + + // Log tool usage status + if (shouldUseTools) { + console.log(`${search} ${logPrefix}: Tools enabled for this step`) + } + + // Use correct web search tool definition matching API documentation + const tools = shouldUseTools + ? [ + { + type: 'web_search_20250305', + name: 'web_search', + max_uses: 3 + } + ] + : undefined + + // Create API request with conditional tool support + const requestParams: any = { + model: 'claude-3-7-sonnet-20250219', + max_tokens: 4096, + system: systemPrompt, + messages: [{ role: 'user', content: userContent }] + } + + // Add tools to request if enabled + if (tools) { + requestParams.tools = tools + } + + // Implement proper tool execution loop + let currentMessages = [...requestParams.messages] + let finalText = '' + let toolCallCount = 0 + const maxCalls = Math.min( + MAX_TOOL_CALLS_PER_STEP, + stepConfigs[stepName as StepName]?.maxToolCalls || MAX_TOOL_CALLS_PER_STEP + ) + + while (toolCallCount < maxCalls) { + // Create request with current message history + const currentRequest = { + ...requestParams, + messages: currentMessages + } + + const response = await anthropic.messages.create(currentRequest) + + // Log the request ID at debug level + console.debug(`${logPrefix} requestId: ${response.id}`) + + // Process response content properly + let hasToolUse = false + let textContent = '' + + for (const content of response.content) { + if (content.type === 'text') { + textContent += content.text + } else if (content.type === 'server_tool_use' && shouldUseTools) { + // Handle server-side tool use (web search is executed automatically) + hasToolUse = true + toolCallCount++ + console.log(`${search} ${logPrefix}: Web search executed - ${content.name}`) + } else if (content.type === 'web_search_tool_result') { + // Handle web search results (automatically provided by API) + console.log(`${search} ${logPrefix}: Received web search results`) + } + } + + // Add assistant's response to conversation history + currentMessages.push({ + role: 'assistant', + content: response.content + }) + + // Accumulate text content + finalText += textContent + + // Break if no tool use or if we've hit limits + if (!hasToolUse || toolCallCount >= maxCalls) { + break + } + + // If there was tool use, Claude might continue in the same turn + // Check if response has pause_turn stop reason + if (response.stop_reason === 'pause_turn') { + // Continue the conversation to let Claude finish its turn + continue + } else { + // Tool use complete, break the loop + break + } + } + + // Ensure we have text content + if (!finalText) { + throw new Error('No text content received from Claude') + } + + const elapsed = (Date.now() - startTime) / 1000 // seconds + + // Log tool usage statistics + if (shouldUseTools && toolCallCount > 0) { + console.log(`${search} ${logPrefix}: Used ${toolCallCount} web searches`) + } + + // Log the full response text at debug level + console.debug(`${logPrefix} full response:\n---\n${finalText}\n---`) + return { text: finalText, durationSec: elapsed } + } catch (error) { + // Better error handling for tool-related failures + if (toolsEnabled && (error.message?.includes('tool') || error.message?.includes('search'))) { + console.warn( + `${search} ${logPrefix}: Tool error, falling back to text-only mode:`, + error.message + ) + // Retry without tools on tool-related errors + return callClaude(stepName, promptNames, userContent, false) + } + throw error // Re-throw non-tool errors + } finally { + console.timeEnd(`${logPrefix}`) + } +} + +// Define step handlers in a map for easy lookup +const stepHandlers: Record< + StepName, + (state: WriteState) => Promise<{ text: string; durationSec: number }> +> = { + // Enable tools for target finding + findTarget: async (state) => { + System_Prompts['Information'] = state.userInput + + // Check if tools should be enabled for this step + const stepConfig = stepConfigs.findTarget + const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH + + const result = await callClaude( + 'findTarget', + ['Basic', 'Target', 'Information'], + 'Hello! Please help me find a person to contact!', + toolsEnabled + ) + + state.information = result.text + System_Prompts['Information'] = result.text + + return result + }, + + // Enable tools for web search + webSearch: async (state) => { + System_Prompts['Information'] = state.userInput + + // Check if tools should be enabled for this step + const stepConfig = stepConfigs.webSearch + const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH + + const result = await callClaude( + 'webSearch', + ['Basic', 'webSearch', 'Information', 'Results'], + 'Hello! Please research this person!', + toolsEnabled + ) + + state.information = result.text + System_Prompts['Information'] = System_Prompts['Information'] + '\n\n' + result.text + + return result + }, + + // Enable tools for research step + research: async (state) => { + System_Prompts['Information'] = state.userInput + + // Check if tools should be enabled for this step + const stepConfig = stepConfigs.research + const toolsEnabled = stepConfig?.toolsEnabled && ENABLE_WEB_SEARCH + + const result = await callClaude( + 'research', + ['Basic', 'Mail', 'Information', 'Research'], + "Hello! Please update the list of information by replacing all instances of 'undefined' with something that belongs under their respective header based on the rest of the information provided. Thank you!", + toolsEnabled + ) + + state.information = result.text + System_Prompts['Information'] = result.text + + return result + }, + + // Text processing steps remain without tools for performance + firstDraft: async (state) => { + return await callClaude( + 'firstDraft', + ['Basic', 'Mail', 'First_Draft', 'Results'], + 'Hello! Please write an email draft using the following information. \n' + state.userInput + ) + }, + + firstCut: async (state) => { + return await callClaude( + 'firstCut', + ['Basic', 'Mail', 'Information', 'First_Cut', 'Results'], + 'Hello! Please cut the following email draft. \n \n' + state.email + ) + }, + + firstEdit: async (state) => { + return await callClaude( + 'firstEdit', + ['Basic', 'Mail', 'Information', 'First_Edit', 'Results'], + 'Hello! Please edit the following email draft. \n \n' + state.email + ) + }, + + toneEdit: async (state) => { + return await callClaude( + 'toneEdit', + ['Basic', 'Mail', 'Information', 'Tone_Edit', 'Results'], + 'Hello! Please edit the tone of the following email draft. \n \n' + state.email + ) + }, + + finalEdit: async (state) => { + return await callClaude( + 'finalEdit', + ['Basic', 'Mail', 'Information', 'Final_Edit', 'Checklist', 'Results'], + 'Hello! Please edit the following email draft. \n \n' + state.email + ) + } +} + +// Process a specific step +export async function processStep(state: WriteState): Promise { + if (!state.nextStep) { + return { + ...state, + step: 'complete', + currentStep: null + } + } + + const currentStep = state.nextStep as StepName + state.currentStep = currentStep + + // Update remaining steps - remove current step from remaining + state.remainingSteps = state.remainingSteps.filter((step) => step !== currentStep) + + // Execute the step using the step handler from the map + const stepHandler = stepHandlers[currentStep] + if (!stepHandler) { + throw new Error(`Unknown step: ${currentStep}`) + } + + const result = await stepHandler(state) + + // Update email content (except for research-like steps which update information) + if (!['research'].includes(currentStep)) { + state.email = result.text + } + + // Update state with results - current step is now completed + state.step = currentStep + state.completedSteps.push({ + name: currentStep, + durationSec: result.durationSec + }) + + // Set next step based on workflow configuration + // Note: This requires access to workflowConfigs which should be imported from the server file + const workflowSteps = getWorkflowSteps(state.workflowType) + const currentIndex = workflowSteps.indexOf(currentStep) + + if (currentIndex !== -1 && currentIndex < workflowSteps.length - 1) { + state.nextStep = workflowSteps[currentIndex + 1] + state.currentStep = workflowSteps[currentIndex + 1] + } else { + // Last step completed, mark as complete + state.nextStep = null + state.currentStep = null + state.step = 'complete' + } + + return state +} + +// Helper function to get workflow steps (this will need to be imported from server) +function getWorkflowSteps(workflowType: string): StepName[] { + // This should be imported from the server file or passed as a parameter + // For now, returning a placeholder + const workflowConfigs = { + '1': ['findTarget'], + '2': ['webSearch', 'research'], + '3': ['research'], + '4': ['firstDraft', 'firstCut', 'firstEdit', 'toneEdit', 'finalEdit'] + } + return workflowConfigs[workflowType] || [] +} From ccf05921767fe895d2aede816e2b91df66e86916 Mon Sep 17 00:00:00 2001 From: andrei Date: Mon, 23 Jun 2025 15:41:01 +0200 Subject: [PATCH 10/16] Added src/lib/storage.ts to better handle the write storage api. src/routes/api/write/+server.ts, process-step-background.ts and status.ts swapped over to blob storage. src/routes/write/+page.svelte now uses blob storage. INCOMPLETE VERSION --- src/lib/storage.ts | 96 ++++ src/routes/api/write/+server.ts | 123 ++++- .../api/write/process-step-background.ts | 218 ++++++-- src/routes/api/write/status.ts | 62 +++ src/routes/write/+page.svelte | 487 ++++++++++-------- 5 files changed, 714 insertions(+), 272 deletions(-) create mode 100644 src/lib/storage.ts create mode 100644 src/routes/api/write/status.ts diff --git a/src/lib/storage.ts b/src/lib/storage.ts new file mode 100644 index 000000000..a1f160ba0 --- /dev/null +++ b/src/lib/storage.ts @@ -0,0 +1,96 @@ +import { getStore } from '@netlify/blobs' + +// Initialize the blob store +const jobStore = getStore('email-jobs') + +// Storage interface functions +export const JobStorage = { + // Create new job entry + async createJob(jobId, initialWriteState) { + const jobData = { + jobId, + status: 'pending', + writeState: initialWriteState, + createdAt: new Date().toISOString(), + error: null, + completedAt: null + } + + await jobStore.set(jobId, JSON.stringify(jobData)) + return jobData + }, + + // Get job by ID + async getJob(jobId) { + try { + const data = await jobStore.get(jobId) + if (!data) return null + return JSON.parse(data) + } catch (error) { + console.error('Error retrieving job:', error) + return null + } + }, + + // Update job status + async updateJobStatus(jobId, status, error = null) { + const jobData = await this.getJob(jobId) + if (!jobData) throw new Error(`Job ${jobId} not found`) + + jobData.status = status + jobData.error = error + + if (status === 'completed' || status === 'failed') { + jobData.completedAt = new Date().toISOString() + } + + await jobStore.set(jobId, JSON.stringify(jobData)) + return jobData + }, + + // Update WriteState for job + async updateWriteState(jobId, newWriteState) { + const jobData = await this.getJob(jobId) + if (!jobData) throw new Error(`Job ${jobId} not found`) + + jobData.writeState = newWriteState + jobData.status = 'processing' + + await jobStore.set(jobId, JSON.stringify(jobData)) + return jobData + }, + + // Complete job with final results + async completeJob(jobId, finalWriteState) { + const jobData = await this.getJob(jobId) + if (!jobData) throw new Error(`Job ${jobId} not found`) + + jobData.writeState = finalWriteState + jobData.status = 'completed' + jobData.completedAt = new Date().toISOString() + + await jobStore.set(jobId, JSON.stringify(jobData)) + return jobData + }, + + // Mark job as failed + async failJob(jobId, errorMessage) { + return await this.updateJobStatus(jobId, 'failed', errorMessage) + }, + + // Delete job (cleanup) + async deleteJob(jobId) { + await jobStore.delete(jobId) + }, + + // List all jobs (for debugging/admin) + async listJobs() { + const { blobs } = await jobStore.list() + return blobs.map((blob) => blob.key) + } +} + +// Utility function to generate unique job IDs +export function generateJobId() { + return `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` +} diff --git a/src/routes/api/write/+server.ts b/src/routes/api/write/+server.ts index 984932479..b342571ad 100644 --- a/src/routes/api/write/+server.ts +++ b/src/routes/api/write/+server.ts @@ -1,6 +1,6 @@ import { error, json } from '@sveltejs/kit' import { env } from '$env/dynamic/private' -import { processStep, stepConfigs } from './process-claude-background' +import { JobStorage, generateJobId } from '$lib/storage' // Type definitions export type StepName = @@ -49,6 +49,8 @@ export type ChatResponse = { progressString?: string complete?: boolean information?: string + jobId?: string + status?: string } export type Message = { @@ -59,7 +61,7 @@ export type Message = { export interface JobStorage { jobId: string status: 'pending' | 'processing' | 'completed' | 'failed' - writeState: WriteState // Your existing state object + writeState: WriteState error?: string createdAt: Date completedAt?: Date @@ -69,6 +71,32 @@ export interface JobStorage { const ANTHROPIC_API_KEY_FOR_WRITE = env.ANTHROPIC_API_KEY_FOR_WRITE || undefined const ENABLE_WEB_SEARCH = env.ENABLE_WEB_SEARCH !== 'false' const IS_API_AVAILABLE = !!ANTHROPIC_API_KEY_FOR_WRITE +const NETLIFY_BACKGROUND_FUNCTION_URL = + env.NETLIFY_BACKGROUND_FUNCTION_URL || '/.src/routes/api/write/process-step-background' + +// Step configurations (imported from background file) +export const stepConfigs: Record = { + findTarget: { + toolsEnabled: true, + maxToolCalls: 5, + description: 'Find possible targets (using web search)' + }, + webSearch: { + toolsEnabled: true, + maxToolCalls: 3, + description: 'Research the target (using web search)' + }, + research: { + toolsEnabled: false, + maxToolCalls: 2, + description: 'Auto-fill missing user inputs' + }, + firstDraft: { toolsEnabled: false }, + firstCut: { toolsEnabled: false }, + firstEdit: { toolsEnabled: false }, + toneEdit: { toolsEnabled: false }, + finalEdit: { toolsEnabled: false } +} // Workflow configurations export const workflowConfigs: Record = { @@ -144,7 +172,7 @@ function getStepDescription(stepName: StepName): string { } // Function to generate a progress string from the state -function generateProgressString(state: WriteState): string { +export function generateProgressString(state: WriteState): string { const pencil = '✏️' const checkmark = 'βœ“' const search = 'πŸ”' @@ -228,7 +256,7 @@ function initializeState(userInput: string): WriteState { } // Helper function to prepare consistent responses -function prepareResponse(state: WriteState): ChatResponse { +export function prepareResponse(state: WriteState): ChatResponse { // Generate progress string const progressString = generateProgressString(state) @@ -244,6 +272,24 @@ function prepareResponse(state: WriteState): ChatResponse { } } +// Trigger background function processing +async function triggerBackgroundProcessing(jobId: string): Promise { + try { + // Call the background function endpoint + await fetch(NETLIFY_BACKGROUND_FUNCTION_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ jobId }) + }) + } catch (error) { + console.error('Error triggering background processing:', error) + // Mark job as failed if we can't trigger the background function + await JobStorage.failJob(jobId, 'Failed to trigger background processing') + } +} + // API endpoint handlers export async function GET() { return json({ apiAvailable: IS_API_AVAILABLE }) @@ -265,13 +311,35 @@ export async function POST({ fetch, request }) { // Check if this is a continuation of an existing process let state: WriteState + let jobId: string if (requestData.stateToken) { - // Continue an existing process + // Continue an existing process - extract jobId from stateToken or request try { - state = JSON.parse(requestData.stateToken) as WriteState + const parsedState = JSON.parse(requestData.stateToken) as WriteState + + // If jobId is provided in the request, use it; otherwise we need to handle legacy stateTokens + if (requestData.jobId) { + jobId = requestData.jobId + // Get the latest state from storage + const jobData = await JobStorage.getJob(jobId) + if (!jobData) { + return json({ + response: 'Job not found. Please try starting over.', + apiAvailable: true + } as ChatResponse) + } + state = jobData.writeState + } else { + // Legacy support - use the state from stateToken directly + state = parsedState + // For legacy requests, we still need to create a job for future processing + jobId = generateJobId() + await JobStorage.createJob(jobId, state) + } + console.log( - `${pencil} write: Continuing from step ${state.step} (workflow ${state.workflowType})` + `${pencil} write: Continuing from step ${state.step} (workflow ${state.workflowType}) - Job ID: ${jobId}` ) } catch (error) { console.error('Error parsing state token:', error) @@ -301,17 +369,46 @@ export async function POST({ fetch, request }) { `${pencil} write: Detected workflow type ${state.workflowType}: ${workflowConfigs[state.workflowType].description}` ) + // Create a new job in storage + jobId = generateJobId() + await JobStorage.createJob(jobId, state) + console.log(`${pencil} write: Created job ${jobId}`) + // For initial calls (no stateToken), return progress string without processing if (requestData.stateToken === undefined) { - return json(prepareResponse(state)) + const response = prepareResponse(state) + return json({ + ...response, + jobId, + status: 'pending' + }) } } - // For subsequent calls, process the step - state = await processStep(state) - - // Return response using helper function - return json(prepareResponse(state)) + // For subsequent calls or continuation, trigger background processing + if (state.nextStep && state.step !== 'complete') { + // Trigger background processing (non-blocking) + triggerBackgroundProcessing(jobId) + + // Update job status to processing + await JobStorage.updateJobStatus(jobId, 'processing') + + // Return current state with job info + const response = prepareResponse(state) + return json({ + ...response, + jobId, + status: 'processing' + }) + } else { + // Job is complete + const response = prepareResponse(state) + return json({ + ...response, + jobId, + status: 'completed' + }) + } } catch (err) { console.error('Error in email generation:', err) return json({ diff --git a/src/routes/api/write/process-step-background.ts b/src/routes/api/write/process-step-background.ts index 4d633dcf5..1cb869d8e 100644 --- a/src/routes/api/write/process-step-background.ts +++ b/src/routes/api/write/process-step-background.ts @@ -1,9 +1,10 @@ import { env } from '$env/dynamic/private' import Anthropic from '@anthropic-ai/sdk' +import { JobStorage } from '$lib/storage' // Import types and configurations from server -import type { StepName, WriteState, StepConfig, JobStorage } from './+server' -import { workflowConfigs } from './+server' +import type { StepName, WriteState, StepConfig } from './+server' +import { workflowConfigs, generateProgressString, prepareResponse } from './+server' // Environment variables for step processing const ANTHROPIC_API_KEY_FOR_WRITE = env.ANTHROPIC_API_KEY_FOR_WRITE || undefined @@ -445,69 +446,176 @@ const stepHandlers: Record< } } -// Process a specific step -export async function processStep(state: WriteState): Promise { - if (!state.nextStep) { - return { - ...state, - step: 'complete', - currentStep: null +// Process a specific step with storage integration +export async function processStep(jobId: string): Promise { + const pencil = '✏️' + + try { + // Load state from storage + const jobData = await JobStorage.getJob(jobId) + if (!jobData) { + throw new Error(`Job ${jobId} not found`) } - } - const currentStep = state.nextStep as StepName - state.currentStep = currentStep + let state = jobData.writeState + console.log(`${pencil} processStep: Processing job ${jobId}, step ${state.nextStep}`) - // Update remaining steps - remove current step from remaining - state.remainingSteps = state.remainingSteps.filter((step) => step !== currentStep) + // Check if job is already complete or has no next step + if (!state.nextStep || state.step === 'complete') { + console.log(`${pencil} processStep: Job ${jobId} already complete`) + return state + } - // Execute the step using the step handler from the map - const stepHandler = stepHandlers[currentStep] - if (!stepHandler) { - throw new Error(`Unknown step: ${currentStep}`) - } + const currentStep = state.nextStep as StepName + state.currentStep = currentStep - const result = await stepHandler(state) + // Update remaining steps - remove current step from remaining + state.remainingSteps = state.remainingSteps.filter((step) => step !== currentStep) - // Update email content (except for research-like steps which update information) - if (!['research'].includes(currentStep)) { - state.email = result.text - } + // Update job status to processing + await JobStorage.updateJobStatus(jobId, 'processing') - // Update state with results - current step is now completed - state.step = currentStep - state.completedSteps.push({ - name: currentStep, - durationSec: result.durationSec - }) - - // Set next step based on workflow configuration - // Note: This requires access to workflowConfigs which should be imported from the server file - const workflowSteps = getWorkflowSteps(state.workflowType) - const currentIndex = workflowSteps.indexOf(currentStep) - - if (currentIndex !== -1 && currentIndex < workflowSteps.length - 1) { - state.nextStep = workflowSteps[currentIndex + 1] - state.currentStep = workflowSteps[currentIndex + 1] - } else { - // Last step completed, mark as complete - state.nextStep = null - state.currentStep = null - state.step = 'complete' - } + // Execute the step using the step handler from the map + const stepHandler = stepHandlers[currentStep] + if (!stepHandler) { + throw new Error(`Unknown step: ${currentStep}`) + } + + console.log(`${pencil} processStep: Executing step ${currentStep} for job ${jobId}`) + const result = await stepHandler(state) - return state + // Update email content (except for research-like steps which update information) + if (!['research'].includes(currentStep)) { + state.email = result.text + } + + // Update state with results - current step is now completed + state.step = currentStep + state.completedSteps.push({ + name: currentStep, + durationSec: result.durationSec + }) + + // Set next step based on workflow configuration + const workflowSteps = getWorkflowSteps(state.workflowType) + const currentIndex = workflowSteps.indexOf(currentStep) + + if (currentIndex !== -1 && currentIndex < workflowSteps.length - 1) { + state.nextStep = workflowSteps[currentIndex + 1] + state.currentStep = workflowSteps[currentIndex + 1] + } else { + // Last step completed, mark as complete + state.nextStep = null + state.currentStep = null + state.step = 'complete' + } + + // Update storage with new state + await JobStorage.updateWriteState(jobId, state) + + // If there are more steps, continue processing + if (state.nextStep && state.step !== 'complete') { + console.log( + `${pencil} processStep: Continuing to next step ${state.nextStep} for job ${jobId}` + ) + // Recursively process the next step + return await processStep(jobId) + } else { + // Mark job as completed + console.log(`${pencil} processStep: Job ${jobId} completed`) + await JobStorage.completeJob(jobId, state) + } + + return state + } catch (error) { + console.error(`${pencil} processStep: Error processing job ${jobId}:`, error) + + // Mark job as failed + await JobStorage.failJob(jobId, error.message || 'Unknown error occurred') + + // Re-throw the error for the caller to handle + throw error + } } -// Helper function to get workflow steps (this will need to be imported from server) +// Helper function to get workflow steps function getWorkflowSteps(workflowType: string): StepName[] { - // This should be imported from the server file or passed as a parameter - // For now, returning a placeholder - const workflowConfigs = { - '1': ['findTarget'], - '2': ['webSearch', 'research'], - '3': ['research'], - '4': ['firstDraft', 'firstCut', 'firstEdit', 'toneEdit', 'finalEdit'] + return workflowConfigs[workflowType]?.steps || [] +} + +// NEW: Background function endpoint handler +export async function POST({ request }) { + const pencil = '✏️' + + try { + const { jobId } = await request.json() + + if (!jobId) { + return new Response(JSON.stringify({ error: 'Job ID is required' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }) + } + + console.log(`${pencil} background: Starting processing for job ${jobId}`) + + // Check if API is available + if (!IS_API_AVAILABLE) { + await JobStorage.failJob(jobId, 'Anthropic API key is not available') + return new Response(JSON.stringify({ error: 'API not available' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }) + } + + // Process the job + const finalState = await processStep(jobId) + + console.log(`${pencil} background: Job ${jobId} processing completed`) + + return new Response( + JSON.stringify({ + success: true, + jobId, + complete: finalState.step === 'complete' + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' } + } + ) + } catch (error) { + console.error(`${pencil} background: Error in background processing:`, error) + + // Try to update job status if we have a jobId + const requestData = await request.json().catch(() => ({})) + if (requestData.jobId) { + await JobStorage.failJob(requestData.jobId, error.message || 'Background processing failed') + } + + return new Response( + JSON.stringify({ + error: 'Background processing failed', + message: error.message + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' } + } + ) } - return workflowConfigs[workflowType] || [] +} + +// For compatibility, also export GET handler +export async function GET() { + return new Response( + JSON.stringify({ + status: 'Background processing service is running', + apiAvailable: IS_API_AVAILABLE + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' } + } + ) } diff --git a/src/routes/api/write/status.ts b/src/routes/api/write/status.ts new file mode 100644 index 000000000..d7c323a55 --- /dev/null +++ b/src/routes/api/write/status.ts @@ -0,0 +1,62 @@ +import { json } from '@sveltejs/kit' +import { JobStorage } from '$lib/storage.js' + +export async function GET({ url }) { + const jobId = url.searchParams.get('jobId') + + if (!jobId) { + return json({ error: 'Job ID is required' }, { status: 400 }) + } + + try { + const jobData = await JobStorage.getJob(jobId) + + if (!jobData) { + return json({ error: 'Job not found' }, { status: 404 }) + } + + // Prepare response using your existing function + const response = prepareResponse(jobData.writeState) + + return json({ + ...response, + jobId, + status: jobData.status, + error: jobData.error, + createdAt: jobData.createdAt, + completedAt: jobData.completedAt + }) + } catch (error) { + console.error('Error checking job status:', error) + return json({ error: 'Internal server error' }, { status: 500 }) + } +} + +// Also support POST for compatibility +export async function POST({ request }) { + const { jobId } = await request.json() + + if (!jobId) { + return json({ error: 'Job ID is required' }, { status: 400 }) + } + + try { + const jobData = await JobStorage.getJob(jobId) + + if (!jobData) { + return json({ error: 'Job not found' }, { status: 404 }) + } + + const response = prepareResponse(jobData.writeState) + + return json({ + ...response, + jobId, + status: jobData.status, + error: jobData.error + }) + } catch (error) { + console.error('Error checking job status:', error) + return json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/src/routes/write/+page.svelte b/src/routes/write/+page.svelte index f42bb0b57..7d1ee45e5 100644 --- a/src/routes/write/+page.svelte +++ b/src/routes/write/+page.svelte @@ -1,7 +1,7 @@