mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
remove old parseAssistantMessage stuff
This commit is contained in:
parent
bcf71c5c9f
commit
946cc0cae2
4 changed files with 0 additions and 738 deletions
|
|
@ -1,339 +0,0 @@
|
|||
// npx jest src/core/assistant-message/__tests__/parseAssistantMessage.test.ts
|
||||
|
||||
import { TextDirective, ToolDirective } from "../directives"
|
||||
import { AssistantMessageContent, parseAssistantMessage as parseAssistantMessageV1 } from "../parseAssistantMessage"
|
||||
import { parseAssistantMessageV2 } from "../parseAssistantMessageV2"
|
||||
|
||||
const isEmptyTextContent = (block: AssistantMessageContent) =>
|
||||
block.type === "text" && (block as TextDirective).content === ""
|
||||
|
||||
;[parseAssistantMessageV1, parseAssistantMessageV2].forEach((parser, index) => {
|
||||
describe(`parseAssistantMessageV${index + 1}`, () => {
|
||||
describe("text content parsing", () => {
|
||||
it("should parse a simple text message", () => {
|
||||
const message = "This is a simple text message"
|
||||
const result = parser(message)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toEqual({
|
||||
type: "text",
|
||||
content: message,
|
||||
partial: true, // Text is always partial when it's the last content
|
||||
})
|
||||
})
|
||||
|
||||
it("should parse a multi-line text message", () => {
|
||||
const message = "This is a multi-line\ntext message\nwith several lines"
|
||||
const result = parser(message)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toEqual({
|
||||
type: "text",
|
||||
content: message,
|
||||
partial: true, // Text is always partial when it's the last content
|
||||
})
|
||||
})
|
||||
|
||||
it("should mark text as partial when it's the last content in the message", () => {
|
||||
const message = "This is a partial text"
|
||||
const result = parser(message)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toEqual({
|
||||
type: "text",
|
||||
content: message,
|
||||
partial: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("tool use parsing", () => {
|
||||
it("should parse a simple tool use", () => {
|
||||
const message = "<read_file><path>src/file.ts</path></read_file>"
|
||||
const result = parser(message).filter((block) => !isEmptyTextContent(block))
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
const ToolDirective = result[0] as ToolDirective
|
||||
expect(ToolDirective.type).toBe("tool_use")
|
||||
expect(ToolDirective.name).toBe("read_file")
|
||||
expect(ToolDirective.params.path).toBe("src/file.ts")
|
||||
expect(ToolDirective.partial).toBe(false)
|
||||
})
|
||||
|
||||
it("should parse a tool use with multiple parameters", () => {
|
||||
const message =
|
||||
"<read_file><path>src/file.ts</path><start_line>10</start_line><end_line>20</end_line></read_file>"
|
||||
const result = parser(message).filter((block) => !isEmptyTextContent(block))
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
const ToolDirective = result[0] as ToolDirective
|
||||
expect(ToolDirective.type).toBe("tool_use")
|
||||
expect(ToolDirective.name).toBe("read_file")
|
||||
expect(ToolDirective.params.path).toBe("src/file.ts")
|
||||
expect(ToolDirective.params.start_line).toBe("10")
|
||||
expect(ToolDirective.params.end_line).toBe("20")
|
||||
expect(ToolDirective.partial).toBe(false)
|
||||
})
|
||||
|
||||
it("should mark tool use as partial when it's not closed", () => {
|
||||
const message = "<read_file><path>src/file.ts</path>"
|
||||
const result = parser(message).filter((block) => !isEmptyTextContent(block))
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
const ToolDirective = result[0] as ToolDirective
|
||||
expect(ToolDirective.type).toBe("tool_use")
|
||||
expect(ToolDirective.name).toBe("read_file")
|
||||
expect(ToolDirective.params.path).toBe("src/file.ts")
|
||||
expect(ToolDirective.partial).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle a partial parameter in a tool use", () => {
|
||||
const message = "<read_file><path>src/file.ts"
|
||||
const result = parser(message).filter((block) => !isEmptyTextContent(block))
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
const ToolDirective = result[0] as ToolDirective
|
||||
expect(ToolDirective.type).toBe("tool_use")
|
||||
expect(ToolDirective.name).toBe("read_file")
|
||||
expect(ToolDirective.params.path).toBe("src/file.ts")
|
||||
expect(ToolDirective.partial).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("mixed content parsing", () => {
|
||||
it("should parse text followed by a tool use", () => {
|
||||
const message = "Here's the file content: <read_file><path>src/file.ts</path></read_file>"
|
||||
const result = parser(message)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
|
||||
const textContent = result[0] as TextDirective
|
||||
expect(textContent.type).toBe("text")
|
||||
expect(textContent.content).toBe("Here's the file content:")
|
||||
expect(textContent.partial).toBe(false)
|
||||
|
||||
const ToolDirective = result[1] as ToolDirective
|
||||
expect(ToolDirective.type).toBe("tool_use")
|
||||
expect(ToolDirective.name).toBe("read_file")
|
||||
expect(ToolDirective.params.path).toBe("src/file.ts")
|
||||
expect(ToolDirective.partial).toBe(false)
|
||||
})
|
||||
|
||||
it("should parse a tool use followed by text", () => {
|
||||
const message = "<read_file><path>src/file.ts</path></read_file>Here's what I found in the file."
|
||||
const result = parser(message).filter((block) => !isEmptyTextContent(block))
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
|
||||
const ToolDirective = result[0] as ToolDirective
|
||||
expect(ToolDirective.type).toBe("tool_use")
|
||||
expect(ToolDirective.name).toBe("read_file")
|
||||
expect(ToolDirective.params.path).toBe("src/file.ts")
|
||||
expect(ToolDirective.partial).toBe(false)
|
||||
|
||||
const textContent = result[1] as TextDirective
|
||||
expect(textContent.type).toBe("text")
|
||||
expect(textContent.content).toBe("Here's what I found in the file.")
|
||||
expect(textContent.partial).toBe(true)
|
||||
})
|
||||
|
||||
it("should parse multiple tool uses separated by text", () => {
|
||||
const message =
|
||||
"First file: <read_file><path>src/file1.ts</path></read_file>Second file: <read_file><path>src/file2.ts</path></read_file>"
|
||||
const result = parser(message)
|
||||
|
||||
expect(result).toHaveLength(4)
|
||||
|
||||
expect(result[0].type).toBe("text")
|
||||
expect((result[0] as TextDirective).content).toBe("First file:")
|
||||
|
||||
expect(result[1].type).toBe("tool_use")
|
||||
expect((result[1] as ToolDirective).name).toBe("read_file")
|
||||
expect((result[1] as ToolDirective).params.path).toBe("src/file1.ts")
|
||||
|
||||
expect(result[2].type).toBe("text")
|
||||
expect((result[2] as TextDirective).content).toBe("Second file:")
|
||||
|
||||
expect(result[3].type).toBe("tool_use")
|
||||
expect((result[3] as ToolDirective).name).toBe("read_file")
|
||||
expect((result[3] as ToolDirective).params.path).toBe("src/file2.ts")
|
||||
})
|
||||
})
|
||||
|
||||
describe("special cases", () => {
|
||||
it("should handle the write_to_file tool with content that contains closing tags", () => {
|
||||
const message = `<write_to_file><path>src/file.ts</path><content>
|
||||
function example() {
|
||||
// This has XML-like content: </content>
|
||||
return true;
|
||||
}
|
||||
</content><line_count>5</line_count></write_to_file>`
|
||||
|
||||
const result = parser(message).filter((block) => !isEmptyTextContent(block))
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
const ToolDirective = result[0] as ToolDirective
|
||||
expect(ToolDirective.type).toBe("tool_use")
|
||||
expect(ToolDirective.name).toBe("write_to_file")
|
||||
expect(ToolDirective.params.path).toBe("src/file.ts")
|
||||
expect(ToolDirective.params.line_count).toBe("5")
|
||||
expect(ToolDirective.params.content).toContain("function example()")
|
||||
expect(ToolDirective.params.content).toContain("// This has XML-like content: </content>")
|
||||
expect(ToolDirective.params.content).toContain("return true;")
|
||||
expect(ToolDirective.partial).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle empty messages", () => {
|
||||
const message = ""
|
||||
const result = parser(message)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle malformed tool use tags", () => {
|
||||
const message = "This has a <not_a_tool>malformed tag</not_a_tool>"
|
||||
const result = parser(message)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].type).toBe("text")
|
||||
expect((result[0] as TextDirective).content).toBe(message)
|
||||
})
|
||||
|
||||
it("should handle tool use with no parameters", () => {
|
||||
const message = "<browser_action></browser_action>"
|
||||
const result = parser(message).filter((block) => !isEmptyTextContent(block))
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
const ToolDirective = result[0] as ToolDirective
|
||||
expect(ToolDirective.type).toBe("tool_use")
|
||||
expect(ToolDirective.name).toBe("browser_action")
|
||||
expect(Object.keys(ToolDirective.params).length).toBe(0)
|
||||
expect(ToolDirective.partial).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle nested tool tags that aren't actually nested", () => {
|
||||
const message =
|
||||
"<execute_command><command>echo '<read_file><path>test.txt</path></read_file>'</command></execute_command>"
|
||||
|
||||
const result = parser(message).filter((block) => !isEmptyTextContent(block))
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
const ToolDirective = result[0] as ToolDirective
|
||||
expect(ToolDirective.type).toBe("tool_use")
|
||||
expect(ToolDirective.name).toBe("execute_command")
|
||||
expect(ToolDirective.params.command).toBe("echo '<read_file><path>test.txt</path></read_file>'")
|
||||
expect(ToolDirective.partial).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle a tool use with a parameter containing XML-like content", () => {
|
||||
const message = "<search_files><regex><div>.*</div></regex><path>src</path></search_files>"
|
||||
const result = parser(message).filter((block) => !isEmptyTextContent(block))
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
const ToolDirective = result[0] as ToolDirective
|
||||
expect(ToolDirective.type).toBe("tool_use")
|
||||
expect(ToolDirective.name).toBe("search_files")
|
||||
expect(ToolDirective.params.regex).toBe("<div>.*</div>")
|
||||
expect(ToolDirective.params.path).toBe("src")
|
||||
expect(ToolDirective.partial).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle consecutive tool uses without text in between", () => {
|
||||
const message =
|
||||
"<read_file><path>file1.ts</path></read_file><read_file><path>file2.ts</path></read_file>"
|
||||
const result = parser(message).filter((block) => !isEmptyTextContent(block))
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
|
||||
const ToolDirective1 = result[0] as ToolDirective
|
||||
expect(ToolDirective1.type).toBe("tool_use")
|
||||
expect(ToolDirective1.name).toBe("read_file")
|
||||
expect(ToolDirective1.params.path).toBe("file1.ts")
|
||||
expect(ToolDirective1.partial).toBe(false)
|
||||
|
||||
const ToolDirective2 = result[1] as ToolDirective
|
||||
expect(ToolDirective2.type).toBe("tool_use")
|
||||
expect(ToolDirective2.name).toBe("read_file")
|
||||
expect(ToolDirective2.params.path).toBe("file2.ts")
|
||||
expect(ToolDirective2.partial).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle whitespace in parameters", () => {
|
||||
const message = "<read_file><path> src/file.ts </path></read_file>"
|
||||
const result = parser(message).filter((block) => !isEmptyTextContent(block))
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
const ToolDirective = result[0] as ToolDirective
|
||||
expect(ToolDirective.type).toBe("tool_use")
|
||||
expect(ToolDirective.name).toBe("read_file")
|
||||
expect(ToolDirective.params.path).toBe("src/file.ts")
|
||||
expect(ToolDirective.partial).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle multi-line parameters", () => {
|
||||
const message = `<write_to_file><path>file.ts</path><content>
|
||||
line 1
|
||||
line 2
|
||||
line 3
|
||||
</content><line_count>3</line_count></write_to_file>`
|
||||
const result = parser(message).filter((block) => !isEmptyTextContent(block))
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
const ToolDirective = result[0] as ToolDirective
|
||||
expect(ToolDirective.type).toBe("tool_use")
|
||||
expect(ToolDirective.name).toBe("write_to_file")
|
||||
expect(ToolDirective.params.path).toBe("file.ts")
|
||||
expect(ToolDirective.params.content).toContain("line 1")
|
||||
expect(ToolDirective.params.content).toContain("line 2")
|
||||
expect(ToolDirective.params.content).toContain("line 3")
|
||||
expect(ToolDirective.params.line_count).toBe("3")
|
||||
expect(ToolDirective.partial).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle a complex message with multiple content types", () => {
|
||||
const message = `I'll help you with that task.
|
||||
|
||||
<read_file><path>src/index.ts</path></read_file>
|
||||
|
||||
Now let's modify the file:
|
||||
|
||||
<write_to_file><path>src/index.ts</path><content>
|
||||
// Updated content
|
||||
console.log("Hello world");
|
||||
</content><line_count>2</line_count></write_to_file>
|
||||
|
||||
Let's run the code:
|
||||
|
||||
<execute_command><command>node src/index.ts</command></execute_command>`
|
||||
|
||||
const result = parser(message)
|
||||
|
||||
expect(result).toHaveLength(6)
|
||||
|
||||
// First text block
|
||||
expect(result[0].type).toBe("text")
|
||||
expect((result[0] as TextDirective).content).toBe("I'll help you with that task.")
|
||||
|
||||
// First tool use (read_file)
|
||||
expect(result[1].type).toBe("tool_use")
|
||||
expect((result[1] as ToolDirective).name).toBe("read_file")
|
||||
|
||||
// Second text block
|
||||
expect(result[2].type).toBe("text")
|
||||
expect((result[2] as TextDirective).content).toContain("Now let's modify the file:")
|
||||
|
||||
// Second tool use (write_to_file)
|
||||
expect(result[3].type).toBe("tool_use")
|
||||
expect((result[3] as ToolDirective).name).toBe("write_to_file")
|
||||
|
||||
// Third text block
|
||||
expect(result[4].type).toBe("text")
|
||||
expect((result[4] as TextDirective).content).toContain("Let's run the code:")
|
||||
|
||||
// Third tool use (execute_command)
|
||||
expect(result[5].type).toBe("tool_use")
|
||||
expect((result[5] as ToolDirective).name).toBe("execute_command")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
||||
|
||||
// node --expose-gc --import tsx src/core/assistant-message/__tests__/parseAssistantMessageBenchmark.ts
|
||||
|
||||
import { performance } from "perf_hooks"
|
||||
import { parseAssistantMessage as parseAssistantMessageV1 } from "../parseAssistantMessage"
|
||||
import { parseAssistantMessageV2 } from "../parseAssistantMessageV2"
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
||||
}
|
||||
|
||||
const measureExecutionTime = (fn: Function, input: string, iterations: number = 1000): number => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
fn(input)
|
||||
}
|
||||
|
||||
const start = performance.now()
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
fn(input)
|
||||
}
|
||||
|
||||
const end = performance.now()
|
||||
return (end - start) / iterations // Average time per iteration in ms.
|
||||
}
|
||||
|
||||
const measureMemoryUsage = (
|
||||
fn: Function,
|
||||
input: string,
|
||||
iterations: number = 100,
|
||||
): { heapUsed: number; heapTotal: number } => {
|
||||
if (global.gc) {
|
||||
// Force garbage collection if available.
|
||||
global.gc()
|
||||
} else {
|
||||
console.warn("No garbage collection hook! Run with --expose-gc for more accurate memory measurements.")
|
||||
}
|
||||
|
||||
const initialMemory = process.memoryUsage()
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
fn(input)
|
||||
}
|
||||
|
||||
const finalMemory = process.memoryUsage()
|
||||
|
||||
return {
|
||||
heapUsed: (finalMemory.heapUsed - initialMemory.heapUsed) / iterations,
|
||||
heapTotal: (finalMemory.heapTotal - initialMemory.heapTotal) / iterations,
|
||||
}
|
||||
}
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
name: "Simple text message",
|
||||
input: "This is a simple text message without any tool uses.",
|
||||
},
|
||||
{
|
||||
name: "Message with a simple tool use",
|
||||
input: "Let's read a file: <read_file><path>src/file.ts</path></read_file>",
|
||||
},
|
||||
{
|
||||
name: "Message with a complex tool use (write_to_file)",
|
||||
input: "<write_to_file><path>src/file.ts</path><content>\nfunction example() {\n // This has XML-like content: </content>\n return true;\n}\n</content><line_count>5</line_count></write_to_file>",
|
||||
},
|
||||
{
|
||||
name: "Message with multiple tool uses",
|
||||
input: "First file: <read_file><path>src/file1.ts</path></read_file>\nSecond file: <read_file><path>src/file2.ts</path></read_file>\nLet's write a new file: <write_to_file><path>src/file3.ts</path><content>\nexport function newFunction() {\n return 'Hello world';\n}\n</content><line_count>3</line_count></write_to_file>",
|
||||
},
|
||||
{
|
||||
name: "Large message with repeated tool uses",
|
||||
input: Array(50)
|
||||
.fill(
|
||||
'<read_file><path>src/file.ts</path></read_file>\n<write_to_file><path>output.ts</path><content>console.log("hello");</content><line_count>1</line_count></write_to_file>',
|
||||
)
|
||||
.join("\n"),
|
||||
},
|
||||
]
|
||||
|
||||
const runBenchmark = () => {
|
||||
const maxNameLength = testCases.reduce((max, testCase) => Math.max(max, testCase.name.length), 0)
|
||||
const namePadding = maxNameLength + 2
|
||||
|
||||
console.log(
|
||||
`| ${"Test Case".padEnd(namePadding)} | V1 Time (ms) | V2 Time (ms) | V1/V2 Ratio | V1 Heap (bytes) | V2 Heap (bytes) |`,
|
||||
)
|
||||
console.log(
|
||||
`| ${"-".repeat(namePadding)} | ------------ | ------------ | ----------- | ---------------- | ---------------- |`,
|
||||
)
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const v1Time = measureExecutionTime(parseAssistantMessageV1, testCase.input)
|
||||
const v2Time = measureExecutionTime(parseAssistantMessageV2, testCase.input)
|
||||
const timeRatio = v1Time / v2Time
|
||||
|
||||
const v1Memory = measureMemoryUsage(parseAssistantMessageV1, testCase.input)
|
||||
const v2Memory = measureMemoryUsage(parseAssistantMessageV2, testCase.input)
|
||||
|
||||
console.log(
|
||||
`| ${testCase.name.padEnd(namePadding)} | ` +
|
||||
`${v1Time.toFixed(4).padStart(12)} | ` +
|
||||
`${v2Time.toFixed(4).padStart(12)} | ` +
|
||||
`${timeRatio.toFixed(2).padStart(11)} | ` +
|
||||
`${formatNumber(Math.round(v1Memory.heapUsed)).padStart(16)} | ` +
|
||||
`${formatNumber(Math.round(v2Memory.heapUsed)).padStart(16)} |`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
runBenchmark()
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { DirectiveStreamingParser } from "./DirectiveStreamingParser"
|
||||
import type { Directive } from "./directives"
|
||||
|
||||
// Backward compatibility alias
|
||||
export type AssistantMessageContent = Directive
|
||||
|
||||
export function parseAssistantMessage(assistantMessage: string): Directive[] {
|
||||
return DirectiveStreamingParser.parse(assistantMessage)
|
||||
}
|
||||
|
|
@ -1,279 +0,0 @@
|
|||
import { type ToolName, toolNames } from "@roo-code/types"
|
||||
import { TextDirective, ToolDirective } from "./directives"
|
||||
import { ToolParamName, toolParamNames } from "./directives"
|
||||
|
||||
export type AssistantMessageContent = TextDirective | ToolDirective
|
||||
|
||||
/**
|
||||
* Parses an assistant message string potentially containing mixed text and tool
|
||||
* usage blocks marked with XML-like tags into an array of structured content
|
||||
* objects.
|
||||
*
|
||||
* This version aims for efficiency by avoiding the character-by-character
|
||||
* accumulator of V1. It iterates through the string using an index `i`. At each
|
||||
* position, it checks if the substring *ending* at `i` matches any known
|
||||
* opening or closing tags for tools or parameters using `startsWith` with an
|
||||
* offset.
|
||||
* It uses pre-computed Maps (`ToolDirectiveOpenTags`, `toolParamOpenTags`) for quick
|
||||
* tag lookups.
|
||||
* State is managed using indices (`currentTextContentStart`,
|
||||
* `currentToolDirectiveStart`, `currentParamValueStart`) pointing to the start of the
|
||||
* current block within the original `assistantMessage` string.
|
||||
*
|
||||
* Slicing is used to extract content only when a block (text, parameter, or
|
||||
* tool use) is completed.
|
||||
*
|
||||
* Special handling for `write_to_file` and `new_rule` content parameters is
|
||||
* included, using `indexOf` and `lastIndexOf` on the relevant slice to handle
|
||||
* potentially nested closing tags.
|
||||
*
|
||||
* If the input string ends mid-block, the last open block is added and marked
|
||||
* as partial.
|
||||
*
|
||||
* @param assistantMessage The raw string output from the assistant.
|
||||
* @returns An array of `AssistantMessageContent` objects, which can be
|
||||
* `TextContent` or `ToolDirective`. Blocks that were not fully closed by the
|
||||
* end of the input string will have their `partial` flag set to
|
||||
* `true`.
|
||||
*/
|
||||
|
||||
export function parseAssistantMessageV2(assistantMessage: string): AssistantMessageContent[] {
|
||||
const contentBlocks: AssistantMessageContent[] = []
|
||||
|
||||
let currentTextContentStart = 0 // Index where the current text block started.
|
||||
let currentTextContent: TextDirective | undefined = undefined
|
||||
let currentToolDirectiveStart = 0 // Index *after* the opening tag of the current tool use.
|
||||
let currentToolDirective: ToolDirective | undefined = undefined
|
||||
let currentParamValueStart = 0 // Index *after* the opening tag of the current param.
|
||||
let currentParamName: ToolParamName | undefined = undefined
|
||||
|
||||
// Precompute tags for faster lookups.
|
||||
const ToolDirectiveOpenTags = new Map<string, ToolName>()
|
||||
const toolParamOpenTags = new Map<string, ToolParamName>()
|
||||
|
||||
for (const name of toolNames) {
|
||||
ToolDirectiveOpenTags.set(`<${name}>`, name)
|
||||
}
|
||||
|
||||
for (const name of toolParamNames) {
|
||||
toolParamOpenTags.set(`<${name}>`, name)
|
||||
}
|
||||
|
||||
const len = assistantMessage.length
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const currentCharIndex = i
|
||||
|
||||
// Parsing a tool parameter
|
||||
if (currentToolDirective && currentParamName) {
|
||||
const closeTag = `</${currentParamName}>`
|
||||
// Check if the string *ending* at index `i` matches the closing tag
|
||||
if (
|
||||
currentCharIndex >= closeTag.length - 1 &&
|
||||
assistantMessage.startsWith(
|
||||
closeTag,
|
||||
currentCharIndex - closeTag.length + 1, // Start checking from potential start of tag.
|
||||
)
|
||||
) {
|
||||
// Found the closing tag for the parameter.
|
||||
const value = assistantMessage
|
||||
.slice(
|
||||
currentParamValueStart, // Start after the opening tag.
|
||||
currentCharIndex - closeTag.length + 1, // End before the closing tag.
|
||||
)
|
||||
.trim()
|
||||
currentToolDirective.params[currentParamName] = value
|
||||
currentParamName = undefined // Go back to parsing tool content.
|
||||
// We don't continue loop here, need to check for tool close or other params at index i.
|
||||
} else {
|
||||
continue // Still inside param value, move to next char.
|
||||
}
|
||||
}
|
||||
|
||||
// Parsing a tool use (but not a specific parameter).
|
||||
if (currentToolDirective && !currentParamName) {
|
||||
// Ensure we are not inside a parameter already.
|
||||
// Check if starting a new parameter.
|
||||
let startedNewParam = false
|
||||
|
||||
for (const [tag, paramName] of toolParamOpenTags.entries()) {
|
||||
if (
|
||||
currentCharIndex >= tag.length - 1 &&
|
||||
assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)
|
||||
) {
|
||||
currentParamName = paramName
|
||||
currentParamValueStart = currentCharIndex + 1 // Value starts after the tag.
|
||||
startedNewParam = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (startedNewParam) {
|
||||
continue // Handled start of param, move to next char.
|
||||
}
|
||||
|
||||
// Check if closing the current tool use.
|
||||
const toolCloseTag = `</${currentToolDirective.name}>`
|
||||
|
||||
if (
|
||||
currentCharIndex >= toolCloseTag.length - 1 &&
|
||||
assistantMessage.startsWith(toolCloseTag, currentCharIndex - toolCloseTag.length + 1)
|
||||
) {
|
||||
// End of the tool use found.
|
||||
// Special handling for content params *before* finalizing the
|
||||
// tool.
|
||||
const toolContentSlice = assistantMessage.slice(
|
||||
currentToolDirectiveStart, // From after the tool opening tag.
|
||||
currentCharIndex - toolCloseTag.length + 1, // To before the tool closing tag.
|
||||
)
|
||||
|
||||
// Check if content parameter needs special handling
|
||||
// (write_to_file/new_rule).
|
||||
// This check is important if the closing </content> tag was
|
||||
// missed by the parameter parsing logic (e.g., if content is
|
||||
// empty or parsing logic prioritizes tool close).
|
||||
const contentParamName: ToolParamName = "content"
|
||||
if (
|
||||
currentToolDirective.name === "write_to_file" /* || currentToolDirective.name === "new_rule" */ &&
|
||||
// !(contentParamName in currentToolDirective.params) && // Only if not already parsed.
|
||||
toolContentSlice.includes(`<${contentParamName}>`) // Check if tag exists.
|
||||
) {
|
||||
const contentStartTag = `<${contentParamName}>`
|
||||
const contentEndTag = `</${contentParamName}>`
|
||||
const contentStart = toolContentSlice.indexOf(contentStartTag)
|
||||
|
||||
// Use `lastIndexOf` for robustness against nested tags.
|
||||
const contentEnd = toolContentSlice.lastIndexOf(contentEndTag)
|
||||
|
||||
if (contentStart !== -1 && contentEnd !== -1 && contentEnd > contentStart) {
|
||||
const contentValue = toolContentSlice
|
||||
.slice(contentStart + contentStartTag.length, contentEnd)
|
||||
.trim()
|
||||
|
||||
currentToolDirective.params[contentParamName] = contentValue
|
||||
}
|
||||
}
|
||||
|
||||
currentToolDirective.partial = false // Mark as complete.
|
||||
contentBlocks.push(currentToolDirective)
|
||||
currentToolDirective = undefined // Reset state.
|
||||
currentTextContentStart = currentCharIndex + 1 // Potential text starts after this tag.
|
||||
continue // Move to next char.
|
||||
}
|
||||
|
||||
// If not starting a param and not closing the tool, continue
|
||||
// accumulating tool content implicitly.
|
||||
continue
|
||||
}
|
||||
|
||||
// Parsing text / looking for tool start.
|
||||
if (!currentToolDirective) {
|
||||
// Check if starting a new tool use.
|
||||
let startedNewTool = false
|
||||
|
||||
for (const [tag, toolName] of ToolDirectiveOpenTags.entries()) {
|
||||
if (
|
||||
currentCharIndex >= tag.length - 1 &&
|
||||
assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)
|
||||
) {
|
||||
// End current text block if one was active.
|
||||
if (currentTextContent) {
|
||||
currentTextContent.content = assistantMessage
|
||||
.slice(
|
||||
currentTextContentStart, // From where text started.
|
||||
currentCharIndex - tag.length + 1, // To before the tool tag starts.
|
||||
)
|
||||
.trim()
|
||||
|
||||
currentTextContent.partial = false // Ended because tool started.
|
||||
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
|
||||
currentTextContent = undefined
|
||||
} else {
|
||||
// Check for any text between the last block and this tag.
|
||||
const potentialText = assistantMessage
|
||||
.slice(
|
||||
currentTextContentStart, // From where text *might* have started.
|
||||
currentCharIndex - tag.length + 1, // To before the tool tag starts.
|
||||
)
|
||||
.trim()
|
||||
|
||||
if (potentialText.length > 0) {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
content: potentialText,
|
||||
partial: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Start the new tool use.
|
||||
currentToolDirective = {
|
||||
type: "tool_use",
|
||||
name: toolName,
|
||||
params: {},
|
||||
partial: true, // Assume partial until closing tag is found.
|
||||
}
|
||||
|
||||
currentToolDirectiveStart = currentCharIndex + 1 // Tool content starts after the opening tag.
|
||||
startedNewTool = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (startedNewTool) {
|
||||
continue // Handled start of tool, move to next char.
|
||||
}
|
||||
|
||||
// If not starting a tool, it must be text content.
|
||||
if (!currentTextContent) {
|
||||
// Start a new text block if we aren't already in one.
|
||||
currentTextContentStart = currentCharIndex // Text starts at the current character.
|
||||
|
||||
// Check if the current char is the start of potential text *immediately* after a tag.
|
||||
// This needs the previous state - simpler to let slicing handle it later.
|
||||
// Resetting start index accurately is key.
|
||||
// It should be the index *after* the last processed tag.
|
||||
// The logic managing currentTextContentStart after closing tags handles this.
|
||||
currentTextContent = {
|
||||
type: "text",
|
||||
content: "", // Will be determined by slicing at the end or when a tool starts
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
// Continue accumulating text implicitly; content is extracted later.
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize any open parameter within an open tool use.
|
||||
if (currentToolDirective && currentParamName) {
|
||||
currentToolDirective.params[currentParamName] = assistantMessage
|
||||
.slice(currentParamValueStart) // From param start to end of string.
|
||||
.trim()
|
||||
// Tool use remains partial.
|
||||
}
|
||||
|
||||
// Finalize any open tool use (which might contain the finalized partial param).
|
||||
if (currentToolDirective) {
|
||||
// Tool use is partial because the loop finished before its closing tag.
|
||||
contentBlocks.push(currentToolDirective)
|
||||
}
|
||||
// Finalize any trailing text content.
|
||||
// Only possible if a tool use wasn't open at the very end.
|
||||
else if (currentTextContent) {
|
||||
currentTextContent.content = assistantMessage
|
||||
.slice(currentTextContentStart) // From text start to end of string.
|
||||
.trim()
|
||||
|
||||
// Text is partial because the loop finished.
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
}
|
||||
|
||||
return contentBlocks
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue