feat(tools): augment system prompt when no native tool calling is detected

This commit is contained in:
Luxferre
2026-09-07 10:23:10 +03:00
parent cfb26602b5
commit b599038881
2 changed files with 163 additions and 13 deletions
+66 -9
View File
@@ -441,23 +441,44 @@ func BuildToolInstruction(tools []Tool) string {
return ""
}
toolsBytes, _ := json.MarshalIndent(tools, "", " ")
sampleFnName := ""
for _, t := range tools {
if fnMap, ok := t.Function.(map[string]interface{}); ok {
if n, ok := fnMap["name"].(string); ok && n != "" {
sampleFnName = n
break
}
}
}
if sampleFnName == "" {
sampleFnName = "example_tool"
}
return fmt.Sprintf(`# Tool Calling Instructions
You are equipped with external tools to assist with user queries.
You have access to the following tools:
<tools>
%s
</tools>
To call a tool, you MUST output a <tool_call> block directly in your text response formatted exactly as follows:
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:
<tool_call>
{"name": "<function-name>", "arguments": {<args-json-object>}}
{"name": "%s", "arguments": {...}}
</tool_call>
Rules:
- If you need to call a tool, respond ONLY with the <tool_call> block. Do not include introductory text, explanations, or commentary around the block.
- If you need to call multiple tools, provide each tool call in its own <tool_call> block.
- If no tool call is needed, answer the user's request directly and normally without using tool tags.
- When you receive a <tool_response>, answer the user's request using the information provided in the response, or call another tool if additional information is required.`, string(toolsBytes))
## 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)
}
func TransformMessages(req ChatCompletionRequest) (processed []ChatMessage, toolInstruction string, hasSystem bool) {
@@ -1496,6 +1517,7 @@ func (f *StreamToolCallFilter) Flush(onContent func(string), onToolCall func(Too
type GradioParamInfo struct {
Label string `json:"label"`
ParameterName string `json:"parameter_name"`
ParameterDefault interface{} `json:"parameter_default,omitempty"`
Component string `json:"component"`
}
@@ -1574,6 +1596,7 @@ type SpaceDiscovery struct {
HistoryIndex int // -1 if none
MessageIndex int // index for user message text
SystemIndex int // -1 if none
DefaultSystemPrompt string // default space system prompt if present
TempIndex int // -1 if none
MaxTokensIndex int // -1 if none
TopPIndex int // -1 if none
@@ -1639,6 +1662,7 @@ func NewDefaultSpaceDiscovery(spaceURL string) *SpaceDiscovery {
HistoryIndex: -1,
MessageIndex: 0,
SystemIndex: -1,
DefaultSystemPrompt: "",
TempIndex: -1,
MaxTokensIndex: -1,
TopPIndex: -1,
@@ -1863,6 +1887,11 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
} else if discovery.SystemIndex == -1 {
mapping.ParamType = "system_prompt"
discovery.SystemIndex = idx
if comp.Props != nil {
if val, ok := comp.Props["value"].(string); ok && strings.TrimSpace(val) != "" {
discovery.DefaultSystemPrompt = strings.TrimSpace(val)
}
}
}
case "state":
mapping.ParamType = "state"
@@ -1905,6 +1934,11 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
pName := strings.ToLower(p.ParameterName)
if strings.Contains(pName, "system") {
discovery.SystemIndex = idx
if p.ParameterDefault != nil && discovery.DefaultSystemPrompt == "" {
if defStr, ok := p.ParameterDefault.(string); ok && strings.TrimSpace(defStr) != "" {
discovery.DefaultSystemPrompt = strings.TrimSpace(defStr)
}
}
} else if strings.Contains(pName, "history") || strings.Contains(pName, "chat") {
discovery.HistoryIndex = idx
} else if strings.Contains(pName, "message") || (strings.Contains(pName, "prompt") && !strings.Contains(pName, "system")) || strings.Contains(pName, "text") {
@@ -2038,6 +2072,7 @@ func (g *GradioGateway) GetDiscovery(spaceURL, userAgent string) *SpaceDiscovery
// BuildGradioPayload packages OpenAI messages and parameters into the target Gradio input array.
func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatCompletionRequest) ([]interface{}, error) {
var transformed []ChatMessage
var toolInstruction string
if disc.IsHunyuan3 && disc.FunctionsJSONIndex != -1 {
for _, msg := range req.Messages {
transformed = append(transformed, ChatMessage{
@@ -2050,7 +2085,16 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
})
}
} else {
transformed, _, _ = TransformMessages(req)
// When no native tool calling support is detected, augment system prompt
transformed, toolInstruction, _ = TransformMessages(req)
}
hasClientSystem := false
for _, m := range req.Messages {
if m.Role == "system" {
hasClientSystem = true
break
}
}
var systemPromptStr string
@@ -2071,13 +2115,22 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
}
}
// If the space has a DefaultSystemPrompt and client provided no system message,
// retain and augment the default system prompt:
if !hasClientSystem && disc.DefaultSystemPrompt != "" {
if systemPromptStr != "" {
systemPromptStr = disc.DefaultSystemPrompt + "\n\n" + systemPromptStr
} else {
systemPromptStr = disc.DefaultSystemPrompt
}
}
// If the space has NO native system prompt input (disc.SystemIndex == -1),
// but we have system instructions (from system message or tool instructions):
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()
systemPromptStr = ""
}
}
@@ -2156,7 +2209,11 @@ 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)
} else {
promptMessageText = lastUserMessage
}
} else {
// Single message space: compose multi-turn history into the prompt
if len(nonSystem) <= 1 {
+93
View File
@@ -1204,4 +1204,97 @@ func TestToolUseFailedImmediateCallRecovery(t *testing.T) {
}
}
func TestSystemPromptAugmentationWithoutNativeToolCalling(t *testing.T) {
gw := NewGradioGateway("https://generic-space.hf.space", "", 10*time.Second)
tools := []Tool{
{
Type: "function",
Function: map[string]interface{}{
"name": "search_docs",
"description": "Search local documentation",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"query": map[string]interface{}{"type": "string"},
},
"required": []string{"query"},
},
},
},
}
// Case 1: Space with SystemIndex and space default prompt, client sends no system message
discWithSys := NewDefaultSpaceDiscovery("https://generic-space.hf.space")
discWithSys.TotalInputs = 2
discWithSys.MessageIndex = 0
discWithSys.SystemIndex = 1
discWithSys.DefaultSystemPrompt = "You are a specialized documentation bot."
req1 := ChatCompletionRequest{
Tools: tools,
Messages: []ChatMessage{
{Role: "user", Content: "How to configure SSL?"},
},
}
payload1, err := gw.BuildGradioPayload(discWithSys, req1)
if err != nil {
t.Fatalf("BuildGradioPayload failed: %v", err)
}
sysStr, ok := payload1[1].(string)
if !ok {
t.Fatalf("expected string at SystemIndex 1, got %T", payload1[1])
}
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") {
t.Errorf("expected tool calling instructions and function name in system prompt, got: %s", sysStr)
}
// Case 2: Space with SystemIndex, client sends their own system message
req2 := ChatCompletionRequest{
Tools: tools,
Messages: []ChatMessage{
{Role: "system", Content: "You are an expert developer assistant."},
{Role: "user", Content: "How to configure SSL?"},
},
}
payload2, err := gw.BuildGradioPayload(discWithSys, req2)
if err != nil {
t.Fatalf("BuildGradioPayload failed: %v", err)
}
sysStr2, ok := payload2[1].(string)
if !ok {
t.Fatalf("expected string at SystemIndex 1, got %T", payload2[1])
}
if !strings.Contains(sysStr2, "You are an expert developer assistant.") {
t.Errorf("expected client system message, got: %s", sysStr2)
}
if !strings.Contains(sysStr2, "search_docs") {
t.Errorf("expected search_docs tool instruction, got: %s", sysStr2)
}
// Case 3: Space without SystemIndex, single input textbox
discSingle := NewDefaultSpaceDiscovery("https://single-input.hf.space")
discSingle.TotalInputs = 1
discSingle.MessageIndex = 0
discSingle.SystemIndex = -1
discSingle.HistoryIndex = -1
payload3, err := gw.BuildGradioPayload(discSingle, req1)
if err != nil {
t.Fatalf("BuildGradioPayload failed: %v", err)
}
singleMsg, ok := payload3[0].(string)
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?") {
t.Errorf("expected single message to contain augmented instructions and user query, got: %s", singleMsg)
}
}