From 4de3ab3646830d67dedbdeacf619d6899f69a5d1 Mon Sep 17 00:00:00 2001 From: Luxferre Date: Mon, 7 Sep 2026 10:36:18 +0300 Subject: [PATCH] fix(tools): adapt tool prompt framing to ensure reliable tool calls in persona spaces --- gr2gw.go | 80 +++++++++++++++++++++++++++++++++------------------ gr2gw_test.go | 42 +++++++++++++++++++++++---- 2 files changed, 89 insertions(+), 33 deletions(-) diff --git a/gr2gw.go b/gr2gw.go index 505f3a7..32448e2 100644 --- a/gr2gw.go +++ b/gr2gw.go @@ -436,10 +436,13 @@ func EffectiveUserAgent(r *http.Request) string { // Tool and message processing // --------------------------------------------------------------------------- -func BuildToolInstruction(tools []Tool) string { +func BuildToolInstruction(tools []Tool, toolChoice interface{}) string { if len(tools) == 0 { return "" } + if tcStr, ok := toolChoice.(string); ok && tcStr == "none" { + return "" + } toolsBytes, _ := json.MarshalIndent(tools, "", " ") sampleFnName := "" @@ -452,33 +455,30 @@ func BuildToolInstruction(tools []Tool) string { } } if sampleFnName == "" { - sampleFnName = "example_tool" + sampleFnName = "function_name" } - return fmt.Sprintf(`# Tool Calling Instructions + directive := "If a tool is relevant, emit the tool call XML. If no tools are relevant, answer the query directly." + if tcStr, ok := toolChoice.(string); ok && tcStr == "required" { + directive = "You MUST call one of the available tools for this query and emit the tool call XML." + } else if tcMap, ok := toolChoice.(map[string]interface{}); ok { + if fnMap, ok := tcMap["function"].(map[string]interface{}); ok { + if fnName, ok := fnMap["name"].(string); ok && fnName != "" { + directive = fmt.Sprintf("You MUST call the %s tool for this query and emit the tool call XML.", fnName) + } + } + } -You are equipped with external tools to assist with user queries. -You have access to the following tools: - + return fmt.Sprintf(`You are an API router and assistant. Convert the user query into the appropriate tool call XML using the available tools. +Available Tools: %s - -When the user asks a question or makes a request that can be fulfilled, assisted, or answered using any of the tools above, you MUST call the appropriate tool. -DO NOT refuse to answer, and DO NOT claim that you lack real-time access, live data, or tool capabilities when a tool is provided for that purpose. - -## Tool Calling Syntax -To call a tool, you MUST output a block formatted as: +Syntax: {"name": "%s", "arguments": {...}} -## Rules: -1. If an available tool is relevant to the user's request, invoking the tool is MANDATORY. -2. When calling a tool, your ENTIRE output must consist ONLY of the block. Do not add introductory text, commentary, or conversational filler. -3. If multiple tools are required, output each in its own block. -4. Arguments must be a valid JSON object strictly matching the tool's parameter definitions. -5. If no tools are relevant to the user's inquiry, respond normally with plain text. -6. When tool execution results are provided to you in subsequent turns (via blocks or tool role messages), formulate your final answer to the user based on those results.`, string(toolsBytes), sampleFnName) +%s`, string(toolsBytes), sampleFnName, directive) } func TransformMessages(req ChatCompletionRequest) (processed []ChatMessage, toolInstruction string, hasSystem bool) { @@ -492,7 +492,7 @@ func TransformMessages(req ChatCompletionRequest) (processed []ChatMessage, tool } } - toolInstruction = BuildToolInstruction(req.Tools) + toolInstruction = BuildToolInstruction(req.Tools, req.ToolChoice) // 2. Process and coalesce messages preserving turn parity var staged []ChatMessage @@ -2130,7 +2130,11 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet if disc.SystemIndex == -1 && systemPromptStr != "" && len(nonSystem) > 0 { // If the space supports conversation history, prepend system instructions to the first turn if disc.HistoryIndex != -1 { - nonSystem[0].Content = systemPromptStr + "\n\n" + nonSystem[0].GetContentString() + if toolInstruction != "" && len(nonSystem) == 1 { + nonSystem[0].Content = fmt.Sprintf("%s\n\nQuery: %s", systemPromptStr, nonSystem[0].GetContentString()) + } else { + nonSystem[0].Content = systemPromptStr + "\n\n" + nonSystem[0].GetContentString() + } } } @@ -2195,9 +2199,9 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet lastUserMessage = "Please proceed based on the tool results." } else { if toolName != "" { - lastUserMessage = fmt.Sprintf("Tool result for %s: %s", toolName, lastContent) + lastUserMessage = fmt.Sprintf("Tool result for %s: %s\nPlease answer the user's request based on the tool result.", toolName, lastContent) } else { - lastUserMessage = lastContent + lastUserMessage = fmt.Sprintf("Tool result: %s\nPlease answer the user's request based on the tool result.", lastContent) } } } else { @@ -2209,16 +2213,32 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet var promptMessageText string if disc.HistoryIndex != -1 { - if disc.SystemIndex == -1 && len(nonSystem) > 1 && toolInstruction != "" { - promptMessageText = fmt.Sprintf("[System Directive: Tool calling mode active. If relevant, output a block.]\n\n%s", lastUserMessage) + if disc.SystemIndex == -1 { + if len(nonSystem) > 1 && toolInstruction != "" { + if !strings.HasPrefix(lastUserMessage, "Tool result") { + promptMessageText = fmt.Sprintf("[System Directive: Tool calling mode active.]\n\nQuery: %s", lastUserMessage) + } else { + promptMessageText = fmt.Sprintf("[System Directive: Tool calling mode active.]\n\n%s", lastUserMessage) + } + } else { + promptMessageText = lastUserMessage + } } else { - promptMessageText = lastUserMessage + if toolInstruction != "" && !strings.HasPrefix(lastUserMessage, "Tool result") { + promptMessageText = fmt.Sprintf("Query: %s", lastUserMessage) + } else { + promptMessageText = lastUserMessage + } } } else { // Single message space: compose multi-turn history into the prompt if len(nonSystem) <= 1 { if systemPromptStr != "" && len(nonSystem) == 1 { - promptMessageText = systemPromptStr + "\n\n" + lastUserMessage + if toolInstruction != "" && !strings.HasPrefix(lastUserMessage, "Tool result") { + promptMessageText = fmt.Sprintf("%s\n\nQuery: %s", systemPromptStr, lastUserMessage) + } else { + promptMessageText = systemPromptStr + "\n\n" + lastUserMessage + } } else if systemPromptStr != "" { promptMessageText = systemPromptStr } else { @@ -2242,7 +2262,11 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet if len(nonSystem) > 0 && nonSystem[len(nonSystem)-1].Role == "assistant" { lastRoleLabel = "Assistant" } - sb.WriteString(fmt.Sprintf("# Current Request\n%s: %s", lastRoleLabel, lastUserMessage)) + if toolInstruction != "" && lastRoleLabel == "User" && !strings.HasPrefix(lastUserMessage, "Tool result") { + sb.WriteString(fmt.Sprintf("# Current Request\nQuery: %s", lastUserMessage)) + } else { + sb.WriteString(fmt.Sprintf("# Current Request\n%s: %s", lastRoleLabel, lastUserMessage)) + } promptMessageText = sb.String() } } diff --git a/gr2gw_test.go b/gr2gw_test.go index dd61744..f20fae7 100644 --- a/gr2gw_test.go +++ b/gr2gw_test.go @@ -534,7 +534,7 @@ func TestUniversalToolCallingTransformMessages(t *testing.T) { t.Fatalf("expected 4 processed messages, got %d", len(processed)) } - if processed[0].Role != "system" || !strings.Contains(processed[0].GetContentString(), "Tool Calling Instructions") { + if processed[0].Role != "system" || !strings.Contains(processed[0].GetContentString(), "API router") { t.Errorf("unexpected message 0: %+v", processed[0]) } @@ -701,7 +701,7 @@ func TestBuildGradioPayloadGenericSpaces(t *testing.T) { t.Fatalf("failed to build payload 1: %v", err) } sysStr, ok := data1[0].(string) - if !ok || !strings.Contains(sysStr, "Tool Calling Instructions") { + if !ok || !strings.Contains(sysStr, "API router") { t.Errorf("expected system prompt at index 0, got %v", data1[0]) } msgStr, ok := data1[1].(string) @@ -733,7 +733,7 @@ func TestBuildGradioPayloadGenericSpaces(t *testing.T) { t.Fatalf("expected 1 history pair at index 1, got %T (%v)", data2[1], data2[1]) } // Instructions prepended to the first user turn: - if !strings.Contains(pairs2[0][0], "Tool Calling Instructions") || !strings.Contains(pairs2[0][0], "What is 10+10?") { + if !strings.Contains(pairs2[0][0], "API router") || !strings.Contains(pairs2[0][0], "What is 10+10?") { t.Errorf("expected system instructions prepended to first pair user message, got: %q", pairs2[0][0]) } if !strings.Contains(pairs2[0][1], "lookup") { @@ -1249,7 +1249,7 @@ func TestSystemPromptAugmentationWithoutNativeToolCalling(t *testing.T) { if !strings.Contains(sysStr, "You are a specialized documentation bot.") { t.Errorf("expected default system prompt to be retained, got: %s", sysStr) } - if !strings.Contains(sysStr, "Tool Calling Instructions") || !strings.Contains(sysStr, "search_docs") { + if !strings.Contains(sysStr, "API router") || !strings.Contains(sysStr, "search_docs") { t.Errorf("expected tool calling instructions and function name in system prompt, got: %s", sysStr) } @@ -1291,10 +1291,42 @@ func TestSystemPromptAugmentationWithoutNativeToolCalling(t *testing.T) { if !ok { t.Fatalf("expected string at index 0, got %T", payload3[0]) } - if !strings.Contains(singleMsg, "Tool Calling Instructions") || !strings.Contains(singleMsg, "How to configure SSL?") { + if !strings.Contains(singleMsg, "API router") || !strings.Contains(singleMsg, "How to configure SSL?") { t.Errorf("expected single message to contain augmented instructions and user query, got: %s", singleMsg) } } +func TestToolChoiceHandling(t *testing.T) { + tools := []Tool{ + { + Type: "function", + Function: map[string]interface{}{ + "name": "calculator", + "description": "Evaluate math expression", + }, + }, + } + // 1. ToolChoice: "none" -> no tool instruction generated + instrNone := BuildToolInstruction(tools, "none") + if instrNone != "" { + t.Errorf("expected empty instruction for tool_choice: none, got: %s", instrNone) + } + // 2. ToolChoice: "required" -> mandatory directive + instrReq := BuildToolInstruction(tools, "required") + if !strings.Contains(instrReq, "You MUST call one of the available tools") { + t.Errorf("expected required directive in instruction, got: %s", instrReq) + } + + // 3. ToolChoice: specific function + instrFn := BuildToolInstruction(tools, map[string]interface{}{ + "type": "function", + "function": map[string]interface{}{ + "name": "calculator", + }, + }) + if !strings.Contains(instrFn, "You MUST call the calculator tool") { + t.Errorf("expected specific function directive in instruction, got: %s", instrFn) + } +}