feat: Implement streaming incremental tool call deltas with a new tool sieve and standalone parser.

This commit is contained in:
CJACK
2026-02-18 16:10:35 +08:00
parent 19289c9008
commit 7beeea5779
9 changed files with 1324 additions and 114 deletions

View File

@@ -33,6 +33,36 @@ func ParseToolCalls(text string, availableToolNames []string) []ParsedToolCall {
return nil
}
return filterToolCalls(parsed, availableToolNames)
}
func ParseStandaloneToolCalls(text string, availableToolNames []string) []ParsedToolCall {
trimmed := strings.TrimSpace(text)
if trimmed == "" {
return nil
}
candidates := []string{trimmed}
if strings.HasPrefix(trimmed, "```") && strings.HasSuffix(trimmed, "```") {
if m := fencedJSONPattern.FindStringSubmatch(trimmed); len(m) >= 2 {
candidates = append(candidates, strings.TrimSpace(m[1]))
}
}
for _, candidate := range candidates {
candidate = strings.TrimSpace(candidate)
if candidate == "" {
continue
}
if !strings.HasPrefix(candidate, "{") && !strings.HasPrefix(candidate, "[") {
continue
}
if parsed := parseToolCallsPayload(candidate); len(parsed) > 0 {
return filterToolCalls(parsed, availableToolNames)
}
}
return nil
}
func filterToolCalls(parsed []ParsedToolCall, availableToolNames []string) []ParsedToolCall {
allowed := map[string]struct{}{}
for _, name := range availableToolNames {
allowed[name] = struct{}{}

View File

@@ -62,3 +62,16 @@ func TestFormatOpenAIToolCalls(t *testing.T) {
t.Fatalf("unexpected function name: %#v", fn)
}
}
func TestParseStandaloneToolCallsOnlyMatchesStandalonePayload(t *testing.T) {
mixed := `这里是示例:{"tool_calls":[{"name":"search","input":{"q":"go"}}]}`
if calls := ParseStandaloneToolCalls(mixed, []string{"search"}); len(calls) != 0 {
t.Fatalf("expected standalone parser to ignore mixed prose, got %#v", calls)
}
standalone := `{"tool_calls":[{"name":"search","input":{"q":"go"}}]}`
calls := ParseStandaloneToolCalls(standalone, []string{"search"})
if len(calls) != 1 {
t.Fatalf("expected standalone parser to match, got %#v", calls)
}
}