mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-18 23:25:10 +08:00
工具优化
This commit is contained in:
@@ -35,9 +35,10 @@ func consumeXMLToolCapture(captured string, toolNames []string) (prefix string,
|
||||
if openIdx < 0 {
|
||||
continue
|
||||
}
|
||||
// Find the LAST occurrence of the specific closing tag to get the outermost block.
|
||||
closeIdx := strings.LastIndex(lower, pair.close)
|
||||
if closeIdx < openIdx {
|
||||
// Find the matching closing tag outside CDATA. Long write-file tool
|
||||
// calls often contain XML examples in CDATA, including </tool_calls>.
|
||||
closeIdx := findXMLCloseOutsideCDATA(captured, pair.close, openIdx+len(pair.open))
|
||||
if closeIdx < 0 {
|
||||
// Opening tag is present but its specific closing tag hasn't arrived.
|
||||
// Return not-ready so we keep buffering until the canonical wrapper closes.
|
||||
return "", nil, "", false
|
||||
@@ -57,7 +58,7 @@ func consumeXMLToolCapture(captured string, toolNames []string) (prefix string,
|
||||
}
|
||||
if !strings.Contains(lower, "<tool_calls") {
|
||||
invokeIdx := strings.Index(lower, "<invoke")
|
||||
closeIdx := strings.LastIndex(lower, "</tool_calls>")
|
||||
closeIdx := findXMLCloseOutsideCDATA(captured, "</tool_calls>", invokeIdx)
|
||||
if invokeIdx >= 0 && closeIdx > invokeIdx {
|
||||
closeEnd := closeIdx + len("</tool_calls>")
|
||||
xmlBlock := "<tool_calls>" + captured[invokeIdx:closeIdx] + "</tool_calls>"
|
||||
@@ -79,8 +80,9 @@ func consumeXMLToolCapture(captured string, toolNames []string) (prefix string,
|
||||
func hasOpenXMLToolTag(captured string) bool {
|
||||
lower := strings.ToLower(captured)
|
||||
for _, pair := range xmlToolCallTagPairs {
|
||||
if strings.Contains(lower, pair.open) {
|
||||
if !strings.Contains(lower, pair.close) {
|
||||
openIdx := strings.Index(lower, pair.open)
|
||||
if openIdx >= 0 {
|
||||
if findXMLCloseOutsideCDATA(captured, pair.close, openIdx+len(pair.open)) < 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -88,6 +90,38 @@ func hasOpenXMLToolTag(captured string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func findXMLCloseOutsideCDATA(s, closeTag string, start int) int {
|
||||
if s == "" || closeTag == "" {
|
||||
return -1
|
||||
}
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
lower := strings.ToLower(s)
|
||||
target := strings.ToLower(closeTag)
|
||||
for i := start; i < len(s); {
|
||||
switch {
|
||||
case strings.HasPrefix(lower[i:], "<![cdata["):
|
||||
end := strings.Index(lower[i+len("<![cdata["):], "]]>")
|
||||
if end < 0 {
|
||||
return -1
|
||||
}
|
||||
i += len("<![cdata[") + end + len("]]>")
|
||||
case strings.HasPrefix(lower[i:], "<!--"):
|
||||
end := strings.Index(lower[i+len("<!--"):], "-->")
|
||||
if end < 0 {
|
||||
return -1
|
||||
}
|
||||
i += len("<!--") + end + len("-->")
|
||||
case strings.HasPrefix(lower[i:], target):
|
||||
return i
|
||||
default:
|
||||
i++
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// findPartialXMLToolTagStart checks if the string ends with a partial canonical
|
||||
// XML wrapper tag (e.g., "<too") and returns the position of the '<'.
|
||||
func findPartialXMLToolTagStart(s string) int {
|
||||
|
||||
@@ -84,6 +84,65 @@ func TestProcessToolSieveHandlesLongXMLToolCall(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessToolSieveKeepsCDATAEmbeddedToolClosingBuffered(t *testing.T) {
|
||||
var state State
|
||||
payload := strings.Join([]string{
|
||||
"# DS2API 4.0 更新内容",
|
||||
"",
|
||||
strings.Repeat("x", 4096),
|
||||
"```xml",
|
||||
"<tool_calls>",
|
||||
" <invoke name=\"demo\">",
|
||||
" <parameter name=\"value\">x</parameter>",
|
||||
" </invoke>",
|
||||
"</tool_calls>",
|
||||
"```",
|
||||
"tail",
|
||||
}, "\n")
|
||||
innerClose := strings.Index(payload, "</tool_calls>") + len("</tool_calls>")
|
||||
chunks := []string{
|
||||
"<tool_calls>\n <invoke name=\"Write\">\n <parameter name=\"content\"><![CDATA[",
|
||||
payload[:innerClose],
|
||||
payload[innerClose:],
|
||||
"]]></parameter>\n <parameter name=\"file_path\">DS2API-4.0-Release-Notes.md</parameter>\n </invoke>\n</tool_calls>",
|
||||
}
|
||||
|
||||
var events []Event
|
||||
for i, c := range chunks {
|
||||
next := ProcessChunk(&state, c, []string{"Write"})
|
||||
if i <= 1 {
|
||||
for _, evt := range next {
|
||||
if evt.Content != "" || len(evt.ToolCalls) > 0 {
|
||||
t.Fatalf("expected no events before outer closing tag, chunk=%d events=%#v", i, next)
|
||||
}
|
||||
}
|
||||
}
|
||||
events = append(events, next...)
|
||||
}
|
||||
events = append(events, Flush(&state, []string{"Write"})...)
|
||||
|
||||
var textContent strings.Builder
|
||||
var gotPayload string
|
||||
toolCalls := 0
|
||||
for _, evt := range events {
|
||||
textContent.WriteString(evt.Content)
|
||||
if len(evt.ToolCalls) > 0 {
|
||||
toolCalls += len(evt.ToolCalls)
|
||||
gotPayload, _ = evt.ToolCalls[0].Input["content"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if toolCalls != 1 {
|
||||
t.Fatalf("expected one parsed tool call, got %d events=%#v", toolCalls, events)
|
||||
}
|
||||
if textContent.Len() != 0 {
|
||||
t.Fatalf("expected no leaked text, got %q", textContent.String())
|
||||
}
|
||||
if gotPayload != payload {
|
||||
t.Fatalf("expected full CDATA payload to survive intact, got len=%d want=%d", len(gotPayload), len(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessToolSieveXMLWithLeadingText(t *testing.T) {
|
||||
var state State
|
||||
// Model outputs some prose then an XML tool call.
|
||||
|
||||
Reference in New Issue
Block a user