From cc8d6a3f23ea79817a4c0864985b1396a75d25e2 Mon Sep 17 00:00:00 2001 From: andrei Date: Wed, 21 May 2025 19:28:57 +0200 Subject: [PATCH 01/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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 a883efc8af6ca3679df1c1ec8a358788db1261a3 Mon Sep 17 00:00:00 2001 From: andrei Date: Tue, 24 Jun 2025 10:48:52 +0200 Subject: [PATCH 09/30] Modified the email writer to use the bare minimum of searches and updated some text. --- src/routes/api/write/+server.ts | 2 +- src/routes/write/+page.svelte | 30 +++++++++++------------------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/routes/api/write/+server.ts b/src/routes/api/write/+server.ts index cc66f4f59..6d8ab2586 100644 --- a/src/routes/api/write/+server.ts +++ b/src/routes/api/write/+server.ts @@ -370,7 +370,7 @@ async function callClaude( { type: 'web_search_20250305', // CHANGED: Use correct tool type from API docs name: 'web_search', - max_uses: 3 // ADDED: Limit searches per request + max_uses: 1 // ADDED: Limit searches per request } ] : undefined diff --git a/src/routes/write/+page.svelte b/src/routes/write/+page.svelte index f42bb0b57..a08b0ae8b 100644 --- a/src/routes/write/+page.svelte +++ b/src/routes/write/+page.svelte @@ -676,29 +676,24 @@ {/if}

    - "Answer questions / fill fields after researching your target. Undefined fields will be + Answer questions / fill fields after researching your target. Undefined fields will be auto-filled. Check the generated email content carefully, as we're bound to make some - mistakes!" + mistakes!

    - The real user interface for entering inputs will surely be refined. For now, scan over the - thirty(!) imperfectly structured input fields at the end of the page, and fill in the ones - that seem the most important. See you back here when done! + You can switch tabs by click on any of the top 4 buttons. This will show new fields: you can + switch back and forth at any time, inputs are saved! If you clicked on "Autofill" and nothing + happened, switch back and forth to refresh the content. The button is grayed out when + unavailable.

    - You can then ask to write content. The AI assistant will auto-fill any fields you didn't - define, based on the ones you did, then proceed to craft an email over a number of steps. The - UX for this part is closer to something we would launch but please give the software team - further feedback! + First, fill in the topmost prompts. You can then ask to autofill content. The AI assistant + will auto-fill any fields you didn't define, based on the ones you did. The UX for this part + is still rough as this feature isn't universally available, so please give us feedback!

    - This is currently a very general writer. If you want it to write to your dad about puppies or - to Trump about how we need to accelerate AI development, it will. We would probably give it - more defaults and impose some restrictions in a truly public version. -

    -

    - For a very quick demo, you can use the button below - it fills in just three fields with - particular hardcoded values, and starts writing content. + To generate the entire email, click the "Write Mail" button. This will take from all input + fields and empty them!

    @@ -738,9 +733,6 @@ Autofill - - - + {:else} - +
    - + {#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 = paragraphText_Research.findIndex((text) => text === question)} - -

    {question}

    - - {/each} - {/each} +
    + + {#if !collapsedSections.form1[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} +
    + {/if} +
    {/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 !collapsedSections.form2[sectionIndex]} +
    + {#each section.subsections as subsection, subsectionIndex} +

    {subsection.title}

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

    Precise Purpose

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

    Supporting Evidence

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

    Logical Structure

    + {/if} + +

    {question}

    + + {/each} + {/each} +
    {/if} - - {#each subsection.questions as question, questionIndex} - - {@const globalIndex = paragraphText_Target.findIndex((text) => text === 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 = paragraphText_Message.findIndex((text) => text === question)} - -

    {question}

    - - {/each} - {/each} +
    + + {#if !collapsedSections.form3[sectionIndex]} +
    + {#each section.subsections as subsection, subsectionIndex} +

    {subsection.title}

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

    {question}

    + + {/each} + {/each} +
    + {/if} +
    {/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 = paragraphText_MessageDetails.findIndex( - (text) => text === question - )} - -

    {question}

    - - {/each} - {/each} +
    + + {#if !collapsedSections.form4[sectionIndex]} +
    + {#each section.subsections as subsection, subsectionIndex} +

    {subsection.title}

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

    {question}

    + + {/each} + {/each} +
    + {/if} +
    {/each}
    {/if} @@ -999,6 +1137,69 @@ background-color: var(--brand-subtle); } + /* UPDATED: Collapsible section styles */ + .section-container { + margin-bottom: 1rem; + border: 1px solid var(--text-subtle); + border-radius: 8px; + overflow: hidden; + } + + .section-header { + width: 100%; + background-color: var(--bg-subtle); + border: none; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + transition: background-color 0.2s ease; + font-weight: normal; + } + + .section-header:hover { + background-color: var(--brand-subtle); + color: var(--bg); + } + + .section-header h1 { + margin: 0; + font-size: 1.5rem; + font-weight: bold; + } + + .chevron { + font-size: 1.2rem; + transition: transform 0.3s ease; + user-select: none; + } + + .chevron.collapsed { + transform: rotate(-90deg); + } + + .chevron.expanded { + transform: rotate(0deg); + } + + .section-content { + padding: 1rem; + border-top: 1px solid var(--text-subtle); + animation: expandSection 0.3s ease-out; + } + + @keyframes expandSection { + from { + opacity: 0; + max-height: 0; + } + to { + opacity: 1; + max-height: 1000px; + } + } + .message { display: flex; border-radius: 10px; From 89c1ca4fa4b4c9cbe87f8628f9384fa0b6ab08fb Mon Sep 17 00:00:00 2001 From: andrei Date: Thu, 26 Jun 2025 12:49:43 +0200 Subject: [PATCH 20/30] Removed question numbering as it was deemed confusing. --- src/routes/write/+page.svelte | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/write/+page.svelte b/src/routes/write/+page.svelte index b0deecfc5..1b530c077 100644 --- a/src/routes/write/+page.svelte +++ b/src/routes/write/+page.svelte @@ -866,7 +866,7 @@

    {question}

    @@ -923,7 +923,7 @@

    {question}

    @@ -967,7 +967,7 @@

    {question}

    @@ -1011,7 +1011,7 @@

    {question}

    From 7763a4ffc656468294ebf86b0d7490cc5d0fb670 Mon Sep 17 00:00:00 2001 From: andrei Date: Thu, 26 Jun 2025 14:21:28 +0200 Subject: [PATCH 21/30] Changed header color on the write page to better match the style. --- 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 1b530c077..e82aaeb6a 100644 --- a/src/routes/write/+page.svelte +++ b/src/routes/write/+page.svelte @@ -1147,7 +1147,7 @@ .section-header { width: 100%; - background-color: var(--bg-subtle); + background-color: var(--brand); border: none; padding: 1rem; display: flex; From c01942aafd07324764afcbe1814d771a71131a21 Mon Sep 17 00:00:00 2001 From: andrei Date: Thu, 26 Jun 2025 14:24:06 +0200 Subject: [PATCH 22/30] Disabled the Write Mail button unless you're on the final form to reduce the amount of buttons. --- 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 e82aaeb6a..a5f194f70 100644 --- a/src/routes/write/+page.svelte +++ b/src/routes/write/+page.svelte @@ -801,7 +801,7 @@ + +
    @@ -1018,6 +1091,50 @@ {/each} {/if} + + + {#if activeForm === 'form5'} +
    + {#each formSections_Revision as section, sectionIndex} +
    + + {#if !collapsedSections.form5[sectionIndex]} +
    + {#each section.subsections as subsection, subsectionIndex} +

    {subsection.title}

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

    {question}

    + + {/each} + {/each} +
    + {/if} +
    + {/each} +
    + {/if}
    {/if} From dc7747cb9879c578541ff00ab9bad259bd241ed2 Mon Sep 17 00:00:00 2001 From: andrei Date: Fri, 27 Jun 2025 14:46:28 +0200 Subject: [PATCH 27/30] Fixed Message Boxes doubling up. Fixed UserInput being sent as a system prompt. Introduced a revise email form for making edits. --- src/routes/api/write/+server.ts | 17 ++++++++++++----- src/routes/write/+page.svelte | 31 ++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/routes/api/write/+server.ts b/src/routes/api/write/+server.ts index 37f30fb17..1c35735ee 100644 --- a/src/routes/api/write/+server.ts +++ b/src/routes/api/write/+server.ts @@ -321,6 +321,8 @@ For each person you find (aim for 3-5 people), please provide: Please cite your sources for each person. Do not tell the user what you are searching for. Only output the final product. + +Please be FAST! ANSWER QUICKLY! ` //Preface with '[Person's Name] = John Doe' etc. @@ -338,6 +340,8 @@ Search for and provide: Please only include information you can verify through your internet search. If you encounter conflicting information, note this and provide the most reliable source. Do not tell the user what you are searching for. Only output the final product. + +Please be FAST! ANSWER QUICKLY! ` //CLAUDE CHANGE: Added User_Revision system prompt with proper email writing context @@ -722,8 +726,8 @@ const stepHandlers: Record< const result = await callClaude( 'findTarget', - ['Basic', 'Target', 'Information'], - 'Hello! Please help me find a person to contact!', + ['Basic', 'Target'], + 'Hello! Please help me find a person to contact!' + System_Prompts['Information'], toolsEnabled // NEW: Pass tool enablement flag ) @@ -743,8 +747,8 @@ const stepHandlers: Record< const result = await callClaude( 'webSearch', - ['Basic', 'webSearch', 'Information', 'Results'], - 'Hello! Please research this person!', + ['Basic', 'webSearch', 'Results'], + 'Hello! Please research this person!' + System_Prompts['Information'], toolsEnabled // NEW: Pass tool enablement flag ) @@ -935,6 +939,7 @@ export async function POST({ fetch, request }) { console.log( `${pencil} write: Continuing from step ${state.step} (workflow ${state.workflowType})` ) + //CLAUDE CHANGE: Debug logging to check if userInput is preserved } catch (error) { console.error('Error parsing state token:', error) return json({ @@ -965,7 +970,9 @@ export async function POST({ fetch, request }) { // For initial calls (no stateToken), return progress string without processing if (requestData.stateToken === undefined) { - return json(prepareResponse(state)) + const response = prepareResponse(state) + //CLAUDE CHANGE: Debug logging to check what's being returned + return json(response) } } diff --git a/src/routes/write/+page.svelte b/src/routes/write/+page.svelte index 6ae681686..35bc9e718 100644 --- a/src/routes/write/+page.svelte +++ b/src/routes/write/+page.svelte @@ -293,7 +293,15 @@ if (typeof localStorage !== 'undefined') { const saved = localStorage.getItem(COLLAPSED_SECTIONS_STORAGE_KEY) if (saved) { - collapsedSections = JSON.parse(saved) + const savedState = JSON.parse(saved) + //CLAUDE CHANGE: Ensure form5 exists in loaded state + collapsedSections = { + form1: savedState.form1 || [false], + form2: savedState.form2 || [false, true, true], + form3: savedState.form3 || [false], + form4: savedState.form4 || [true, true, true], + form5: savedState.form5 || [false] + } } else { // Reset to default if no saved state collapsedSections = { @@ -511,7 +519,7 @@ input = input + '[3]' break - //CLAUDE CHANGE: Added form5 case for revision workflow + //CLAUDE CHANGE: Added form5 case for revision workflow with current email case 'form5': input = input + '[5]' break @@ -520,6 +528,15 @@ input = input + '[4]' break } + + //CLAUDE CHANGE: For form5, add current email before the questions + if (activeForm === 'form5') { + const latestEmail = messages.filter((m) => m.role === 'assistant')[0]?.content || '' + if (latestEmail) { + input += `Current Email:\n${latestEmail}\n\n` + } + } + for (let i = 0; i < currentQuestionArray.length; i++) { input = input + currentQuestionArray[i] + ':\n' + (currentInputArray[i] || 'undefined') + '\n\n' @@ -543,6 +560,10 @@ }) const initialData = await initialResponse.json() + console.log('initialData: ', initialData.stateToken) + + //CLAUDE CHANGE: Clean up any incomplete progress messages before adding new one + messages = messages.filter((m) => m.role !== 'progress' || m.complete) // Add server-generated progress message with complete flag messages = [ @@ -563,6 +584,7 @@ }, 100) // Continue with the normal process, but pass the stateToken + console.log('initialData: ', initialData.stateToken) await processSteps(null, initialData.stateToken) } catch (error) { console.error('Error calling email API:', error) @@ -586,6 +608,7 @@ try { // Prepare the request body const requestBody = stateToken ? { stateToken } : inputMessages + console.log(requestBody) // Make the API call const response = await fetch('api/write', { @@ -598,7 +621,6 @@ // Process the response const data = await response.json() - console.log('DATA: \n' + data) console.log(data.information) // Update API availability @@ -606,6 +628,9 @@ // Show progress if available if (data.progressString) { + //CLAUDE CHANGE: Clean up incomplete progress messages before updating + messages = messages.filter((m) => m.role !== 'progress' || m.complete) + // Find existing progress message or create one const progressIndex = messages.findIndex((m) => m.role === 'progress') if (progressIndex >= 0) { From 3a68c35a89177dbce5a49fa357b1ffcf629b4ad1 Mon Sep 17 00:00:00 2001 From: andrei Date: Fri, 27 Jun 2025 15:13:23 +0200 Subject: [PATCH 28/30] Touch commit. --- touch.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 touch.txt diff --git a/touch.txt b/touch.txt new file mode 100644 index 000000000..e69de29bb From e4a56a293498db28978955d18edaa1b64d03a424 Mon Sep 17 00:00:00 2001 From: andrei Date: Fri, 27 Jun 2025 15:13:36 +0200 Subject: [PATCH 29/30] Touch commit. --- touch.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 touch.txt diff --git a/touch.txt b/touch.txt deleted file mode 100644 index e69de29bb..000000000 From 8716d46df0009f6c7effd1ddb3d726547213edb3 Mon Sep 17 00:00:00 2001 From: andrei Date: Fri, 27 Jun 2025 15:58:41 +0200 Subject: [PATCH 30/30] Adjusted prompts to better target individuals (not orgs) and be faster. --- src/routes/api/write/+server.ts | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/routes/api/write/+server.ts b/src/routes/api/write/+server.ts index 1c35735ee..54b24681e 100644 --- a/src/routes/api/write/+server.ts +++ b/src/routes/api/write/+server.ts @@ -60,44 +60,44 @@ const stepConfigs: Record = { toolsEnabled: true, maxToolCalls: 3, description: 'Find possible targets (using web search)', - model: 'claude-3-5-haiku-20241022' + model: 'claude-3-5-haiku-latest' }, webSearch: { toolsEnabled: true, maxToolCalls: 3, description: 'Research the target (using web search)', - model: 'claude-3-5-haiku-20241022' + model: 'claude-3-5-haiku-latest' }, research: { toolsEnabled: false, maxToolCalls: 2, description: 'Auto-fill missing user inputs', - model: 'claude-3-5-sonnet-20241022' + model: 'claude-3-7-sonnet-latest' }, // Text processing steps use Sonnet for quality firstDraft: { toolsEnabled: false, - model: 'claude-3-5-sonnet-20241022' + model: 'claude-3-7-sonnet-latest' }, firstCut: { toolsEnabled: false, - model: 'claude-3-5-sonnet-20241022' + model: 'claude-3-7-sonnet-latest' }, firstEdit: { toolsEnabled: false, - model: 'claude-3-5-sonnet-20241022' + model: 'claude-3-7-sonnet-latest' }, toneEdit: { toolsEnabled: false, - model: 'claude-3-5-sonnet-20241022' + model: 'claude-3-7-sonnet-latest' }, finalEdit: { toolsEnabled: false, - model: 'claude-3-5-sonnet-20241022' + model: 'claude-3-7-sonnet-latest' }, userRevision: { toolsEnabled: false, - model: 'claude-3-5-sonnet-20241022' + model: 'claude-3-7-sonnet-latest' } } @@ -319,10 +319,10 @@ For each person you find (aim for 3-5 people), please provide: 3. Their organization 4. Brief note on their public stance on AI safety -Please cite your sources for each person. Do not tell the user what you are searching for. Only output the final product. -Please be FAST! ANSWER QUICKLY! +Focus on finding People. NOT Organisations. +OUTPUT VERY VERY QUICKLY! YOU HAVE VERY LITTLE TIME! OTHERWISE IT'LL ALL BE WASTED! BE FAST!!! ` //Preface with '[Person's Name] = John Doe' etc. @@ -340,8 +340,7 @@ Search for and provide: Please only include information you can verify through your internet search. If you encounter conflicting information, note this and provide the most reliable source. Do not tell the user what you are searching for. Only output the final product. - -Please be FAST! ANSWER QUICKLY! +OUTPUT VERY VERY QUICKLY! YOU HAVE VERY LITTLE TIME! OTHERWISE IT'LL ALL BE WASTED! BE FAST!!! ` //CLAUDE CHANGE: Added User_Revision system prompt with proper email writing context @@ -424,8 +423,7 @@ async function callClaude( const startTime = Date.now() //CLAUDE CHANGE: Determine model to use - from parameter, step config, or default - const modelToUse = - model || stepConfigs[stepName as StepName]?.model || 'claude-3-5-sonnet-20241022' + const modelToUse = model || stepConfigs[stepName as StepName]?.model || 'claude-3-5-haiku-latest' console.time(`${logPrefix}`)