fix: detect loose functionCall keys in tool sieve

This commit is contained in:
CJACK.
2026-04-02 15:19:45 +08:00
parent 15bf77e044
commit 02fe3e4bfc
2 changed files with 55 additions and 1 deletions

View File

@@ -4,7 +4,22 @@ import "strings"
func findQuotedFunctionCallKeyStart(s string) int {
lower := strings.ToLower(s)
const key = "\"functioncall\""
quotedIdx := findFunctionCallKeyStart(lower, `"functioncall"`)
baretIdx := findFunctionCallKeyStart(lower, "functioncall")
switch {
case quotedIdx < 0:
return baretIdx
case baretIdx < 0:
return quotedIdx
case quotedIdx < baretIdx:
return quotedIdx
default:
return baretIdx
}
}
func findFunctionCallKeyStart(lower, key string) int {
for from := 0; from < len(lower); {
rel := strings.Index(lower[from:], key)
if rel < 0 {
@@ -15,6 +30,10 @@ func findQuotedFunctionCallKeyStart(s string) int {
from = idx + 1
continue
}
if !hasJSONKeyBoundary(lower, idx, len(key)) {
from = idx + 1
continue
}
j := idx + len(key)
for j < len(lower) && (lower[j] == ' ' || lower[j] == '\t' || lower[j] == '\r' || lower[j] == '\n') {
j++
@@ -30,3 +49,23 @@ func findQuotedFunctionCallKeyStart(s string) int {
func hasJSONObjectContextPrefix(prefix string) bool {
return strings.LastIndex(prefix, "{") >= 0
}
func hasJSONKeyBoundary(s string, idx, keyLen int) bool {
if idx > 0 {
prev := s[idx-1]
if isLowerAlphaNumeric(prev) {
return false
}
}
if end := idx + keyLen; end < len(s) {
next := s[end]
if isLowerAlphaNumeric(next) {
return false
}
}
return true
}
func isLowerAlphaNumeric(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9') || b == '_'
}

View File

@@ -135,6 +135,21 @@ func TestFindToolSegmentStartDetectsQuotedFunctionCallKey(t *testing.T) {
}
}
func TestFindToolSegmentStartDetectsLooseFunctionCallKey(t *testing.T) {
input := `prefix {functionCall: {"name":"search_web","args":{"query":"x"}}}`
want := strings.Index(input, "{")
if got := findToolSegmentStart(input); got != want {
t.Fatalf("expected JSON object start %d, got %d", want, got)
}
}
func TestFindToolSegmentStartIgnoresLooseFunctionCallProse(t *testing.T) {
input := "Please explain why functionCall: is used in documentation examples."
if got := findToolSegmentStart(input); got != -1 {
t.Fatalf("expected no tool segment start for prose, got %d", got)
}
}
func TestProcessToolSieveDoesNotBufferFunctionCallProse(t *testing.T) {
var state toolStreamSieveState
chunk := "Please explain the functionCall API field and keep streaming this sentence."