tools fix
This commit is contained in:
@@ -1600,6 +1600,9 @@ func scrubToolMarkers(text string) string {
|
||||
reEmptyFences := regexp.MustCompile("(?s)```(?:xml|json|ya?ml)?\\s*```")
|
||||
s = reEmptyFences.ReplaceAllString(s, "")
|
||||
|
||||
reCitationDisclaimer := regexp.MustCompile(`(?i)\*?Web evidence was retrieved[^\n*]*\.\*?`)
|
||||
s = reCitationDisclaimer.ReplaceAllString(s, "")
|
||||
|
||||
trimmed := strings.TrimSpace(s)
|
||||
if trimmed == "```xml" || trimmed == "```json" || trimmed == "```" {
|
||||
return ""
|
||||
@@ -1618,8 +1621,10 @@ func scrubToolMarkers(text string) string {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
func ExtractToolCallBlocks(content string) (blocks []string, remaining string) {
|
||||
func ExtractToolCallBlocks(content string) (blocks []string, preCallText string, remaining string) {
|
||||
remaining = content
|
||||
var firstPreamble string
|
||||
firstBlockFound := false
|
||||
|
||||
reDynamicOpen := regexp.MustCompile(`(?i)<(?:tool_calls?|toolCalls?|function_calls?|functionCalls?|invoke|call|command|commands|action|function)(?:\s+[^>]*)?>|<(?:call|tool_call|function_call):[a-zA-Z0-9_-]+(?:\s+[^>]*)?>|<function=[a-zA-Z0-9_-]+>|\[TOOL_CALLS?\]`)
|
||||
|
||||
@@ -1670,9 +1675,21 @@ func ExtractToolCallBlocks(content string) (blocks []string, remaining string) {
|
||||
before = reFenceOpen.ReplaceAllString(before, "")
|
||||
after = reFenceClose.ReplaceAllString(after, "")
|
||||
}
|
||||
if !firstBlockFound {
|
||||
preClean := reFenceOpen.ReplaceAllString(before, "")
|
||||
firstPreamble = strings.TrimRight(preClean, "\r\n ")
|
||||
firstBlockFound = true
|
||||
}
|
||||
remaining = strings.TrimSpace(before + after)
|
||||
} else {
|
||||
nextLoc := reDynamicOpen.FindStringIndex(rest)
|
||||
before := remaining[:sIdx]
|
||||
reFenceOpen := regexp.MustCompile("(?s)\\n?\\s*```(?:xml|json|ya?ml)?\\s*$")
|
||||
if !firstBlockFound {
|
||||
preClean := reFenceOpen.ReplaceAllString(before, "")
|
||||
firstPreamble = strings.TrimRight(preClean, "\r\n ")
|
||||
firstBlockFound = true
|
||||
}
|
||||
if nextLoc != nil {
|
||||
blockEndPos := loc[1] + nextLoc[0]
|
||||
blockText = remaining[sIdx:blockEndPos]
|
||||
@@ -1692,16 +1709,17 @@ func ExtractToolCallBlocks(content string) (blocks []string, remaining string) {
|
||||
blockText := remaining[loc[0]:loc[1]]
|
||||
before := remaining[:loc[0]]
|
||||
after := remaining[loc[1]:]
|
||||
firstPreamble = strings.TrimSpace(before)
|
||||
remaining = strings.TrimSpace(before + " " + after)
|
||||
blocks = append(blocks, blockText)
|
||||
}
|
||||
}
|
||||
|
||||
return blocks, remaining
|
||||
return blocks, firstPreamble, remaining
|
||||
}
|
||||
|
||||
func DetectToolCalls(content string) ([]ToolCall, string, bool) {
|
||||
blocks, remaining := ExtractToolCallBlocks(content)
|
||||
blocks, preCallText, _ := ExtractToolCallBlocks(content)
|
||||
var calls []ToolCall
|
||||
|
||||
for _, block := range blocks {
|
||||
@@ -1711,25 +1729,29 @@ func DetectToolCalls(content string) ([]ToolCall, string, bool) {
|
||||
}
|
||||
|
||||
if len(calls) > 0 {
|
||||
remaining = scrubToolMarkers(remaining)
|
||||
return calls, remaining, true
|
||||
return calls, scrubToolMarkers(preCallText), true
|
||||
}
|
||||
|
||||
reFenced := regexp.MustCompile("(?s)```(?:json|xml)?\\s*([\\s\\S]*?)\\s*```")
|
||||
matches := reFenced.FindAllStringSubmatchIndex(content, -1)
|
||||
if len(matches) > 0 {
|
||||
var fencedCalls []ToolCall
|
||||
rem := content
|
||||
firstFenceStart := -1
|
||||
for _, loc := range matches {
|
||||
fenceTotal := content[loc[0]:loc[1]]
|
||||
fenceInner := strings.TrimSpace(content[loc[2]:loc[3]])
|
||||
if tcs, ok := parseMultipleToolCalls(fenceInner); ok && len(tcs) > 0 {
|
||||
if firstFenceStart == -1 {
|
||||
firstFenceStart = loc[0]
|
||||
}
|
||||
fencedCalls = append(fencedCalls, tcs...)
|
||||
rem = strings.Replace(rem, fenceTotal, "", 1)
|
||||
}
|
||||
}
|
||||
if len(fencedCalls) > 0 {
|
||||
return fencedCalls, scrubToolMarkers(rem), true
|
||||
pre := ""
|
||||
if firstFenceStart > 0 {
|
||||
pre = content[:firstFenceStart]
|
||||
}
|
||||
return fencedCalls, scrubToolMarkers(pre), true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1744,22 +1766,33 @@ func DetectToolCalls(content string) ([]ToolCall, string, bool) {
|
||||
callLocs := reCallInContent.FindAllStringIndex(content, -1)
|
||||
if len(callLocs) > 0 {
|
||||
var pyCalls []ToolCall
|
||||
rem := content
|
||||
firstCallStart := -1
|
||||
for _, loc := range callLocs {
|
||||
callStr := strings.TrimSpace(content[loc[0]:loc[1]])
|
||||
if tc, ok := parsePythonFunctionCall(callStr); ok {
|
||||
if firstCallStart == -1 {
|
||||
firstCallStart = loc[0]
|
||||
}
|
||||
pyCalls = append(pyCalls, tc)
|
||||
rem = strings.Replace(rem, content[loc[0]:loc[1]], "", 1)
|
||||
}
|
||||
}
|
||||
if len(pyCalls) > 0 {
|
||||
return pyCalls, scrubToolMarkers(rem), true
|
||||
pre := ""
|
||||
if firstCallStart > 0 {
|
||||
pre = content[:firstCallStart]
|
||||
}
|
||||
return pyCalls, scrubToolMarkers(pre), true
|
||||
}
|
||||
}
|
||||
|
||||
if tc, ok := parseReActToolCall(content); ok {
|
||||
rem := scrubToolMarkers(content)
|
||||
return []ToolCall{tc}, rem, true
|
||||
reAction := regexp.MustCompile(`(?i)(?:Action|Command):\s*[a-zA-Z0-9_.-]+`)
|
||||
loc := reAction.FindStringIndex(content)
|
||||
pre := ""
|
||||
if loc != nil && loc[0] > 0 {
|
||||
pre = content[:loc[0]]
|
||||
}
|
||||
return []ToolCall{tc}, scrubToolMarkers(pre), true
|
||||
}
|
||||
|
||||
if tcs, ok := parseMultiplePythonFunctionCalls(trimmed); ok && len(tcs) > 0 {
|
||||
@@ -2363,7 +2396,7 @@ func (f *StreamToolCallFilter) Feed(chunk string, onContent func(string), onTool
|
||||
} else {
|
||||
before = strings.TrimRight(before, "\r\n")
|
||||
}
|
||||
if before != "" {
|
||||
if before != "" && !f.emittedCall {
|
||||
onContent(before)
|
||||
}
|
||||
f.inToolCall = true
|
||||
@@ -2385,7 +2418,7 @@ func (f *StreamToolCallFilter) Feed(chunk string, onContent func(string), onTool
|
||||
safe = reTrailingFence.ReplaceAllString(safe, "")
|
||||
safe = strings.TrimRight(safe, "\r\n")
|
||||
}
|
||||
if strings.TrimSpace(safe) != "" {
|
||||
if strings.TrimSpace(safe) != "" && !f.emittedCall {
|
||||
onContent(safe)
|
||||
}
|
||||
f.buf = f.buf[len(f.buf)-holdLen:]
|
||||
@@ -2393,7 +2426,9 @@ func (f *StreamToolCallFilter) Feed(chunk string, onContent func(string), onTool
|
||||
} else if len(f.buf) < 16 && strings.TrimSpace(f.buf) == "" {
|
||||
break
|
||||
} else {
|
||||
if !f.emittedCall {
|
||||
onContent(f.buf)
|
||||
}
|
||||
f.buf = ""
|
||||
break
|
||||
}
|
||||
@@ -2432,7 +2467,7 @@ func (f *StreamToolCallFilter) Feed(chunk string, onContent func(string), onTool
|
||||
f.toolIndex++
|
||||
f.emittedCall = true
|
||||
onToolCall(tc4)
|
||||
} else {
|
||||
} else if !f.emittedCall {
|
||||
onContent(f.activePair.Start + f.toolCallBuf + f.activePair.End)
|
||||
}
|
||||
f.toolCallBuf = ""
|
||||
@@ -2484,18 +2519,13 @@ func (f *StreamToolCallFilter) Flush(onContent func(string), onToolCall func(Too
|
||||
f.toolIndex++
|
||||
f.emittedCall = true
|
||||
onToolCall(tc4)
|
||||
} else {
|
||||
} else if !f.emittedCall {
|
||||
onContent(f.activePair.Start + f.toolCallBuf)
|
||||
}
|
||||
f.toolCallBuf = ""
|
||||
}
|
||||
if len(f.buf) > 0 {
|
||||
if f.emittedCall {
|
||||
clean := scrubToolMarkers(f.buf)
|
||||
if strings.TrimSpace(clean) != "" {
|
||||
onContent(strings.TrimSpace(clean))
|
||||
}
|
||||
} else {
|
||||
if !f.emittedCall {
|
||||
onContent(f.buf)
|
||||
}
|
||||
f.buf = ""
|
||||
@@ -2576,8 +2606,9 @@ type SpaceParamMapping struct {
|
||||
ComponentType string `json:"component_type,omitempty"`
|
||||
Label string `json:"label,omitempty"`
|
||||
ParamName string `json:"param_name,omitempty"`
|
||||
ParamType string `json:"param_type"` // "message", "history", "system_prompt", "temperature", "max_tokens", "top_p", "think_level", "tools", "stream", "state", "other"
|
||||
ParamType string `json:"param_type"` // "message", "history", "system_prompt", "temperature", "max_tokens", "top_p", "think_level", "tools", "stream", "state", "web_search", "other"
|
||||
DefaultValue interface{} `json:"default_value,omitempty"`
|
||||
Choices []string `json:"choices,omitempty"`
|
||||
}
|
||||
|
||||
type SpaceDiscovery struct {
|
||||
@@ -2609,6 +2640,7 @@ type SpaceDiscovery struct {
|
||||
ThinkLevelIndex int `json:"think_level_index"` // -1 if none
|
||||
FunctionsJSONIndex int `json:"functions_json_index"` // -1 if none
|
||||
PreservedThinkingIndex int `json:"preserved_thinking_index"` // -1 if none
|
||||
WebSearchIndex int `json:"web_search_index"` // -1 if none
|
||||
IsHunyuan3 bool `json:"is_hunyuan3"`
|
||||
HistoryFormat string `json:"history_format"` // "messages", "pairs", "gradio_messages", "none"
|
||||
ToolCallMode string `json:"tool_call_mode"` // "native_slot", "prompt_augmented_system", "prompt_augmented_first_turn", "prompt_augmented_single_prompt"
|
||||
@@ -2758,6 +2790,7 @@ func NewDefaultSpaceDiscovery(spaceURL string) *SpaceDiscovery {
|
||||
ThinkLevelIndex: -1,
|
||||
FunctionsJSONIndex: -1,
|
||||
PreservedThinkingIndex: -1,
|
||||
WebSearchIndex: -1,
|
||||
HistoryFormat: "messages",
|
||||
ToolCallMode: "prompt_augmented_single_prompt",
|
||||
LastDiscovered: time.Now(),
|
||||
@@ -3293,6 +3326,29 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
|
||||
discovery.DefaultInputs[idx] = val
|
||||
mapping.DefaultValue = val
|
||||
}
|
||||
if chList, ok := comp.Props["choices"].([]interface{}); ok {
|
||||
for _, ch := range chList {
|
||||
if chStr, ok := ch.(string); ok {
|
||||
mapping.Choices = append(mapping.Choices, chStr)
|
||||
} else if chPair, ok := ch.([]interface{}); ok && len(chPair) > 0 {
|
||||
if chStr, ok := chPair[0].(string); ok {
|
||||
mapping.Choices = append(mapping.Choices, chStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if bestEndpointInfo != nil && idx < len(bestEndpointInfo.Parameters) && len(mapping.Choices) == 0 {
|
||||
p := bestEndpointInfo.Parameters[idx]
|
||||
if typeMap, ok := p.Type.(map[string]interface{}); ok {
|
||||
if enumArr, ok := typeMap["enum"].([]interface{}); ok {
|
||||
for _, e := range enumArr {
|
||||
if s, ok := e.(string); ok {
|
||||
mapping.Choices = append(mapping.Choices, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(pName, "function") || strings.Contains(pName, "tool") || strings.Contains(pLabel, "tool") || strings.Contains(pLabel, "function") || strings.Contains(cLabel, "tool") || strings.Contains(cLabel, "function") {
|
||||
@@ -3335,6 +3391,9 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
|
||||
} else if strings.Contains(pName, "stream") || strings.Contains(cLabel, "stream") {
|
||||
mapping.ParamType = "stream"
|
||||
discovery.StreamIndex = idx
|
||||
} else if strings.Contains(pName, "search") || strings.Contains(pLabel, "search") || strings.Contains(cLabel, "search") || strings.Contains(pName, "browse") || strings.Contains(pLabel, "browse") || strings.Contains(cLabel, "browse") || strings.Contains(pName, "web") || strings.Contains(pLabel, "web") || strings.Contains(cLabel, "web") {
|
||||
mapping.ParamType = "web_search"
|
||||
discovery.WebSearchIndex = idx
|
||||
} else if cType == "state" {
|
||||
mapping.ParamType = "state"
|
||||
}
|
||||
@@ -3402,6 +3461,9 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
|
||||
if discovery.StreamIndex >= discovery.TotalInputs {
|
||||
discovery.StreamIndex = -1
|
||||
}
|
||||
if discovery.WebSearchIndex >= discovery.TotalInputs {
|
||||
discovery.WebSearchIndex = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3501,6 +3563,19 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
|
||||
} else if strings.Contains(pName, "stream") || strings.Contains(pLabel, "stream") {
|
||||
discovery.StreamIndex = idx
|
||||
mapping.ParamType = "stream"
|
||||
} else if strings.Contains(pName, "search") || strings.Contains(pLabel, "search") || strings.Contains(pName, "browse") || strings.Contains(pLabel, "browse") || strings.Contains(pName, "web") || strings.Contains(pLabel, "web") {
|
||||
discovery.WebSearchIndex = idx
|
||||
mapping.ParamType = "web_search"
|
||||
}
|
||||
|
||||
if typeMap, ok := p.Type.(map[string]interface{}); ok {
|
||||
if enumArr, ok := typeMap["enum"].([]interface{}); ok {
|
||||
for _, e := range enumArr {
|
||||
if s, ok := e.(string); ok {
|
||||
mapping.Choices = append(mapping.Choices, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
discovery.ParamMappings = append(discovery.ParamMappings, mapping)
|
||||
@@ -4043,6 +4118,38 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
|
||||
data[disc.FunctionsJSONIndex] = functionsJSONStr
|
||||
}
|
||||
|
||||
if disc.WebSearchIndex >= 0 && disc.WebSearchIndex != disc.MessageIndex && disc.WebSearchIndex != disc.HistoryIndex && disc.WebSearchIndex != disc.SystemIndex && disc.WebSearchIndex < len(data) && len(req.Tools) > 0 {
|
||||
var mapping *SpaceParamMapping
|
||||
for i := range disc.ParamMappings {
|
||||
if disc.ParamMappings[i].InputIndex == disc.WebSearchIndex {
|
||||
mapping = &disc.ParamMappings[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
disabledSet := false
|
||||
if mapping != nil && len(mapping.Choices) > 0 {
|
||||
for _, choice := range mapping.Choices {
|
||||
cLower := strings.ToLower(choice)
|
||||
if cLower == "direct" || cLower == "off" || cLower == "disabled" || cLower == "none" || cLower == "false" || cLower == "no" || strings.Contains(cLower, "direct") || strings.Contains(cLower, "no search") || strings.Contains(cLower, "disable") {
|
||||
data[disc.WebSearchIndex] = choice
|
||||
disabledSet = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !disabledSet {
|
||||
switch data[disc.WebSearchIndex].(type) {
|
||||
case bool:
|
||||
data[disc.WebSearchIndex] = false
|
||||
case string:
|
||||
strVal := strings.ToLower(data[disc.WebSearchIndex].(string))
|
||||
if strings.Contains(strVal, "search") {
|
||||
data[disc.WebSearchIndex] = "Direct"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
|
||||
+243
-6
@@ -149,8 +149,8 @@ func TestStreamToolCallFilter(t *testing.T) {
|
||||
t.Errorf("expected tool name 'search_web', got %q", toolCalls[0].Function.Name)
|
||||
}
|
||||
fullContent := strings.Join(contentParts, "")
|
||||
if fullContent != "Searching now: Done." {
|
||||
t.Errorf("expected 'Searching now: Done.', got %q", fullContent)
|
||||
if fullContent != "Searching now: " {
|
||||
t.Errorf("expected 'Searching now: ' (post-call text suppressed), got %q", fullContent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,8 +639,11 @@ Some postamble.`
|
||||
if strings.Contains(rem2, "function_call") {
|
||||
t.Errorf("expected tag stripped from remaining, got %q", rem2)
|
||||
}
|
||||
if !strings.Contains(rem2, "Some preamble") || !strings.Contains(rem2, "Some postamble") {
|
||||
t.Errorf("expected surrounding text preserved in remaining, got %q", rem2)
|
||||
if !strings.Contains(rem2, "Some preamble") {
|
||||
t.Errorf("expected preamble text preserved in remaining, got %q", rem2)
|
||||
}
|
||||
if strings.Contains(rem2, "Some postamble") {
|
||||
t.Errorf("expected postamble text discarded from remaining on tool call, got %q", rem2)
|
||||
}
|
||||
|
||||
// 3. [TOOL_CALLS] bracket syntax
|
||||
@@ -723,8 +726,8 @@ func TestUniversalStreamToolCallFilterVariants(t *testing.T) {
|
||||
if strings.Contains(fullContent, "TOOL_CALLS") {
|
||||
t.Errorf("tag leaked into stream content: %q", fullContent)
|
||||
}
|
||||
if fullContent != "Preamble text: Completed." {
|
||||
t.Errorf("unexpected streamed content: %q", fullContent)
|
||||
if fullContent != "Preamble text: " {
|
||||
t.Errorf("unexpected streamed content (expected post-call text suppressed): %q", fullContent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3367,3 +3370,237 @@ func TestHunyuan3CallAndStreamingNoInterleaving(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectToolCallsDiscardsPostambleAndCitationDisclaimer(t *testing.T) {
|
||||
raw := `<tool_call>
|
||||
{"name": "get_current_weather", "arguments": {"location": "Tokyo"}}
|
||||
</tool_call>
|
||||
|
||||
*Web evidence was retrieved, but the response did not include a valid source citation.*`
|
||||
|
||||
tcs, rem, ok := DetectToolCalls(raw)
|
||||
if !ok || len(tcs) != 1 {
|
||||
t.Fatalf("expected 1 tool call, got %d (ok: %v)", len(tcs), ok)
|
||||
}
|
||||
if tcs[0].Function.Name != "get_current_weather" {
|
||||
t.Errorf("expected function get_current_weather, got %q", tcs[0].Function.Name)
|
||||
}
|
||||
if rem != "" {
|
||||
t.Errorf("expected empty remaining content, got %q", rem)
|
||||
}
|
||||
|
||||
frame := GradioOutputFrame{Content: raw}
|
||||
finalContent, reasoning, finalCalls, finishReason := finalizeOutput(frame)
|
||||
if finishReason != "tool_calls" {
|
||||
t.Errorf("expected finish_reason 'tool_calls', got %q", finishReason)
|
||||
}
|
||||
if finalContent != nil {
|
||||
t.Errorf("expected finalContent to be nil, got %v", finalContent)
|
||||
}
|
||||
if reasoning != "" {
|
||||
t.Errorf("expected empty reasoning, got %q", reasoning)
|
||||
}
|
||||
if len(finalCalls) != 1 {
|
||||
t.Fatalf("expected 1 final tool call, got %d", len(finalCalls))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectToolCallsPreservesPreambleBeforeToolCall(t *testing.T) {
|
||||
raw := "I will check the weather in Tokyo for you.\n" +
|
||||
"```xml\n" +
|
||||
"<tool_call>\n" +
|
||||
"{\"name\": \"get_current_weather\", \"arguments\": {\"location\": \"Tokyo\"}}\n" +
|
||||
"</tool_call>\n" +
|
||||
"```\n" +
|
||||
"*Web evidence was retrieved, but the response did not include a valid source citation.*"
|
||||
|
||||
tcs, rem, ok := DetectToolCalls(raw)
|
||||
if !ok || len(tcs) != 1 {
|
||||
t.Fatalf("expected 1 tool call, got %d (ok: %v)", len(tcs), ok)
|
||||
}
|
||||
if tcs[0].Function.Name != "get_current_weather" {
|
||||
t.Errorf("expected function get_current_weather, got %q", tcs[0].Function.Name)
|
||||
}
|
||||
if rem != "I will check the weather in Tokyo for you." {
|
||||
t.Errorf("expected preamble preserved without postamble, got %q", rem)
|
||||
}
|
||||
|
||||
frame := GradioOutputFrame{Content: raw}
|
||||
finalContent, _, finalCalls, finishReason := finalizeOutput(frame)
|
||||
if finishReason != "tool_calls" {
|
||||
t.Errorf("expected finish_reason 'tool_calls', got %q", finishReason)
|
||||
}
|
||||
if finalContentStr, ok := finalContent.(string); !ok || finalContentStr != "I will check the weather in Tokyo for you." {
|
||||
t.Errorf("expected finalContent 'I will check the weather in Tokyo for you.', got %v", finalContent)
|
||||
}
|
||||
if len(finalCalls) != 1 {
|
||||
t.Fatalf("expected 1 tool call, got %d", len(finalCalls))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamToolCallFilterPostCallLeakSuppression(t *testing.T) {
|
||||
// Test 1: Tool call followed by citation disclaimer
|
||||
filter1 := NewStreamToolCallFilter()
|
||||
var contentParts1 []string
|
||||
var toolCalls1 []ToolCall
|
||||
|
||||
onContent1 := func(s string) { contentParts1 = append(contentParts1, s) }
|
||||
onToolCall1 := func(tc ToolCall) { toolCalls1 = append(toolCalls1, tc) }
|
||||
|
||||
chunks1 := []string{
|
||||
"<tool_call>\n",
|
||||
"{\"name\": \"get_current_weather\", \"arguments\": {\"location\": \"Tokyo\"}}\n",
|
||||
"</tool_call>\n",
|
||||
"\n*Web evidence was retrieved, but the response did not include a valid source citation.*",
|
||||
}
|
||||
|
||||
for _, c := range chunks1 {
|
||||
filter1.Feed(c, onContent1, onToolCall1)
|
||||
}
|
||||
filter1.Flush(onContent1, onToolCall1)
|
||||
|
||||
if len(toolCalls1) != 1 {
|
||||
t.Fatalf("expected 1 tool call, got %d", len(toolCalls1))
|
||||
}
|
||||
if toolCalls1[0].Function.Name != "get_current_weather" {
|
||||
t.Errorf("expected get_current_weather, got %q", toolCalls1[0].Function.Name)
|
||||
}
|
||||
if len(contentParts1) > 0 {
|
||||
t.Errorf("expected 0 content parts leaked after tool call, got %v", contentParts1)
|
||||
}
|
||||
|
||||
// Test 2: Parallel tool calls followed by trailing residue
|
||||
filter2 := NewStreamToolCallFilter()
|
||||
var contentParts2 []string
|
||||
var toolCalls2 []ToolCall
|
||||
|
||||
onContent2 := func(s string) { contentParts2 = append(contentParts2, s) }
|
||||
onToolCall2 := func(tc ToolCall) { toolCalls2 = append(toolCalls2, tc) }
|
||||
|
||||
chunks2 := []string{
|
||||
"<tool_call>{\"name\": \"call_a\", \"arguments\": {}}</tool_call>\n",
|
||||
"<tool_call>{\"name\": \"call_b\", \"arguments\": {}}</tool_call>\n",
|
||||
"Residual text after parallel calls that should be suppressed.",
|
||||
}
|
||||
|
||||
for _, c := range chunks2 {
|
||||
filter2.Feed(c, onContent2, onToolCall2)
|
||||
}
|
||||
filter2.Flush(onContent2, onToolCall2)
|
||||
|
||||
if len(toolCalls2) != 2 {
|
||||
t.Fatalf("expected 2 tool calls, got %d", len(toolCalls2))
|
||||
}
|
||||
if toolCalls2[0].Function.Name != "call_a" || toolCalls2[1].Function.Name != "call_b" {
|
||||
t.Errorf("unexpected tool calls: %+v", toolCalls2)
|
||||
}
|
||||
if len(contentParts2) > 0 {
|
||||
t.Errorf("expected 0 content parts leaked, got %v", contentParts2)
|
||||
}
|
||||
|
||||
// Test 3: Preamble + Tool call + Postamble
|
||||
filter3 := NewStreamToolCallFilter()
|
||||
var contentParts3 []string
|
||||
var toolCalls3 []ToolCall
|
||||
|
||||
onContent3 := func(s string) { contentParts3 = append(contentParts3, s) }
|
||||
onToolCall3 := func(tc ToolCall) { toolCalls3 = append(toolCalls3, tc) }
|
||||
|
||||
chunks3 := []string{
|
||||
"Thinking about your query: ",
|
||||
"<tool_call>{\"name\": \"search\", \"arguments\": {\"q\": \"go\"}}</tool_call>",
|
||||
" Trailing hallucinated answer that must not be emitted.",
|
||||
}
|
||||
|
||||
for _, c := range chunks3 {
|
||||
filter3.Feed(c, onContent3, onToolCall3)
|
||||
}
|
||||
filter3.Flush(onContent3, onToolCall3)
|
||||
|
||||
if len(toolCalls3) != 1 {
|
||||
t.Fatalf("expected 1 tool call, got %d", len(toolCalls3))
|
||||
}
|
||||
fullContent3 := strings.Join(contentParts3, "")
|
||||
if fullContent3 != "Thinking about your query: " {
|
||||
t.Errorf("expected preamble 'Thinking about your query: ', got %q", fullContent3)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebSearchParameterMappingAndSuppression(t *testing.T) {
|
||||
gw := &GradioGateway{}
|
||||
|
||||
// 1. Radio search_mode with choices ["Auto search", "Always search", "Direct"]
|
||||
discRadio := NewDefaultSpaceDiscovery("https://test-search-space.hf.space")
|
||||
discRadio.TotalInputs = 3
|
||||
discRadio.MessageIndex = 0
|
||||
discRadio.HistoryIndex = 1
|
||||
discRadio.WebSearchIndex = 2
|
||||
discRadio.ParamMappings = []SpaceParamMapping{
|
||||
{InputIndex: 0, ComponentType: "textbox", Label: "message", ParamType: "message"},
|
||||
{InputIndex: 1, ComponentType: "chatbot", Label: "history", ParamType: "history"},
|
||||
{
|
||||
InputIndex: 2,
|
||||
ComponentType: "radio",
|
||||
Label: "search_mode",
|
||||
ParamName: "search_mode",
|
||||
ParamType: "web_search",
|
||||
DefaultValue: "Auto search",
|
||||
Choices: []string{"Auto search", "Always search", "Direct"},
|
||||
},
|
||||
}
|
||||
discRadio.DefaultInputs = []interface{}{"", nil, "Auto search"}
|
||||
|
||||
// Normal request without tools retains space default ("Auto search")
|
||||
reqNoTools := ChatCompletionRequest{
|
||||
Model: "test-model",
|
||||
Messages: []ChatMessage{
|
||||
{Role: "user", Content: "Hello world"},
|
||||
},
|
||||
}
|
||||
dataNoTools, err := gw.BuildGradioPayload(discRadio, reqNoTools)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build payload without tools: %v", err)
|
||||
}
|
||||
if dataNoTools[2] != "Auto search" {
|
||||
t.Errorf("expected 'Auto search' default retained, got %v", dataNoTools[2])
|
||||
}
|
||||
|
||||
// Request with explicit tools disables web search ("Direct")
|
||||
reqWithTools := ChatCompletionRequest{
|
||||
Model: "test-model",
|
||||
Tools: []Tool{
|
||||
{Type: "function", Function: map[string]interface{}{"name": "get_weather"}},
|
||||
},
|
||||
Messages: []ChatMessage{
|
||||
{Role: "user", Content: "Weather in Tokyo?"},
|
||||
},
|
||||
}
|
||||
dataWithTools, err := gw.BuildGradioPayload(discRadio, reqWithTools)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build payload with tools: %v", err)
|
||||
}
|
||||
if dataWithTools[2] != "Direct" {
|
||||
t.Errorf("expected search_mode disabled to 'Direct', got %v", dataWithTools[2])
|
||||
}
|
||||
|
||||
// 2. Checkbox web_search with bool default
|
||||
discCheck := NewDefaultSpaceDiscovery("https://test-check-space.hf.space")
|
||||
discCheck.TotalInputs = 2
|
||||
discCheck.MessageIndex = 0
|
||||
discCheck.WebSearchIndex = 1
|
||||
discCheck.ParamMappings = []SpaceParamMapping{
|
||||
{InputIndex: 0, ComponentType: "textbox", Label: "message", ParamType: "message"},
|
||||
{InputIndex: 1, ComponentType: "checkbox", Label: "enable_web_search", ParamType: "web_search", DefaultValue: true},
|
||||
}
|
||||
discCheck.DefaultInputs = []interface{}{"", true}
|
||||
|
||||
dataCheckNoTools, _ := gw.BuildGradioPayload(discCheck, reqNoTools)
|
||||
if dataCheckNoTools[1] != true {
|
||||
t.Errorf("expected bool true retained without tools, got %v", dataCheckNoTools[1])
|
||||
}
|
||||
|
||||
dataCheckWithTools, _ := gw.BuildGradioPayload(discCheck, reqWithTools)
|
||||
if dataCheckWithTools[1] != false {
|
||||
t.Errorf("expected bool false set with tools, got %v", dataCheckWithTools[1])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user