Refactor tool responses

This commit is contained in:
Saoud Rizwan
2024-09-29 02:11:30 -04:00
parent 06ccaf6f67
commit ebead1b1fa
2 changed files with 73 additions and 68 deletions

View File

@@ -9,6 +9,7 @@ import * as path from "path"
import { serializeError } from "serialize-error" import { serializeError } from "serialize-error"
import * as vscode from "vscode" import * as vscode from "vscode"
import { ApiHandler, buildApiHandler } from "../api" import { ApiHandler, buildApiHandler } from "../api"
import { ApiStream } from "../api/transform/stream"
import { diagnosticsToProblemsString, getNewDiagnostics } from "../integrations/diagnostics" import { diagnosticsToProblemsString, getNewDiagnostics } from "../integrations/diagnostics"
import { formatContentBlockToMarkdown } from "../integrations/misc/export-markdown" import { formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
import { extractTextFromFile } from "../integrations/misc/extract-text" import { extractTextFromFile } from "../integrations/misc/extract-text"
@@ -18,7 +19,7 @@ import { listFiles } from "../services/glob/list-files"
import { regexSearchFiles } from "../services/ripgrep" import { regexSearchFiles } from "../services/ripgrep"
import { parseSourceCodeForDefinitionsTopLevel } from "../services/tree-sitter" import { parseSourceCodeForDefinitionsTopLevel } from "../services/tree-sitter"
import { ApiConfiguration } from "../shared/api" import { ApiConfiguration } from "../shared/api"
import { findLast, findLastIndex } from "../shared/array" import { findLastIndex } from "../shared/array"
import { combineApiRequests } from "../shared/combineApiRequests" import { combineApiRequests } from "../shared/combineApiRequests"
import { combineCommandSequences } from "../shared/combineCommandSequences" import { combineCommandSequences } from "../shared/combineCommandSequences"
import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "../shared/ExtensionMessage" import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "../shared/ExtensionMessage"
@@ -37,11 +38,10 @@ import {
toolParamNames, toolParamNames,
} from "./AssistantMessage" } from "./AssistantMessage"
import { parseMentions } from "./mentions" import { parseMentions } from "./mentions"
import { formatResponse } from "./prompts/responses"
import { SYSTEM_PROMPT } from "./prompts/system" import { SYSTEM_PROMPT } from "./prompts/system"
import { TOOLS } from "./prompts/tools"
import { truncateHalfConversation } from "./sliding-window" import { truncateHalfConversation } from "./sliding-window"
import { ClaudeDevProvider } from "./webview/ClaudeDevProvider" import { ClaudeDevProvider } from "./webview/ClaudeDevProvider"
import { ApiStream } from "../api/transform/stream"
const cwd = const cwd =
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
@@ -359,7 +359,7 @@ export class ClaudeDev {
await this.say("text", task, images) await this.say("text", task, images)
let imageBlocks: Anthropic.ImageBlockParam[] = this.formatImagesIntoBlocks(images) let imageBlocks: Anthropic.ImageBlockParam[] = formatResponse.imageBlocks(images)
await this.initiateTaskLoop([ await this.initiateTaskLoop([
{ {
type: "text", type: "text",
@@ -547,7 +547,7 @@ export class ClaudeDev {
}) })
if (responseImages && responseImages.length > 0) { if (responseImages && responseImages.length > 0) {
newUserContent.push(...this.formatImagesIntoBlocks(responseImages)) newUserContent.push(...formatResponse.imageBlocks(responseImages))
} }
await this.overwriteApiConversationHistory(modifiedApiConversationHistory) await this.overwriteApiConversationHistory(modifiedApiConversationHistory)
@@ -577,7 +577,7 @@ export class ClaudeDev {
nextUserContent = [ nextUserContent = [
{ {
type: "text", type: "text",
text: this.formatNoToolsResponse(), text: formatResponse.noToolsUsed(),
}, },
] ]
this.consecutiveMistakeCount++ this.consecutiveMistakeCount++
@@ -628,7 +628,7 @@ export class ClaudeDev {
) )
return [ return [
false, false,
await this.formatToolError( formatResponse.toolError(
`Missing value for required parameter 'content'. This may occur if the file is too large, exceeding output limits. Consider splitting into smaller files or reducing content size. Please retry with all required parameters.` `Missing value for required parameter 'content'. This may occur if the file is too large, exceeding output limits. Consider splitting into smaller files or reducing content size. Please retry with all required parameters.`
), ),
] ]
@@ -867,9 +867,9 @@ export class ClaudeDev {
if (response === "messageResponse") { if (response === "messageResponse") {
await this.say("user_feedback", text, images) await this.say("user_feedback", text, images)
return [true, this.formatToolResponseWithImages(await this.formatToolDeniedFeedback(text), images)] return [true, formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images)]
} }
return [true, await this.formatToolDenied()] return [true, formatResponse.toolDenied()]
} }
// Save the changes // Save the changes
@@ -988,7 +988,7 @@ export class ClaudeDev {
"error", "error",
`Error writing file:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}` `Error writing file:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`
) )
return [false, await this.formatToolError(errorString)] return [false, formatResponse.toolError(errorString)]
} }
} }
@@ -1181,7 +1181,7 @@ export class ClaudeDev {
await this.say("user_feedback", userFeedback.text, userFeedback.images) await this.say("user_feedback", userFeedback.text, userFeedback.images)
return [ return [
true, true,
this.formatToolResponseWithImages( formatResponse.toolResult(
`Command is still running in the user's terminal.${ `Command is still running in the user's terminal.${
result.length > 0 ? `\nHere's the output so far:\n${result}` : "" result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
}\n\nThe user provided the following feedback:\n<feedback>\n${userFeedback.text}\n</feedback>`, }\n\nThe user provided the following feedback:\n<feedback>\n${userFeedback.text}\n</feedback>`,
@@ -1345,7 +1345,7 @@ ${this.customInstructions.trim()}
if (response === "messageResponse") { if (response === "messageResponse") {
await this.say("user_feedback", text, images) await this.say("user_feedback", text, images)
pushToolResult( pushToolResult(
this.formatToolResponseWithImages(await this.formatToolDeniedFeedback(text), images) formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images)
) )
// this.userMessageContent.push({ // this.userMessageContent.push({
// type: "text", // type: "text",
@@ -1362,7 +1362,7 @@ ${this.customInstructions.trim()}
this.didRejectTool = true this.didRejectTool = true
return false return false
} }
pushToolResult(await this.formatToolDenied()) pushToolResult(formatResponse.toolDenied())
// this.toolResults.push({ // this.toolResults.push({
// type: "tool_result", // type: "tool_result",
// tool_use_id: toolUseId, // tool_use_id: toolUseId,
@@ -1385,7 +1385,7 @@ ${this.customInstructions.trim()}
// tool_use_id: toolUseId, // tool_use_id: toolUseId,
// content: await this.formatToolError(errorString), // content: await this.formatToolError(errorString),
// }) // })
pushToolResult(await this.formatToolError(errorString)) pushToolResult(formatResponse.toolError(errorString))
} }
switch (block.name) { switch (block.name) {
@@ -1981,7 +1981,7 @@ ${this.customInstructions.trim()}
await this.say("inspect_site_result", logs, [screenshot]) await this.say("inspect_site_result", logs, [screenshot])
pushToolResult( pushToolResult(
this.formatToolResponseWithImages( formatResponse.toolResult(
`The site has been visited, with console logs captured and a screenshot taken for your analysis.\n\nConsole logs:\n${ `The site has been visited, with console logs captured and a screenshot taken for your analysis.\n\nConsole logs:\n${
logs || "(No logs)" logs || "(No logs)"
}`, }`,
@@ -2044,9 +2044,7 @@ ${this.customInstructions.trim()}
this.consecutiveMistakeCount = 0 this.consecutiveMistakeCount = 0
const { text, images } = await this.ask("followup", question, false) const { text, images } = await this.ask("followup", question, false)
await this.say("user_feedback", text ?? "", images) await this.say("user_feedback", text ?? "", images)
pushToolResult( pushToolResult(formatResponse.toolResult(`<answer>\n${text}\n</answer>`, images))
this.formatToolResponseWithImages(`<answer>\n${text}\n</answer>`, images)
)
break break
} }
} catch (error) { } catch (error) {
@@ -2140,7 +2138,7 @@ ${this.customInstructions.trim()}
} }
await this.say("user_feedback", text ?? "", images) await this.say("user_feedback", text ?? "", images)
pushToolResult( pushToolResult(
this.formatToolResponseWithImages( formatResponse.toolResult(
`The user has provided feedback on the results. Consider their input to continue the task, and then attempt completion again.\n<feedback>\n${text}\n</feedback>`, `The user has provided feedback on the results. Consider their input to continue the task, and then attempt completion again.\n<feedback>\n${text}\n</feedback>`,
images images
) )
@@ -2325,9 +2323,9 @@ ${this.customInstructions.trim()}
...[ ...[
{ {
type: "text", type: "text",
text: `You seem to be having trouble proceeding. The user has provided the following feedback to help guide you:\n<feedback>\n${text}\n</feedback>`, text: formatResponse.tooManyMistakes(text),
} as Anthropic.Messages.TextBlockParam, } as Anthropic.Messages.TextBlockParam,
...this.formatImagesIntoBlocks(images), ...formatResponse.imageBlocks(images),
] ]
) )
} }
@@ -2468,7 +2466,7 @@ ${this.customInstructions.trim()}
if (!didToolUse) { if (!didToolUse) {
this.userMessageContent.push({ this.userMessageContent.push({
type: "text", type: "text",
text: this.formatNoToolsResponse(), text: formatResponse.noToolsUsed(),
}) })
} }
@@ -2539,33 +2537,6 @@ ${this.customInstructions.trim()}
]) ])
} }
// Formatting responses to Claude
private formatImagesIntoBlocks(images?: string[]): Anthropic.ImageBlockParam[] {
return images
? images.map((dataUrl) => {
// data:image/png;base64,base64string
const [rest, base64] = dataUrl.split(",")
const mimeType = rest.split(":")[1].split(";")[0]
return {
type: "image",
source: { type: "base64", media_type: mimeType, data: base64 },
} as Anthropic.ImageBlockParam
})
: []
}
private formatToolResponseWithImages(text: string, images?: string[]): ToolResponse {
if (images && images.length > 0) {
const textBlock: Anthropic.TextBlockParam = { type: "text", text }
const imageBlocks: Anthropic.ImageBlockParam[] = this.formatImagesIntoBlocks(images)
// Placing images after text leads to better results
return [textBlock, ...imageBlocks]
} else {
return text
}
}
async getEnvironmentDetails(includeFileDetails: boolean = false) { async getEnvironmentDetails(includeFileDetails: boolean = false) {
let details = "" let details = ""
@@ -2696,22 +2667,6 @@ ${this.customInstructions.trim()}
return `<environment_details>\n${details.trim()}\n</environment_details>` return `<environment_details>\n${details.trim()}\n</environment_details>`
} }
async formatToolDeniedFeedback(feedback?: string) {
return `The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`
}
async formatToolDenied() {
return `The user denied this operation.`
}
async formatToolError(error?: string) {
return `The tool execution failed with the following error:\n<error>\n${error}\n</error>`
}
formatNoToolsResponse() {
return "If you have completed the user's task, use the attempt_completion tool. If you require additional information from the user, use the ask_followup_question tool. Otherwise, if you have not completed the task and do not need additional information, then proceed with the next step of the task. (This is an automated message, so do not respond to it conversationally.)"
}
async sayAndCreateMissingParamError(toolName: ToolName, paramName: string, relPath?: string) { async sayAndCreateMissingParamError(toolName: ToolName, paramName: string, relPath?: string) {
await this.say( await this.say(
"error", "error",
@@ -2719,8 +2674,6 @@ ${this.customInstructions.trim()}
relPath ? ` for '${relPath.toPosix()}'` : "" relPath ? ` for '${relPath.toPosix()}'` : ""
} without value for required parameter '${paramName}'. Retrying...` } without value for required parameter '${paramName}'. Retrying...`
) )
return await this.formatToolError( return formatResponse.toolError(formatResponse.missingToolParameterError(paramName))
`Missing value for required parameter '${paramName}'. Please retry with complete response.`
)
} }
} }

