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 ""
|
return ""
|
||||||
}
|
}
|
||||||
toolsBytes, _ := json.MarshalIndent(tools, "", " ")
|
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
|
return fmt.Sprintf(`# Tool Calling Instructions
|
||||||
|
|
||||||
|
You are equipped with external tools to assist with user queries.
|
||||||
You have access to the following tools:
|
You have access to the following tools:
|
||||||
<tools>
|
<tools>
|
||||||
%s
|
%s
|
||||||
</tools>
|
</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>
|
<tool_call>
|
||||||
{"name": "<function-name>", "arguments": {<args-json-object>}}
|
{"name": "%s", "arguments": {...}}
|
||||||
</tool_call>
|
</tool_call>
|
||||||
|
|
||||||
Rules:
|
## 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.
|
1. If an available tool is relevant to the user's request, invoking the tool is MANDATORY.
|
||||||
- If you need to call multiple tools, provide each tool call in its own <tool_call> block.
|
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.
|
||||||
- If no tool call is needed, answer the user's request directly and normally without using tool tags.
|
3. If multiple tools are required, output each in its own <tool_call> block.
|
||||||
- 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))
|
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) {
|
||||||
@@ -1494,9 +1515,10 @@ func (f *StreamToolCallFilter) Flush(onContent func(string), onToolCall func(Too
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
type GradioParamInfo struct {
|
type GradioParamInfo struct {
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
ParameterName string `json:"parameter_name"`
|
ParameterName string `json:"parameter_name"`
|
||||||
Component string `json:"component"`
|
ParameterDefault interface{} `json:"parameter_default,omitempty"`
|
||||||
|
Component string `json:"component"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GradioEndpointInfo struct {
|
type GradioEndpointInfo struct {
|
||||||
@@ -1574,6 +1596,7 @@ type SpaceDiscovery struct {
|
|||||||
HistoryIndex int // -1 if none
|
HistoryIndex int // -1 if none
|
||||||
MessageIndex int // index for user message text
|
MessageIndex int // index for user message text
|
||||||
SystemIndex int // -1 if none
|
SystemIndex int // -1 if none
|
||||||
|
DefaultSystemPrompt string // default space system prompt if present
|
||||||
TempIndex int // -1 if none
|
TempIndex int // -1 if none
|
||||||
MaxTokensIndex int // -1 if none
|
MaxTokensIndex int // -1 if none
|
||||||
TopPIndex int // -1 if none
|
TopPIndex int // -1 if none
|
||||||
@@ -1639,6 +1662,7 @@ func NewDefaultSpaceDiscovery(spaceURL string) *SpaceDiscovery {
|
|||||||
HistoryIndex: -1,
|
HistoryIndex: -1,
|
||||||
MessageIndex: 0,
|
MessageIndex: 0,
|
||||||
SystemIndex: -1,
|
SystemIndex: -1,
|
||||||
|
DefaultSystemPrompt: "",
|
||||||
TempIndex: -1,
|
TempIndex: -1,
|
||||||
MaxTokensIndex: -1,
|
MaxTokensIndex: -1,
|
||||||
TopPIndex: -1,
|
TopPIndex: -1,
|
||||||
@@ -1863,6 +1887,11 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
|
|||||||
} else if discovery.SystemIndex == -1 {
|
} else if discovery.SystemIndex == -1 {
|
||||||
mapping.ParamType = "system_prompt"
|
mapping.ParamType = "system_prompt"
|
||||||
discovery.SystemIndex = idx
|
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":
|
case "state":
|
||||||
mapping.ParamType = "state"
|
mapping.ParamType = "state"
|
||||||
@@ -1905,6 +1934,11 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
|
|||||||
pName := strings.ToLower(p.ParameterName)
|
pName := strings.ToLower(p.ParameterName)
|
||||||
if strings.Contains(pName, "system") {
|
if strings.Contains(pName, "system") {
|
||||||
discovery.SystemIndex = idx
|
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") {
|
} else if strings.Contains(pName, "history") || strings.Contains(pName, "chat") {
|
||||||
discovery.HistoryIndex = idx
|
discovery.HistoryIndex = idx
|
||||||
} else if strings.Contains(pName, "message") || (strings.Contains(pName, "prompt") && !strings.Contains(pName, "system")) || strings.Contains(pName, "text") {
|
} 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.
|
// BuildGradioPayload packages OpenAI messages and parameters into the target Gradio input array.
|
||||||
func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatCompletionRequest) ([]interface{}, error) {
|
func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatCompletionRequest) ([]interface{}, error) {
|
||||||
var transformed []ChatMessage
|
var transformed []ChatMessage
|
||||||
|
var toolInstruction string
|
||||||
if disc.IsHunyuan3 && disc.FunctionsJSONIndex != -1 {
|
if disc.IsHunyuan3 && disc.FunctionsJSONIndex != -1 {
|
||||||
for _, msg := range req.Messages {
|
for _, msg := range req.Messages {
|
||||||
transformed = append(transformed, ChatMessage{
|
transformed = append(transformed, ChatMessage{
|
||||||
@@ -2050,7 +2085,16 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
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),
|
// If the space has NO native system prompt input (disc.SystemIndex == -1),
|
||||||
// but we have system instructions (from system message or tool instructions):
|
// but we have system instructions (from system message or tool instructions):
|
||||||
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()
|
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
|
var promptMessageText string
|
||||||
if disc.HistoryIndex != -1 {
|
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 {
|
} 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 {
|
||||||
|
|||||||
@@ -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