fix(tools): adapt tool prompt framing to ensure reliable tool calls in persona spaces

This commit is contained in:
Luxferre
2026-09-07 10:36:18 +03:00
parent b599038881
commit 4de3ab3646
2 changed files with 89 additions and 33 deletions
+52 -28
View File
@@ -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:
<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
</tools>
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 <tool_call> block formatted as:
Syntax:
<tool_call>
{"name": "%s", "arguments": {...}}
</tool_call>
## 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 <tool_call> block. Do not add introductory text, commentary, or conversational filler.
3. If multiple tools are required, output each in its own <tool_call> 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 <tool_response> 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 <tool_call> 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()
}
}
+37 -5
View File
@@ -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)
}
}