View File

@@ -0,0 +1,52 @@
import { Anthropic } from "@anthropic-ai/sdk"
export const formatResponse = {
toolDenied: () => `The user denied this operation.`,
toolDeniedWithFeedback: (feedback?: string) =>
`The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
noToolsUsed: () =>
"If you have completed the user's task, use the attempt_completion tool. If you require additional information from the user, use the ask_followup_question tool. Otherwise, if you have not completed the task and do not need additional information, then proceed with the next step of the task. (This is an automated message, so do not respond to it conversationally.)",
tooManyMistakes: (feedback?: string) =>
`You seem to be having trouble proceeding. The user has provided the following feedback to help guide you:\n<feedback>\n${feedback}\n</feedback>`,
missingToolParameterError: (paramName: string) =>
`Missing value for required parameter '${paramName}'. Please retry with complete response.`,
toolResult: (
text: string,
images?: string[]
): string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam> => {
if (images && images.length > 0) {
const textBlock: Anthropic.TextBlockParam = { type: "text", text }
const imageBlocks: Anthropic.ImageBlockParam[] = formatImagesIntoBlocks(images)
// Placing images after text leads to better results
return [textBlock, ...imageBlocks]
} else {
return text
}
},
imageBlocks: (images?: string[]): Anthropic.ImageBlockParam[] => {
return formatImagesIntoBlocks(images)
},
}
// to avoid circular dependency
const formatImagesIntoBlocks = (images?: string[]): Anthropic.ImageBlockParam[] => {
return images
? images.map((dataUrl) => {
// data:image/png;base64,base64string
const [rest, base64] = dataUrl.split(",")
const mimeType = rest.split(":")[1].split(";")[0]
return {
type: "image",
source: { type: "base64", media_type: mimeType, data: base64 },
} as Anthropic.ImageBlockParam
})
: []
}