Update system prompt to tool call more reliably

This commit is contained in:
Saoud Rizwan
2024-09-27 19:47:16 -04:00
parent b1dcff8f64
commit 36ba28d347
3 changed files with 105 additions and 72 deletions

View File

@@ -227,21 +227,24 @@ export class ClaudeDev {
}
let askTs: number
if (partial !== undefined) {
const lastMessage = this.claudeMessages.at(-1)
const lastMessageOfType = findLast(this.claudeMessages, (m) => m.ask === type)
const isUpdatingPreviousPartial =
lastMessage && lastMessage.partial && lastMessage.type === "ask" && lastMessage.ask === type
lastMessageOfType &&
lastMessageOfType.partial &&
lastMessageOfType.type === "ask" &&
lastMessageOfType.ask === type
if (partial) {
if (isUpdatingPreviousPartial) {
// existing partial message, so update it
lastMessage.text = text
lastMessage.partial = partial
lastMessageOfType.text = text
lastMessageOfType.partial = partial
// todo be more efficient about saving and posting only new data or one whole message at a time so ignore partial for saves, and only post parts of partial message instead of whole array in new listener
// await this.saveClaudeMessages()
// await this.providerRef.deref()?.postStateToWebview()
await this.providerRef
.deref()
?.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage })
throw new Error("Current ask promise was ignored")
?.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessageOfType })
throw new Error("Current ask promise was ignored 1")
} else {
// this is a new partial message, so add it with partial state
// this.askResponse = undefined
@@ -251,7 +254,7 @@ export class ClaudeDev {
// this.lastMessageTs = askTs
await this.addToClaudeMessages({ ts: Date.now(), type: "ask", ask: type, text, partial })
await this.providerRef.deref()?.postStateToWebview()
throw new Error("Current ask promise was ignored")
throw new Error("Current ask promise was ignored 2")
}
} else {
// partial=false means its a complete version of a previously partial message
@@ -262,9 +265,9 @@ export class ClaudeDev {
this.askResponseImages = undefined
askTs = Date.now()
this.lastMessageTs = askTs
lastMessage.ts = askTs
lastMessage.text = text
lastMessage.partial = false
lastMessageOfType.ts = askTs
lastMessageOfType.text = text
lastMessageOfType.partial = false
await this.saveClaudeMessages()
await this.providerRef.deref()?.postStateToWebview()
} else {
@@ -319,20 +322,23 @@ export class ClaudeDev {
}
if (partial !== undefined) {
const lastMessage = this.claudeMessages.at(-1)
const lastMessageOfType = findLast(this.claudeMessages, (m) => m.say === type)
const isUpdatingPreviousPartial =
lastMessage && lastMessage.partial && lastMessage.type === "say" && lastMessage.say === type
lastMessageOfType &&
lastMessageOfType.partial &&
lastMessageOfType.type === "say" &&
lastMessageOfType.say === type
if (partial) {
if (isUpdatingPreviousPartial) {
// existing partial message, so update it
lastMessage.text = text
lastMessage.images = images
lastMessage.partial = partial
lastMessageOfType.text = text
lastMessageOfType.images = images
lastMessageOfType.partial = partial
// await this.saveClaudeMessages()
// await this.providerRef.deref()?.postStateToWebview()
await this.providerRef
.deref()
?.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage })
?.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessageOfType })
} else {
// this is a new partial message, so add it with partial state
@@ -345,10 +351,10 @@ export class ClaudeDev {
// this is the complete version of a previously partial message, so replace the partial with the complete version
const sayTs = Date.now()
this.lastMessageTs = sayTs
lastMessage.ts = sayTs
lastMessage.text = text
lastMessage.images = images
lastMessage.partial = false
lastMessageOfType.ts = sayTs
lastMessageOfType.text = text
lastMessageOfType.images = images
lastMessageOfType.partial = false
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
await this.saveClaudeMessages()
@@ -1673,16 +1679,22 @@ ${this.customInstructions.trim()}
}
// If the last API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
const lastApiReqFinished = findLast(this.claudeMessages, (m) => m.say === "api_req_finished")
if (lastApiReqFinished && lastApiReqFinished.text) {
const lastApiReqStarted = findLast(this.claudeMessages, (m) => m.say === "api_req_started")
if (lastApiReqStarted && lastApiReqStarted.text) {
const {
tokensIn,
tokensOut,
cacheWrites,
cacheReads,
}: { tokensIn?: number; tokensOut?: number; cacheWrites?: number; cacheReads?: number } = JSON.parse(
lastApiReqFinished.text
lastApiReqStarted.text
)
console.log("lastApiReqStarted", lastApiReqStarted.text, {
tokensIn,
tokensOut,
cacheWrites,
cacheReads,
})
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
const contextWindow = this.api.getModel().info.contextWindow
const maxAllowedSize = Math.max(contextWindow - 40_000, contextWindow * 0.8)
@@ -2494,9 +2506,6 @@ ${this.customInstructions.trim()}
textContent.content = textContentLines.join("\n")
this.assistantMessageContent = [textContent, ...toolCalls]
// Present the updated content
this.presentAssistantMessage()
}
async recursivelyMakeClaudeRequests(
@@ -2692,23 +2701,31 @@ ${this.customInstructions.trim()}
let totalCost: string | undefined
await this.say(
"api_req_finished",
JSON.stringify({
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWrites: cacheCreationInputTokens,
cacheReads: cacheReadInputTokens,
cost:
totalCost ||
this.calculateApiCost(
inputTokens,
outputTokens,
cacheCreationInputTokens,
cacheReadInputTokens
),
})
)
// update api_req_started. we can't use api_req_finished anymore since it's a unique case where it could come after a streaming message (ie in the middle of being updated or executed)
// fortunately api_req_finished was always parsed out for the gui anyways, so it remains solely for legacy purposes to keep track of prices in tasks from history
// (it's worth removing a few months from now)
this.claudeMessages[lastApiReqIndex].text = JSON.stringify({
...JSON.parse(this.claudeMessages[lastApiReqIndex].text),
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWrites: cacheCreationInputTokens,
cacheReads: cacheReadInputTokens,
cost:
totalCost ||
this.calculateApiCost(inputTokens, outputTokens, cacheCreationInputTokens, cacheReadInputTokens),
})
await this.saveClaudeMessages()
await this.providerRef.deref()?.postStateToWebview()
// await this.say(
// "api_req_finished",
// JSON.stringify({
// })
// )
// console.log("apiContentBlocks", apiContentBlocks)
// throw new Error("ClaudeDev fail")
// now add to apiconversationhistory
// need to save assistant responses to file before proceeding to tool use since user can exit at any moment and we wouldn't be able to save the assistant's response
@@ -2725,6 +2742,10 @@ ${this.customInstructions.trim()}
await pWaitFor(() => this.userMessageContentReady)
console.log("attempted to send new request")
// throw new Error("ClaudeDev fail")
const recDidEndLoop = await this.recursivelyMakeClaudeRequests(this.userMessageContent)
didEndLoop = recDidEndLoop
} else {

View File

@@ -77,15 +77,14 @@ INSTRUCTIONS FOR FORMULATING YOUR RESPONSE
You must respond to the user's request by using at least one tool call. When formulating your response, follow these guidelines:
1. Begin your response with normal text, explaining your thoughts, analysis, or plan of action.
2. If you need to use any tools, place ALL tool calls at the END of your message, after your normal text explanation.
1. You might begin your response explaining your thoughts, analysis, plan of action, etc.
2. Place ALL tool calls at the END of your message.
3. You can use multiple tool calls if needed, but they should all be grouped together at the end of your message.
4. After placing the tool calls, do not add any additional normal text. The tool calls should be the final content in your message.
Here's the general structure your responses should follow:
\`\`\`
[Your normal text response explaining your thoughts and actions]
...Your thoughts...
[Tool Call 1]
[Tool Call 2 if needed]
@@ -96,10 +95,25 @@ Here's the general structure your responses should follow:
Remember:
- Choose the most appropriate tool(s) based on the task and the tool descriptions provided.
- Formulate your tool calls using the XML format specified for each tool.
- Provide clear explanations in your normal text about what actions you're taking and why you're using particular tools.
- Provide clear explanations about what actions you're taking and why you're using particular tools.
- Act as if the tool calls will be executed immediately after your message, and your next response will have access to their results.
# Tool Descriptions and XML Formats
# Tool Calls Formatting
Tool calls are formatted with the name of the tool enclosed in XML tags on their own line.
Each parameter is defined within its own set of XML tags, also each on their own line.
Example:
<tool_name>
<parameter1_name>
value1
</parameter1_name>
<parameter2_name>
value2
</parameter2_name>
</tool_name>
Ensure that each tool call follows this structure for consistent parsing and execution.
# Tool Descriptions
## execute_command
<execute_command>
@@ -217,14 +231,10 @@ Parameters:
- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use 'open index.html' to display a created website. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
# Examples
Here are some examples of how to structure your responses with tool calls:
# Tool Calls Examples
## Example 1: Using a single tool
Let's run the test suite for our project. This will help us ensure that all our components are functioning correctly.
<execute_command>
<command>
npm test
@@ -233,8 +243,6 @@ npm test
## Example 2: Using multiple tools
Let's create two new configuration files for the web application, one for the frontend and one for the backend.
<write_to_file>
<path>
./frontend-config.json
@@ -281,8 +289,6 @@ externalServices:
## Example 3: Asking a follow-up question
I've analyzed the project structure, but I need more information to proceed. Let me ask the user for clarification.
<ask_followup_question>
<question>
Which specific feature would you like me to implement in the example.py file?

View File

@@ -426,23 +426,29 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
[isAtBottom, visibleMessages, expandedRows]
)
const [lastScrollMessageCount, setLastScrollMessageCount] = useState<number>(0)
useEffect(() => {
// dont scroll if we're just updating the api req started informational body
const lastMessage = visibleMessages.at(-1)
const isLastApiReqStarted = lastMessage?.say === "api_req_started"
if (didScrollFromApiReqTs && isLastApiReqStarted && lastMessage?.ts === didScrollFromApiReqTs) {
return
if (lastMessage?.partial && isAtBottom) {
virtuosoRef.current?.scrollTo({ top: Number.MAX_SAFE_INTEGER, behavior: "auto" })
} else {
// dont scroll if we're just updating the api req started informational body
const isLastApiReqStarted = lastMessage?.say === "api_req_started"
if (didScrollFromApiReqTs && isLastApiReqStarted && lastMessage?.ts === didScrollFromApiReqTs) {
return
}
// We use a setTimeout to ensure new content is rendered before scrolling to the bottom. virtuoso's followOutput would scroll to the bottom before the new content could render.
const timer = setTimeout(() => {
// TODO: we can use virtuoso's isAtBottom to prevent scrolling if user is scrolled up, and show a 'scroll to bottom' button for better UX
// NOTE: scroll to bottom may not work if you use margin, see virtuoso's troubleshooting
virtuosoRef.current?.scrollTo({ top: Number.MAX_SAFE_INTEGER, behavior: "smooth" })
setDidScrollFromApiReqTs(isLastApiReqStarted ? lastMessage?.ts : undefined) // need to do this in timer since this effect can get called a few times simultaneously
}, 50)
return () => clearTimeout(timer)
}
// We use a setTimeout to ensure new content is rendered before scrolling to the bottom. virtuoso's followOutput would scroll to the bottom before the new content could render.
const timer = setTimeout(() => {
// TODO: we can use virtuoso's isAtBottom to prevent scrolling if user is scrolled up, and show a 'scroll to bottom' button for better UX
// NOTE: scroll to bottom may not work if you use margin, see virtuoso's troubleshooting
virtuosoRef.current?.scrollTo({ top: Number.MAX_SAFE_INTEGER, behavior: "smooth" })
setDidScrollFromApiReqTs(isLastApiReqStarted ? lastMessage?.ts : undefined) // need to do this in timer since this effect can get called a few times simultaneously
}, 50)
return () => clearTimeout(timer)
}, [visibleMessages, didScrollFromApiReqTs])
const placeholderText = useMemo(() => {