diff --git a/gr2gw.go b/gr2gw.go index bf8437f..505f3a7 100644 --- a/gr2gw.go +++ b/gr2gw.go @@ -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: %s -To call a tool, you MUST output a 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 block formatted as: -{"name": "", "arguments": {}} +{"name": "%s", "arguments": {...}} -Rules: -- If you need to call a tool, respond ONLY with the 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 block. -- If no tool call is needed, answer the user's request directly and normally without using tool tags. -- When you receive a , 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 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) } func TransformMessages(req ChatCompletionRequest) (processed []ChatMessage, toolInstruction string, hasSystem bool) { @@ -1494,9 +1515,10 @@ func (f *StreamToolCallFilter) Flush(onContent func(string), onToolCall func(Too // --------------------------------------------------------------------------- type GradioParamInfo struct { - Label string `json:"label"` - ParameterName string `json:"parameter_name"` - Component string `json:"component"` + Label string `json:"label"` + ParameterName string `json:"parameter_name"` + ParameterDefault interface{} `json:"parameter_default,omitempty"` + Component string `json:"component"` } type GradioEndpointInfo struct { @@ -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 { - promptMessageText = lastUserMessage + 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) + } else { + promptMessageText = lastUserMessage + } } else { // Single message space: compose multi-turn history into the prompt if len(nonSystem) <= 1 { diff --git a/gr2gw_test.go b/gr2gw_test.go index c0ab1ac..dd61744 100644 --- a/gr2gw_test.go +++ b/gr2gw_test.go @@ -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) + } +} + +