mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-15 13:45:10 +08:00
fix: align tool call protocol and thinking controls
This commit is contained in:
@@ -13,18 +13,18 @@ func TestRegression_RobustXMLAndCDATA(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "Standard JSON parameters (Regression)",
|
||||
text: `<tools><tool_call><tool_name>foo</tool_name><param>{"a": 1}</param></tool_call></tools>`,
|
||||
expected: []ParsedToolCall{{Name: "foo", Input: map[string]any{"a": float64(1)}}},
|
||||
text: `<tool_calls><invoke name="foo"><parameter name="a">1</parameter></invoke></tool_calls>`,
|
||||
expected: []ParsedToolCall{{Name: "foo", Input: map[string]any{"a": "1"}}},
|
||||
},
|
||||
{
|
||||
name: "XML tags parameters (Regression)",
|
||||
text: `<tools><tool_call><tool_name>foo</tool_name><param><arg1>hello</arg1></param></tool_call></tools>`,
|
||||
text: `<tool_calls><invoke name="foo"><parameter name="arg1">hello</parameter></invoke></tool_calls>`,
|
||||
expected: []ParsedToolCall{{Name: "foo", Input: map[string]any{"arg1": "hello"}}},
|
||||
},
|
||||
{
|
||||
name: "CDATA parameters (New Feature)",
|
||||
text: `<tools><tool_call><tool_name>write_file</tool_name><param><content><![CDATA[line 1
|
||||
line 2 with <tags> and & symbols]]></content></param></tool_call></tools>`,
|
||||
text: `<tool_calls><invoke name="write_file"><parameter name="content"><![CDATA[line 1
|
||||
line 2 with <tags> and & symbols]]></parameter></invoke></tool_calls>`,
|
||||
expected: []ParsedToolCall{{
|
||||
Name: "write_file",
|
||||
Input: map[string]any{"content": "line 1\nline 2 with <tags> and & symbols"},
|
||||
@@ -32,9 +32,9 @@ line 2 with <tags> and & symbols]]></content></param></tool_call></tools>`,
|
||||
},
|
||||
{
|
||||
name: "Nested XML with repeated parameters (New Feature)",
|
||||
text: `<tools><tool_call><tool_name>write_file</tool_name><param><path>script.sh</path><content><![CDATA[#!/bin/bash
|
||||
text: `<tool_calls><invoke name="write_file"><parameter name="path">script.sh</parameter><parameter name="content"><![CDATA[#!/bin/bash
|
||||
echo "hello"
|
||||
]]></content><item>first</item><item>second</item></param></tool_call></tools>`,
|
||||
]]></parameter><parameter name="item">first</parameter><parameter name="item">second</parameter></invoke></tool_calls>`,
|
||||
expected: []ParsedToolCall{{
|
||||
Name: "write_file",
|
||||
Input: map[string]any{
|
||||
@@ -46,7 +46,7 @@ echo "hello"
|
||||
},
|
||||
{
|
||||
name: "Dirty XML with unescaped symbols (Robustness Improvement)",
|
||||
text: `<tools><tool_call><tool_name>bash</tool_name><param><command>echo "hello" > out.txt && cat out.txt</command></param></tool_call></tools>`,
|
||||
text: `<tool_calls><invoke name="bash"><parameter name="command">echo "hello" > out.txt && cat out.txt</parameter></invoke></tool_calls>`,
|
||||
expected: []ParsedToolCall{{
|
||||
Name: "bash",
|
||||
Input: map[string]any{"command": "echo \"hello\" > out.txt && cat out.txt"},
|
||||
@@ -54,7 +54,7 @@ echo "hello"
|
||||
},
|
||||
{
|
||||
name: "Mixed JSON inside CDATA (New Hybrid Case)",
|
||||
text: `<tools><tool_call><tool_name>foo</tool_name><param><![CDATA[{"json_param": "works"}]]></param></tool_call></tools>`,
|
||||
text: `<tool_calls><invoke name="foo"><parameter name="json_param"><![CDATA[works]]></parameter></invoke></tool_calls>`,
|
||||
expected: []ParsedToolCall{{
|
||||
Name: "foo",
|
||||
Input: map[string]any{"json_param": "works"},
|
||||
|
||||
@@ -36,93 +36,139 @@ func BuildToolCallInstructions(toolNames []string) string {
|
||||
|
||||
return `TOOL CALL FORMAT — FOLLOW EXACTLY:
|
||||
|
||||
<tools>
|
||||
<tool_call>
|
||||
<tool_name>TOOL_NAME_HERE</tool_name>
|
||||
<param>
|
||||
<PARAMETER_NAME><![CDATA[PARAMETER_VALUE]]></PARAMETER_NAME>
|
||||
</param>
|
||||
</tool_call>
|
||||
</tools>
|
||||
<tool_calls>
|
||||
<invoke name="TOOL_NAME_HERE">
|
||||
<parameter name="PARAMETER_NAME"><![CDATA[PARAMETER_VALUE]]></parameter>
|
||||
</invoke>
|
||||
</tool_calls>
|
||||
|
||||
RULES:
|
||||
1) Use the <tools> XML wrapper format only.
|
||||
2) Put one or more <tool_call> entries under a single <tools> root.
|
||||
3) Use <tool_name> for the tool name and <param> for the argument container.
|
||||
1) Use the <tool_calls> XML wrapper format only.
|
||||
2) Put one or more <invoke> entries under a single <tool_calls> root.
|
||||
3) Put the tool name in the invoke name attribute: <invoke name="TOOL_NAME">.
|
||||
4) All string values must use <![CDATA[...]]>, even short ones. This includes code, scripts, file contents, prompts, paths, names, and queries.
|
||||
5) Objects use nested XML elements. Arrays may repeat the same tag or use <item> children.
|
||||
6) Numbers, booleans, and null stay plain text.
|
||||
7) Use only the parameter names in the tool schema. Do not invent fields.
|
||||
8) Do NOT wrap XML in markdown fences. Do NOT output explanations, role markers, or internal monologue.
|
||||
5) Every top-level argument must be a <parameter name="ARG_NAME">...</parameter> node.
|
||||
6) Objects use nested XML elements inside the parameter body. Arrays may repeat <item> children.
|
||||
7) Numbers, booleans, and null stay plain text.
|
||||
8) Use only the parameter names in the tool schema. Do not invent fields.
|
||||
9) Do NOT wrap XML in markdown fences. Do NOT output explanations, role markers, or internal monologue.
|
||||
|
||||
PARAMETER SHAPES:
|
||||
- string => <name><![CDATA[value]]></name>
|
||||
- object => nested XML elements
|
||||
- array => repeated tags or <item> children
|
||||
- number/bool/null => plain text
|
||||
- string => <parameter name="x"><![CDATA[value]]></parameter>
|
||||
- object => <parameter name="x"><field>...</field></parameter>
|
||||
- array => <parameter name="x"><item>...</item><item>...</item></parameter>
|
||||
- number/bool/null => <parameter name="x">plain_text</parameter>
|
||||
|
||||
【WRONG — Do NOT do these】:
|
||||
|
||||
Wrong 1 — mixed text after XML:
|
||||
<tools>...</tools> I hope this helps.
|
||||
Wrong 2 — JSON payload inside <param>:
|
||||
<tool_call><tool_name>` + ex1 + `</tool_name><param>{"path":"x"}</param></tool_call>
|
||||
Wrong 3 — Markdown code fences:
|
||||
<tool_calls>...</tool_calls> I hope this helps.
|
||||
Wrong 2 — Markdown code fences:
|
||||
` + "```xml" + `
|
||||
<tools>...</tools>
|
||||
<tool_calls>...</tool_calls>
|
||||
` + "```" + `
|
||||
|
||||
Remember: The ONLY valid way to use tools is the <tools>...</tools> XML block at the end of your response.
|
||||
Remember: The ONLY valid way to use tools is the <tool_calls>...</tool_calls> XML block at the end of your response.
|
||||
|
||||
【CORRECT EXAMPLES】:
|
||||
|
||||
Example A — Single tool:
|
||||
<tools>
|
||||
<tool_call>
|
||||
<tool_name>` + ex1 + `</tool_name>
|
||||
<param>` + ex1Params + `</param>
|
||||
</tool_call>
|
||||
</tools>
|
||||
<tool_calls>
|
||||
<invoke name="` + ex1 + `">
|
||||
` + indentPromptParameters(ex1Params, " ") + `
|
||||
</invoke>
|
||||
</tool_calls>
|
||||
|
||||
Example B — Two tools in parallel:
|
||||
<tools>
|
||||
<tool_call>
|
||||
<tool_name>` + ex1 + `</tool_name>
|
||||
<param>` + ex1Params + `</param>
|
||||
</tool_call>
|
||||
<tool_call>
|
||||
<tool_name>` + ex2 + `</tool_name>
|
||||
<param>` + ex2Params + `</param>
|
||||
</tool_call>
|
||||
</tools>
|
||||
<tool_calls>
|
||||
<invoke name="` + ex1 + `">
|
||||
` + indentPromptParameters(ex1Params, " ") + `
|
||||
</invoke>
|
||||
<invoke name="` + ex2 + `">
|
||||
` + indentPromptParameters(ex2Params, " ") + `
|
||||
</invoke>
|
||||
<invoke name="Read">
|
||||
<parameter name="file_path">` + promptCDATA("/abs/path/to/another-file.txt") + `</parameter>
|
||||
</invoke>
|
||||
</tool_calls>
|
||||
|
||||
Example C — Tool with nested XML parameters:
|
||||
<tools>
|
||||
<tool_call>
|
||||
<tool_name>` + ex3 + `</tool_name>
|
||||
<param>` + ex3Params + `</param>
|
||||
</tool_call>
|
||||
</tools>
|
||||
|
||||
<tool_calls>
|
||||
<invoke name="` + ex3 + `">
|
||||
` + indentPromptParameters(ex3Params, " ") + `
|
||||
</invoke>
|
||||
</tool_calls>
|
||||
|
||||
Example D — Tool with long script using CDATA (RELIABLE FOR CODE/SCRIPTS):
|
||||
<tools>
|
||||
<tool_call>
|
||||
<tool_name>` + ex2 + `</tool_name>
|
||||
<param>
|
||||
<path>` + promptCDATA("script.sh") + `</path>
|
||||
<content><![CDATA[
|
||||
<tool_calls>
|
||||
<invoke name="` + ex2 + `">
|
||||
<parameter name="path">` + promptCDATA("script.sh") + `</parameter>
|
||||
<parameter name="content"><![CDATA[
|
||||
#!/bin/bash
|
||||
if [ "$1" == "test" ]; then
|
||||
echo "Success!"
|
||||
fi
|
||||
]]></content>
|
||||
</param>
|
||||
</tool_call>
|
||||
</tools>
|
||||
]]></parameter>
|
||||
</invoke>
|
||||
</tool_calls>
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
func indentPromptParameters(body, indent string) string {
|
||||
if strings.TrimSpace(body) == "" {
|
||||
return indent + `<parameter name="content"></parameter>`
|
||||
}
|
||||
lines := strings.Split(body, "\n")
|
||||
for i, line := range lines {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
lines[i] = line
|
||||
continue
|
||||
}
|
||||
lines[i] = indent + line
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func wrapParameter(name, inner string) string {
|
||||
return `<parameter name="` + name + `">` + inner + `</parameter>`
|
||||
}
|
||||
|
||||
func exampleReadParams(name string) string {
|
||||
switch strings.TrimSpace(name) {
|
||||
case "Read":
|
||||
return wrapParameter("file_path", promptCDATA("README.md"))
|
||||
case "Glob":
|
||||
return wrapParameter("pattern", promptCDATA("**/*.go")) + "\n" + wrapParameter("path", promptCDATA("."))
|
||||
default:
|
||||
return wrapParameter("path", promptCDATA("src/main.go"))
|
||||
}
|
||||
}
|
||||
|
||||
func exampleWriteOrExecParams(name string) string {
|
||||
switch strings.TrimSpace(name) {
|
||||
case "Bash", "execute_command":
|
||||
return wrapParameter("command", promptCDATA("pwd"))
|
||||
case "exec_command":
|
||||
return wrapParameter("cmd", promptCDATA("pwd"))
|
||||
case "Edit":
|
||||
return wrapParameter("file_path", promptCDATA("README.md")) + "\n" + wrapParameter("old_string", promptCDATA("foo")) + "\n" + wrapParameter("new_string", promptCDATA("bar"))
|
||||
case "MultiEdit":
|
||||
return wrapParameter("file_path", promptCDATA("README.md")) + "\n" + `<parameter name="edits"><item><old_string>` + promptCDATA("foo") + `</old_string><new_string>` + promptCDATA("bar") + `</new_string></item></parameter>`
|
||||
default:
|
||||
return wrapParameter("path", promptCDATA("output.txt")) + "\n" + wrapParameter("content", promptCDATA("Hello world"))
|
||||
}
|
||||
}
|
||||
|
||||
func exampleInteractiveParams(name string) string {
|
||||
switch strings.TrimSpace(name) {
|
||||
case "Task":
|
||||
return wrapParameter("description", promptCDATA("Investigate flaky tests")) + "\n" + wrapParameter("prompt", promptCDATA("Run targeted tests and summarize failures"))
|
||||
default:
|
||||
return wrapParameter("question", promptCDATA("Which approach do you prefer?")) + "\n" + `<parameter name="follow_up"><item><text>` + promptCDATA("Option A") + `</text></item><item><text>` + promptCDATA("Option B") + `</text></item></parameter>`
|
||||
}
|
||||
}
|
||||
|
||||
func matchAny(name string, candidates ...string) bool {
|
||||
for _, c := range candidates {
|
||||
if name == c {
|
||||
@@ -132,41 +178,6 @@ func matchAny(name string, candidates ...string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func exampleReadParams(name string) string {
|
||||
switch strings.TrimSpace(name) {
|
||||
case "Read":
|
||||
return `<file_path>` + promptCDATA("README.md") + `</file_path>`
|
||||
case "Glob":
|
||||
return `<pattern>` + promptCDATA("**/*.go") + `</pattern><path>` + promptCDATA(".") + `</path>`
|
||||
default:
|
||||
return `<path>` + promptCDATA("src/main.go") + `</path>`
|
||||
}
|
||||
}
|
||||
|
||||
func exampleWriteOrExecParams(name string) string {
|
||||
switch strings.TrimSpace(name) {
|
||||
case "Bash", "execute_command":
|
||||
return `<command>` + promptCDATA("pwd") + `</command>`
|
||||
case "exec_command":
|
||||
return `<cmd>` + promptCDATA("pwd") + `</cmd>`
|
||||
case "Edit":
|
||||
return `<file_path>` + promptCDATA("README.md") + `</file_path><old_string>` + promptCDATA("foo") + `</old_string><new_string>` + promptCDATA("bar") + `</new_string>`
|
||||
case "MultiEdit":
|
||||
return `<file_path>` + promptCDATA("README.md") + `</file_path><edits><old_string>` + promptCDATA("foo") + `</old_string><new_string>` + promptCDATA("bar") + `</new_string></edits>`
|
||||
default:
|
||||
return `<path>` + promptCDATA("output.txt") + `</path><content>` + promptCDATA("Hello world") + `</content>`
|
||||
}
|
||||
}
|
||||
|
||||
func exampleInteractiveParams(name string) string {
|
||||
switch strings.TrimSpace(name) {
|
||||
case "Task":
|
||||
return `<description>` + promptCDATA("Investigate flaky tests") + `</description><prompt>` + promptCDATA("Run targeted tests and summarize failures") + `</prompt>`
|
||||
default:
|
||||
return `<question>` + promptCDATA("Which approach do you prefer?") + `</question><follow_up><text>` + promptCDATA("Option A") + `</text></follow_up><follow_up><text>` + promptCDATA("Option B") + `</text></follow_up>`
|
||||
}
|
||||
}
|
||||
|
||||
func promptCDATA(text string) string {
|
||||
if text == "" {
|
||||
return ""
|
||||
|
||||
@@ -7,20 +7,20 @@ import (
|
||||
|
||||
func TestBuildToolCallInstructions_ExecCommandUsesCmdExample(t *testing.T) {
|
||||
out := BuildToolCallInstructions([]string{"exec_command"})
|
||||
if !strings.Contains(out, `<tool_name>exec_command</tool_name>`) {
|
||||
if !strings.Contains(out, `<invoke name="exec_command">`) {
|
||||
t.Fatalf("expected exec_command in examples, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, `<param><cmd><![CDATA[pwd]]></cmd></param>`) {
|
||||
if !strings.Contains(out, `<parameter name="cmd"><![CDATA[pwd]]></parameter>`) {
|
||||
t.Fatalf("expected cmd parameter example for exec_command, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildToolCallInstructions_ExecuteCommandUsesCommandExample(t *testing.T) {
|
||||
out := BuildToolCallInstructions([]string{"execute_command"})
|
||||
if !strings.Contains(out, `<tool_name>execute_command</tool_name>`) {
|
||||
if !strings.Contains(out, `<invoke name="execute_command">`) {
|
||||
t.Fatalf("expected execute_command in examples, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, `<param><command><![CDATA[pwd]]></command></param>`) {
|
||||
if !strings.Contains(out, `<parameter name="command"><![CDATA[pwd]]></parameter>`) {
|
||||
t.Fatalf("expected command parameter example for execute_command, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,12 +74,7 @@ func filterToolCallsDetailed(parsed []ParsedToolCall) ([]ParsedToolCall, []strin
|
||||
|
||||
func looksLikeToolCallSyntax(text string) bool {
|
||||
lower := strings.ToLower(text)
|
||||
return strings.Contains(lower, "<tools") ||
|
||||
strings.Contains(lower, "<tool_call") ||
|
||||
strings.Contains(lower, "<attempt_completion") ||
|
||||
strings.Contains(lower, "<ask_followup_question") ||
|
||||
strings.Contains(lower, "<new_task") ||
|
||||
strings.Contains(lower, "<result")
|
||||
return strings.Contains(lower, "<tool_calls")
|
||||
}
|
||||
|
||||
func stripFencedCodeBlocks(text string) string {
|
||||
|
||||
@@ -7,12 +7,13 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var xmlToolsWrapperPattern = regexp.MustCompile(`(?is)<tools\b[^>]*>\s*(.*?)\s*</tools>`)
|
||||
var xmlToolCallPattern = regexp.MustCompile(`(?is)<tool_call\b[^>]*>\s*(.*?)\s*</tool_call>`)
|
||||
var xmlCanonicalToolCallBodyPattern = regexp.MustCompile(`(?is)^\s*<(?:[a-z0-9_:-]+:)?tool_name\b[^>]*>(.*?)</(?:[a-z0-9_:-]+:)?tool_name>\s*<(?:[a-z0-9_:-]+:)?param\b[^>]*>(.*?)</(?:[a-z0-9_:-]+:)?param>\s*$`)
|
||||
var xmlToolCallsWrapperPattern = regexp.MustCompile(`(?is)<tool_calls\b[^>]*>\s*(.*?)\s*</tool_calls>`)
|
||||
var xmlInvokePattern = regexp.MustCompile(`(?is)<invoke\b([^>]*)>\s*(.*?)\s*</invoke>`)
|
||||
var xmlParameterPattern = regexp.MustCompile(`(?is)<parameter\b([^>]*)>\s*(.*?)\s*</parameter>`)
|
||||
var xmlAttrPattern = regexp.MustCompile(`(?is)\b([a-z0-9_:-]+)\s*=\s*("([^"]*)"|'([^']*)')`)
|
||||
|
||||
func parseXMLToolCalls(text string) []ParsedToolCall {
|
||||
wrappers := xmlToolsWrapperPattern.FindAllStringSubmatch(text, -1)
|
||||
wrappers := xmlToolCallsWrapperPattern.FindAllStringSubmatch(text, -1)
|
||||
if len(wrappers) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -21,7 +22,7 @@ func parseXMLToolCalls(text string) []ParsedToolCall {
|
||||
if len(wrapper) < 2 {
|
||||
continue
|
||||
}
|
||||
for _, block := range xmlToolCallPattern.FindAllString(wrapper[1], -1) {
|
||||
for _, block := range xmlInvokePattern.FindAllStringSubmatch(wrapper[1], -1) {
|
||||
call, ok := parseSingleXMLToolCall(block)
|
||||
if !ok {
|
||||
continue
|
||||
@@ -35,37 +36,90 @@ func parseXMLToolCalls(text string) []ParsedToolCall {
|
||||
return out
|
||||
}
|
||||
|
||||
func parseSingleXMLToolCall(block string) (ParsedToolCall, bool) {
|
||||
inner := strings.TrimSpace(block)
|
||||
inner = strings.TrimPrefix(inner, "<tool_call>")
|
||||
inner = strings.TrimSuffix(inner, "</tool_call>")
|
||||
inner = strings.TrimSpace(inner)
|
||||
func parseSingleXMLToolCall(block []string) (ParsedToolCall, bool) {
|
||||
if len(block) < 3 {
|
||||
return ParsedToolCall{}, false
|
||||
}
|
||||
attrs := parseXMLTagAttributes(block[1])
|
||||
name := strings.TrimSpace(html.UnescapeString(attrs["name"]))
|
||||
if name == "" {
|
||||
return ParsedToolCall{}, false
|
||||
}
|
||||
|
||||
inner := strings.TrimSpace(block[2])
|
||||
if strings.HasPrefix(inner, "{") {
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal([]byte(inner), &payload); err == nil {
|
||||
name := strings.TrimSpace(asString(payload["name"]))
|
||||
if name != "" {
|
||||
input := map[string]any{}
|
||||
if params, ok := payload["input"].(map[string]any); ok {
|
||||
input := map[string]any{}
|
||||
if params, ok := payload["input"].(map[string]any); ok {
|
||||
input = params
|
||||
}
|
||||
if len(input) == 0 {
|
||||
if params, ok := payload["parameters"].(map[string]any); ok {
|
||||
input = params
|
||||
}
|
||||
return ParsedToolCall{Name: name, Input: input}, true
|
||||
}
|
||||
return ParsedToolCall{Name: name, Input: input}, true
|
||||
}
|
||||
}
|
||||
|
||||
m := xmlCanonicalToolCallBodyPattern.FindStringSubmatch(inner)
|
||||
if len(m) < 3 {
|
||||
return ParsedToolCall{}, false
|
||||
input := map[string]any{}
|
||||
for _, paramMatch := range xmlParameterPattern.FindAllStringSubmatch(inner, -1) {
|
||||
if len(paramMatch) < 3 {
|
||||
continue
|
||||
}
|
||||
paramAttrs := parseXMLTagAttributes(paramMatch[1])
|
||||
paramName := strings.TrimSpace(html.UnescapeString(paramAttrs["name"]))
|
||||
if paramName == "" {
|
||||
continue
|
||||
}
|
||||
value := parseInvokeParameterValue(paramMatch[2])
|
||||
appendMarkupValue(input, paramName, value)
|
||||
}
|
||||
name := strings.TrimSpace(html.UnescapeString(extractRawTagValue(m[1])))
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return ParsedToolCall{}, false
|
||||
|
||||
if len(input) == 0 {
|
||||
if strings.TrimSpace(inner) != "" {
|
||||
return ParsedToolCall{}, false
|
||||
}
|
||||
return ParsedToolCall{Name: name, Input: map[string]any{}}, true
|
||||
}
|
||||
return ParsedToolCall{Name: name, Input: parseStructuredToolCallInput(m[2])}, true
|
||||
return ParsedToolCall{Name: name, Input: input}, true
|
||||
}
|
||||
|
||||
func asString(v any) string {
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
func parseXMLTagAttributes(raw string) map[string]string {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return map[string]string{}
|
||||
}
|
||||
out := map[string]string{}
|
||||
for _, m := range xmlAttrPattern.FindAllStringSubmatch(raw, -1) {
|
||||
if len(m) < 5 {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(strings.TrimSpace(m[1]))
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
value := m[3]
|
||||
if value == "" {
|
||||
value = m[4]
|
||||
}
|
||||
out[key] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseInvokeParameterValue(raw string) any {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
if parsed := parseStructuredToolCallInput(trimmed); len(parsed) > 0 {
|
||||
if len(parsed) == 1 {
|
||||
if rawValue, ok := parsed["_raw"].(string); ok {
|
||||
return rawValue
|
||||
}
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
return html.UnescapeString(extractRawTagValue(trimmed))
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ func TestFormatOpenAIToolCalls(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseToolCallsSupportsToolsWrapper(t *testing.T) {
|
||||
text := `<tools><tool_call><tool_name>Bash</tool_name><param><command>pwd</command><description>show cwd</description></param></tool_call></tools>`
|
||||
func TestParseToolCallsSupportsToolCallsWrapper(t *testing.T) {
|
||||
text := `<tool_calls><invoke name="Bash"><parameter name="command">pwd</parameter><parameter name="description">show cwd</parameter></invoke></tool_calls>`
|
||||
calls := ParseToolCalls(text, []string{"bash"})
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("expected 1 call, got %#v", calls)
|
||||
@@ -31,9 +31,9 @@ func TestParseToolCallsSupportsToolsWrapper(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseToolCallsSupportsStandaloneToolWithMultilineCDATAAndRepeatedXMLTags(t *testing.T) {
|
||||
text := `<tools><tool_call><tool_name>write_file</tool_name><param><path>script.sh</path><content><![CDATA[#!/bin/bash
|
||||
text := `<tool_calls><invoke name="write_file"><parameter name="path">script.sh</parameter><parameter name="content"><![CDATA[#!/bin/bash
|
||||
echo "hello"
|
||||
]]></content><item>first</item><item>second</item></param></tool_call></tools>`
|
||||
]]></parameter><parameter name="item">first</parameter><parameter name="item">second</parameter></invoke></tool_calls>`
|
||||
calls := ParseToolCalls(text, []string{"write_file"})
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("expected 1 call, got %#v", calls)
|
||||
@@ -54,8 +54,8 @@ echo "hello"
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseToolCallsSupportsCanonicalParamsJSON(t *testing.T) {
|
||||
text := `<tools><tool_call><tool_name>get_weather</tool_name><param>{"city":"beijing","unit":"c"}</param></tool_call></tools>`
|
||||
func TestParseToolCallsSupportsInvokeParameters(t *testing.T) {
|
||||
text := `<tool_calls><invoke name="get_weather"><parameter name="city">beijing</parameter><parameter name="unit">c</parameter></invoke></tool_calls>`
|
||||
calls := ParseToolCalls(text, []string{"get_weather"})
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("expected 1 call, got %#v", calls)
|
||||
@@ -69,7 +69,7 @@ func TestParseToolCallsSupportsCanonicalParamsJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseToolCallsPreservesRawMalformedParams(t *testing.T) {
|
||||
text := `<tools><tool_call><tool_name>execute_command</tool_name><param>cd /root && git status</param></tool_call></tools>`
|
||||
text := `<tool_calls><invoke name="execute_command"><parameter name="command">cd /root && git status</parameter></invoke></tool_calls>`
|
||||
calls := ParseToolCalls(text, []string{"execute_command"})
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("expected 1 call, got %#v", calls)
|
||||
@@ -77,9 +77,9 @@ func TestParseToolCallsPreservesRawMalformedParams(t *testing.T) {
|
||||
if calls[0].Name != "execute_command" {
|
||||
t.Fatalf("expected tool name execute_command, got %q", calls[0].Name)
|
||||
}
|
||||
raw, ok := calls[0].Input["_raw"].(string)
|
||||
raw, ok := calls[0].Input["command"].(string)
|
||||
if !ok {
|
||||
t.Fatalf("expected raw argument tracking, got %#v", calls[0].Input)
|
||||
t.Fatalf("expected raw command tracking, got %#v", calls[0].Input)
|
||||
}
|
||||
if raw != "cd /root && git status" {
|
||||
t.Fatalf("expected raw arguments to be preserved, got %q", raw)
|
||||
@@ -87,7 +87,7 @@ func TestParseToolCallsPreservesRawMalformedParams(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseToolCallsSupportsParamsJSONWithAmpersandCommand(t *testing.T) {
|
||||
text := `<tools><tool_call><tool_name>execute_command</tool_name><param>{"command":"sshpass -p 'xxx' ssh -o StrictHostKeyChecking=no -p 1111 root@111.111.111.111 'cd /root && git clone https://github.com/ericc-ch/copilot-api.git'","cwd":null,"timeout":null}</param></tool_call></tools>`
|
||||
text := `<tool_calls><invoke name="execute_command"><parameter name="command">sshpass -p 'xxx' ssh -o StrictHostKeyChecking=no -p 1111 root@111.111.111.111 'cd /root && git clone https://github.com/ericc-ch/copilot-api.git'</parameter><parameter name="cwd"></parameter><parameter name="timeout"></parameter></invoke></tool_calls>`
|
||||
calls := ParseToolCalls(text, []string{"execute_command"})
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("expected 1 call, got %#v", calls)
|
||||
@@ -102,7 +102,7 @@ func TestParseToolCallsSupportsParamsJSONWithAmpersandCommand(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseToolCallsDoesNotTreatParamsNameTagAsToolName(t *testing.T) {
|
||||
text := `<tools><tool_call><tool_name>execute_command</tool_name><param><tool_name>file.txt</tool_name><command>pwd</command></param></tool_call></tools>`
|
||||
text := `<tool_calls><invoke name="execute_command"><parameter name="tool_name">file.txt</parameter><parameter name="command">pwd</parameter></invoke></tool_calls>`
|
||||
calls := ParseToolCalls(text, []string{"execute_command"})
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("expected 1 call, got %#v", calls)
|
||||
@@ -115,8 +115,8 @@ func TestParseToolCallsDoesNotTreatParamsNameTagAsToolName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseToolCallsDetailedMarksToolsSyntax(t *testing.T) {
|
||||
text := `<tools><tool_call><tool_name>Bash</tool_name><param><command>pwd</command></param></tool_call></tools>`
|
||||
func TestParseToolCallsDetailedMarksToolCallsSyntax(t *testing.T) {
|
||||
text := `<tool_calls><invoke name="Bash"><parameter name="command">pwd</parameter></invoke></tool_calls>`
|
||||
res := ParseToolCallsDetailed(text, []string{"bash"})
|
||||
if !res.SawToolCallSyntax {
|
||||
t.Fatalf("expected SawToolCallSyntax=true, got %#v", res)
|
||||
@@ -127,7 +127,7 @@ func TestParseToolCallsDetailedMarksToolsSyntax(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseToolCallsSupportsInlineJSONToolObject(t *testing.T) {
|
||||
text := `<tools><tool_call>{"name":"Bash","input":{"command":"pwd","description":"show cwd"}}</tool_call></tools>`
|
||||
text := `<tool_calls><invoke name="Bash">{"input":{"command":"pwd","description":"show cwd"}}</invoke></tool_calls>`
|
||||
calls := ParseToolCalls(text, []string{"bash"})
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("expected 1 call, got %#v", calls)
|
||||
@@ -141,7 +141,7 @@ func TestParseToolCallsSupportsInlineJSONToolObject(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseToolCallsDoesNotAcceptMismatchedMarkupTags(t *testing.T) {
|
||||
text := `<tools><tool_call><tool_name>read_file</function><param>{"path":"README.md"}</param></tool_call></tools>`
|
||||
text := `<tool_calls><invoke name="read_file"><parameter name="path">README.md</function></invoke></tool_calls>`
|
||||
calls := ParseToolCalls(text, []string{"read_file"})
|
||||
if len(calls) != 0 {
|
||||
t.Fatalf("expected mismatched tags to be rejected, got %#v", calls)
|
||||
@@ -149,26 +149,37 @@ func TestParseToolCallsDoesNotAcceptMismatchedMarkupTags(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseToolCallsDoesNotTreatNameInsideParamsAsToolName(t *testing.T) {
|
||||
text := `<tools><tool_call><param><tool_name>data_only</tool_name><path>README.md</path></param></tool_call></tools>`
|
||||
text := `<tool_calls><invoke><parameter name="path">README.md</parameter></invoke></tool_calls>`
|
||||
calls := ParseToolCalls(text, []string{"read_file"})
|
||||
if len(calls) != 0 {
|
||||
t.Fatalf("expected no tool call when name appears only under params, got %#v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseToolCallsRejectsLegacyToolCallsRoot(t *testing.T) {
|
||||
text := `<tool_calls><tool_call><tool_name>read_file</tool_name><param>{"path":"README.md"}</param></tool_call></tool_calls>`
|
||||
func TestParseToolCallsRejectsLegacyToolsWrapper(t *testing.T) {
|
||||
text := `<tools><tool_call><tool_name>read_file</tool_name><param>{"path":"README.md"}</param></tool_call></tools>`
|
||||
calls := ParseToolCalls(text, []string{"read_file"})
|
||||
if len(calls) != 0 {
|
||||
t.Fatalf("expected legacy tool_calls root to be rejected, got %#v", calls)
|
||||
t.Fatalf("expected legacy tools wrapper to be rejected, got %#v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseToolCallsRejectsLegacyParametersTag(t *testing.T) {
|
||||
text := `<tools><tool_call><tool_name>read_file</tool_name><parameters>{"path":"README.md"}</parameters></tool_call></tools>`
|
||||
func TestParseToolCallsRejectsBareInvokeWithoutToolCallsWrapper(t *testing.T) {
|
||||
text := `<invoke name="read_file"><parameter name="path">README.md</parameter></invoke>`
|
||||
res := ParseToolCallsDetailed(text, []string{"read_file"})
|
||||
if len(res.Calls) != 0 {
|
||||
t.Fatalf("expected bare invoke to be rejected, got %#v", res.Calls)
|
||||
}
|
||||
if res.SawToolCallSyntax {
|
||||
t.Fatalf("expected bare invoke to no longer count as supported syntax, got %#v", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseToolCallsRejectsLegacyCanonicalBody(t *testing.T) {
|
||||
text := `<tool_calls><invoke name="read_file"><tool_name>read_file</tool_name><param>{"path":"README.md"}</param></invoke></tool_calls>`
|
||||
calls := ParseToolCalls(text, []string{"read_file"})
|
||||
if len(calls) != 0 {
|
||||
t.Fatalf("expected legacy parameters tag to be rejected, got %#v", calls)
|
||||
t.Fatalf("expected legacy canonical body to be rejected, got %#v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +321,7 @@ func TestRepairLooseJSONWithNestedObjects(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseToolCallsUnescapesHTMLEntityArguments(t *testing.T) {
|
||||
text := `<tools><tool_call><tool_name>Bash</tool_name><param>{"command":"echo a > out.txt"}</param></tool_call></tools>`
|
||||
text := `<tool_calls><invoke name="Bash"><parameter name="command">echo a > out.txt</parameter></invoke></tool_calls>`
|
||||
calls := ParseToolCalls(text, []string{"bash"})
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("expected one call, got %#v", calls)
|
||||
@@ -322,7 +333,7 @@ func TestParseToolCallsUnescapesHTMLEntityArguments(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseToolCallsIgnoresXMLInsideFencedCodeBlock(t *testing.T) {
|
||||
text := "Here is an example:\n```xml\n<tools><tool_call><tool_name>read_file</tool_name><param>{\"path\":\"README.md\"}</param></tool_call></tools>\n```\nDo not execute it."
|
||||
text := "Here is an example:\n```xml\n<tool_calls><invoke name=\"read_file\"><parameter name=\"path\">README.md</parameter></invoke></tool_calls>\n```\nDo not execute it."
|
||||
res := ParseToolCallsDetailed(text, []string{"read_file"})
|
||||
if len(res.Calls) != 0 {
|
||||
t.Fatalf("expected no parsed calls for fenced example, got %#v", res.Calls)
|
||||
@@ -330,7 +341,7 @@ func TestParseToolCallsIgnoresXMLInsideFencedCodeBlock(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseToolCallsParsesOnlyNonFencedXMLToolCall(t *testing.T) {
|
||||
text := "```xml\n<tools><tool_call><tool_name>read_file</tool_name><param>{\"path\":\"README.md\"}</param></tool_call></tools>\n```\n<tools><tool_call><tool_name>search</tool_name><param>{\"q\":\"golang\"}</param></tool_call></tools>"
|
||||
text := "```xml\n<tool_calls><invoke name=\"read_file\"><parameter name=\"path\">README.md</parameter></invoke></tool_calls>\n```\n<tool_calls><invoke name=\"search\"><parameter name=\"q\">golang</parameter></invoke></tool_calls>"
|
||||
res := ParseToolCallsDetailed(text, []string{"read_file", "search"})
|
||||
if len(res.Calls) != 1 {
|
||||
t.Fatalf("expected exactly one parsed call outside fence, got %#v", res.Calls)
|
||||
@@ -341,7 +352,7 @@ func TestParseToolCallsParsesOnlyNonFencedXMLToolCall(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseToolCallsParsesAfterFourBacktickFence(t *testing.T) {
|
||||
text := "````markdown\n```xml\n<tools><tool_call><tool_name>read_file</tool_name><param>{\"path\":\"README.md\"}</param></tool_call></tools>\n```\n````\n<tools><tool_call><tool_name>search</tool_name><param>{\"q\":\"outside\"}</param></tool_call></tools>"
|
||||
text := "````markdown\n```xml\n<tool_calls><invoke name=\"read_file\"><parameter name=\"path\">README.md</parameter></invoke></tool_calls>\n```\n````\n<tool_calls><invoke name=\"search\"><parameter name=\"q\">outside</parameter></invoke></tool_calls>"
|
||||
res := ParseToolCallsDetailed(text, []string{"read_file", "search"})
|
||||
if len(res.Calls) != 1 {
|
||||
t.Fatalf("expected exactly one parsed call outside four-backtick fence, got %#v", res.Calls)
|
||||
|
||||
Reference in New Issue
Block a user