fix(tools): adapt tool prompt framing to ensure reliable tool calls in persona spaces
This commit is contained in:
@@ -436,10 +436,13 @@ func EffectiveUserAgent(r *http.Request) string {
|
|||||||
// Tool and message processing
|
// Tool and message processing
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func BuildToolInstruction(tools []Tool) string {
|
func BuildToolInstruction(tools []Tool, toolChoice interface{}) string {
|
||||||
if len(tools) == 0 {
|
if len(tools) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
if tcStr, ok := toolChoice.(string); ok && tcStr == "none" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
toolsBytes, _ := json.MarshalIndent(tools, "", " ")
|
toolsBytes, _ := json.MarshalIndent(tools, "", " ")
|
||||||
|
|
||||||
sampleFnName := ""
|
sampleFnName := ""
|
||||||
@@ -452,33 +455,30 @@ func BuildToolInstruction(tools []Tool) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if sampleFnName == "" {
|
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.
|
return fmt.Sprintf(`You are an API router and assistant. Convert the user query into the appropriate tool call XML using the available tools.
|
||||||
You have access to the following tools:
|
Available Tools:
|
||||||
<tools>
|
|
||||||
%s
|
%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.
|
Syntax:
|
||||||
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:
|
|
||||||
<tool_call>
|
<tool_call>
|
||||||
{"name": "%s", "arguments": {...}}
|
{"name": "%s", "arguments": {...}}
|
||||||
</tool_call>
|
</tool_call>
|
||||||
|
|
||||||
## Rules:
|
%s`, string(toolsBytes), sampleFnName, directive)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TransformMessages(req ChatCompletionRequest) (processed []ChatMessage, toolInstruction string, hasSystem bool) {
|
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
|
// 2. Process and coalesce messages preserving turn parity
|
||||||
var staged []ChatMessage
|
var staged []ChatMessage
|
||||||
@@ -2130,7 +2130,11 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
|
|||||||
if disc.SystemIndex == -1 && systemPromptStr != "" && len(nonSystem) > 0 {
|
if disc.SystemIndex == -1 && systemPromptStr != "" && len(nonSystem) > 0 {
|
||||||
// If the space supports conversation history, prepend system instructions to the first turn
|
// If the space supports conversation history, prepend system instructions to the first turn
|
||||||
if disc.HistoryIndex != -1 {
|
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."
|
lastUserMessage = "Please proceed based on the tool results."
|
||||||
} else {
|
} else {
|
||||||
if toolName != "" {
|
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 {
|
} else {
|
||||||
lastUserMessage = lastContent
|
lastUserMessage = fmt.Sprintf("Tool result: %s\nPlease answer the user's request based on the tool result.", lastContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -2209,16 +2213,32 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
|
|||||||
|
|
||||||
var promptMessageText string
|
var promptMessageText string
|
||||||
if disc.HistoryIndex != -1 {
|
if disc.HistoryIndex != -1 {
|
||||||
if disc.SystemIndex == -1 && len(nonSystem) > 1 && toolInstruction != "" {
|
if disc.SystemIndex == -1 {
|
||||||
promptMessageText = fmt.Sprintf("[System Directive: Tool calling mode active. If relevant, output a <tool_call> block.]\n\n%s", lastUserMessage)
|
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 {
|
} else {
|
||||||
promptMessageText = lastUserMessage
|
if toolInstruction != "" && !strings.HasPrefix(lastUserMessage, "Tool result") {
|
||||||
|
promptMessageText = fmt.Sprintf("Query: %s", lastUserMessage)
|
||||||
|
} else {
|
||||||
|
promptMessageText = lastUserMessage
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Single message space: compose multi-turn history into the prompt
|
// Single message space: compose multi-turn history into the prompt
|
||||||
if len(nonSystem) <= 1 {
|
if len(nonSystem) <= 1 {
|
||||||
if systemPromptStr != "" && 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 != "" {
|
} else if systemPromptStr != "" {
|
||||||
promptMessageText = systemPromptStr
|
promptMessageText = systemPromptStr
|
||||||
} else {
|
} else {
|
||||||
@@ -2242,7 +2262,11 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
|
|||||||
if len(nonSystem) > 0 && nonSystem[len(nonSystem)-1].Role == "assistant" {
|
if len(nonSystem) > 0 && nonSystem[len(nonSystem)-1].Role == "assistant" {
|
||||||
lastRoleLabel = "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()
|
promptMessageText = sb.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-5
@@ -534,7 +534,7 @@ func TestUniversalToolCallingTransformMessages(t *testing.T) {
|
|||||||
t.Fatalf("expected 4 processed messages, got %d", len(processed))
|
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])
|
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)
|
t.Fatalf("failed to build payload 1: %v", err)
|
||||||
}
|
}
|
||||||
sysStr, ok := data1[0].(string)
|
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])
|
t.Errorf("expected system prompt at index 0, got %v", data1[0])
|
||||||
}
|
}
|
||||||
msgStr, ok := data1[1].(string)
|
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])
|
t.Fatalf("expected 1 history pair at index 1, got %T (%v)", data2[1], data2[1])
|
||||||
}
|
}
|
||||||
// Instructions prepended to the first user turn:
|
// 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])
|
t.Errorf("expected system instructions prepended to first pair user message, got: %q", pairs2[0][0])
|
||||||
}
|
}
|
||||||
if !strings.Contains(pairs2[0][1], "lookup") {
|
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.") {
|
if !strings.Contains(sysStr, "You are a specialized documentation bot.") {
|
||||||
t.Errorf("expected default system prompt to be retained, got: %s", sysStr)
|
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)
|
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 {
|
if !ok {
|
||||||
t.Fatalf("expected string at index 0, got %T", payload3[0])
|
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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user