feat(tools): augment system prompt when no native tool calling is detected
This commit is contained in:
@@ -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) {
|
||||
@@ -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 <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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user