feat(tools): implement universal tool calling and multi-turn resolution for generic Gradio spaces

This commit is contained in:
Luxferre
2026-09-07 08:24:07 +03:00
parent d3be7d4e80
commit de19bab4eb
3 changed files with 825 additions and 182 deletions
+349 -181
View File
@@ -438,18 +438,33 @@ func BuildToolInstruction(tools []Tool) string {
return ""
}
toolsBytes, _ := json.MarshalIndent(tools, "", " ")
return fmt.Sprintf("\n\n# Tool Calling Instructions\n\nYou have access to the following functions:\n<tools>\n%s\n</tools>\n\nWhen you need to call a function, respond ONLY with a <tool_call> block formatted exactly as follows:\n<tool_call>\n{\"name\": \"<function-name>\", \"arguments\": {<args-json-object>}}\n</tool_call>\n\nDo not include conversational filler before or after the tool call.", string(toolsBytes))
return fmt.Sprintf("\n\n# Tool Calling Instructions\n\nYou have access to the following functions:\n<tools>\n%s\n</tools>\n\nWhen you need to call a function, respond ONLY with a <tool_call> block formatted exactly as follows:\n<tool_call>\n{\"name\": \"<function-name>\", \"arguments\": {<args-json-object>}}\n</tool_call>\n\nWhen you receive a <tool_response>, use the provided information to answer the user's request, or call further tools if needed.\nDo not include conversational filler before or after the tool call.", string(toolsBytes))
}
func TransformMessages(req ChatCompletionRequest) (processed []ChatMessage, toolInstruction string, hasSystem bool) {
toolInstruction = BuildToolInstruction(req.Tools)
// 1. Build lookup map from tool_call_id to function name across all assistant messages
toolIDToName := make(map[string]string)
for _, msg := range req.Messages {
for _, tc := range msg.ToolCalls {
if tc.ID != "" && tc.Function.Name != "" {
toolIDToName[tc.ID] = tc.Function.Name
}
}
}
toolInstruction = BuildToolInstruction(req.Tools)
// 2. Process and coalesce messages preserving turn parity
var staged []ChatMessage
for i := 0; i < len(req.Messages); i++ {
msg := req.Messages[i]
contentStr := msg.GetContentString()
m := ChatMessage{Role: msg.Role, Content: contentStr}
switch msg.Role {
case "system":
hasSystem = true
m.Content = contentStr
staged = append(staged, ChatMessage{Role: "system", Content: contentStr})
case "assistant":
var sb strings.Builder
if contentStr != "" {
@@ -465,40 +480,79 @@ func TransformMessages(req ChatCompletionRequest) (processed []ChatMessage, tool
}
sb.WriteString(fmt.Sprintf("<tool_call>\n{\"name\": %q, \"arguments\": %s}\n</tool_call>", tc.Function.Name, args))
}
m.Content = sb.String()
staged = append(staged, ChatMessage{
Role: "assistant",
Content: sb.String(),
ReasoningContent: msg.ReasoningContent,
ToolCalls: msg.ToolCalls,
})
case "tool", "function":
m.Role = "user"
toolName := msg.Name
if toolName == "" {
toolName = msg.ToolCallID
// Gather consecutive tool returns into a coalesced turn
var toolResponses []string
j := i
for j < len(req.Messages) && (req.Messages[j].Role == "tool" || req.Messages[j].Role == "function") {
tMsg := req.Messages[j]
tContent := tMsg.GetContentString()
tName := tMsg.Name
if tName == "" && tMsg.ToolCallID != "" {
if mapped, ok := toolIDToName[tMsg.ToolCallID]; ok {
tName = mapped
} else {
tName = tMsg.ToolCallID
}
}
var contentJSON []byte
if json.Valid([]byte(tContent)) {
contentJSON = []byte(tContent)
} else {
contentJSON, _ = json.Marshal(tContent)
}
toolResponses = append(toolResponses, fmt.Sprintf("<tool_response>\n{\"name\": %q, \"content\": %s}\n</tool_response>", tName, string(contentJSON)))
j++
}
var contentJSON []byte
if json.Valid([]byte(contentStr)) {
contentJSON = []byte(contentStr)
} else {
contentJSON, _ = json.Marshal(contentStr)
i = j - 1 // advance loop
promptSuffix := "Please answer the user's request based on the tool result."
if len(toolResponses) > 1 {
promptSuffix = "Please answer the user's request based on the tool results."
}
m.Content = fmt.Sprintf("<tool_response>\n{\"name\": %q, \"content\": %s}\n</tool_response>", toolName, string(contentJSON))
coalesced := strings.Join(toolResponses, "\n") + "\n\n" + promptSuffix
staged = append(staged, ChatMessage{
Role: "user",
Content: coalesced,
})
default: // "user" or other roles
staged = append(staged, ChatMessage{Role: msg.Role, Content: contentStr})
}
processed = append(processed, m)
}
// 3. Inject tool instructions into system prompt
if toolInstruction != "" {
if hasSystem {
for i, m := range processed {
systemInjected := false
for i, m := range staged {
if m.Role == "system" {
processed[i].Content = m.GetContentString() + "\n" + strings.TrimSpace(toolInstruction)
staged[i].Content = m.GetContentString() + "\n\n" + strings.TrimSpace(toolInstruction)
systemInjected = true
break
}
}
if !systemInjected {
staged = append([]ChatMessage{
{Role: "system", Content: strings.TrimSpace(toolInstruction)},
}, staged...)
}
} else {
processed = append([]ChatMessage{
{Role: "user", Content: strings.TrimSpace(toolInstruction)},
}, processed...)
staged = append([]ChatMessage{
{Role: "system", Content: strings.TrimSpace(toolInstruction)},
}, staged...)
hasSystem = true
}
}
return processed, toolInstruction, hasSystem
return staged, toolInstruction, hasSystem
}
func cleanJSONBlock(input string) string {
@@ -538,6 +592,35 @@ func sanitizeJSONValue(v interface{}) interface{} {
}
}
type ToolTagPair struct {
Start string
End string
}
var ToolTagPairs = []ToolTagPair{
{Start: "<tool_call>", End: "</tool_call>"},
{Start: "<tool_calls>", End: "</tool_calls>"},
{Start: "<function_call>", End: "</function_call>"},
{Start: "[TOOL_CALLS]", End: "[/TOOL_CALLS]"},
}
func getToolStartPrefixes() []string {
seen := make(map[string]bool)
var prefixes []string
for _, pair := range ToolTagPairs {
for i := 1; i <= len(pair.Start); i++ {
pref := pair.Start[:i]
if !seen[pref] {
seen[pref] = true
prefixes = append(prefixes, pref)
}
}
}
return prefixes
}
var toolStartPrefixes = getToolStartPrefixes()
func repairToolCallJSON(input string) (ToolCall, bool) {
s := strings.TrimSpace(input)
reName := regexp.MustCompile(`"(?:name|function|action|call)"\s*:\s*"([^"]+)"`)
@@ -547,8 +630,8 @@ func repairToolCallJSON(input string) (ToolCall, bool) {
}
fnName := matches[1]
reArgs := regexp.MustCompile(`"(?:arguments|parameters|args|input)"\s*:\s*(\{[\s\S]*\})`)
argMatches := reArgs.FindStringSubmatch(s)
reArgsObj := regexp.MustCompile(`"(?:arguments|parameters|args|input)"\s*:\s*(\{[\s\S]*\})`)
argMatches := reArgsObj.FindStringSubmatch(s)
argsStr := "{}"
if len(argMatches) >= 2 {
candidate := argMatches[1]
@@ -556,6 +639,15 @@ func repairToolCallJSON(input string) (ToolCall, bool) {
if json.Unmarshal([]byte(candidate), &dummy) == nil {
argsStr = candidate
}
} else {
reArgsStr := regexp.MustCompile(`"(?:arguments|parameters|args|input)"\s*:\s*"((?:\\.|[^"\\])*)"`)
strMatches := reArgsStr.FindStringSubmatch(s)
if len(strMatches) >= 2 {
var unescaped string
if json.Unmarshal([]byte(`"`+strMatches[1]+`"`), &unescaped) == nil {
argsStr = unescaped
}
}
}
return ToolCall{
@@ -671,18 +763,68 @@ func parseSingleToolCall(jsonStr string) (ToolCall, bool) {
return repairToolCallJSON(cleaned)
}
func parseXMLToolCall(block string) (ToolCall, bool) {
inner := strings.TrimSpace(block)
if strings.HasPrefix(inner, "<tool_call>") {
inner = strings.TrimPrefix(inner, "<tool_call>")
func parseMultipleToolCalls(raw string) ([]ToolCall, bool) {
cleaned := cleanJSONBlock(raw)
if cleaned == "" {
return nil, false
}
if strings.HasSuffix(inner, "</tool_call>") {
inner = strings.TrimSuffix(inner, "</tool_call>")
// 1. Direct JSON array: [{"name":...}, ...]
var rawList []interface{}
if err := json.Unmarshal([]byte(cleaned), &rawList); err == nil {
var calls []ToolCall
for _, item := range rawList {
b, err := json.Marshal(item)
if err == nil {
if tc, ok := parseSingleToolCall(string(b)); ok {
calls = append(calls, tc)
}
}
}
if len(calls) > 0 {
return calls, true
}
}
// 2. Wrapper object with tool_calls / calls array
var rawMap map[string]interface{}
if err := json.Unmarshal([]byte(cleaned), &rawMap); err == nil {
for _, listKey := range []string{"tool_calls", "calls", "functions"} {
if subArr, ok := rawMap[listKey].([]interface{}); ok && len(subArr) > 0 {
var calls []ToolCall
for _, item := range subArr {
b, err := json.Marshal(item)
if err == nil {
if tc, ok := parseSingleToolCall(string(b)); ok {
calls = append(calls, tc)
}
}
}
if len(calls) > 0 {
return calls, true
}
}
}
}
// 3. Single tool call
if tc, ok := parseSingleToolCall(cleaned); ok {
return []ToolCall{tc}, true
}
return nil, false
}
func parseXMLToolCall(block string) ([]ToolCall, bool) {
inner := strings.TrimSpace(block)
for _, pair := range ToolTagPairs {
inner = strings.ReplaceAll(inner, pair.Start, "")
inner = strings.ReplaceAll(inner, pair.End, "")
}
inner = cleanJSONBlock(inner)
if tc, ok := parseSingleToolCall(inner); ok {
return tc, true
if calls, ok := parseMultipleToolCalls(inner); ok && len(calls) > 0 {
return calls, true
}
var fnName string
@@ -707,88 +849,58 @@ func parseXMLToolCall(block string) (ToolCall, bool) {
if argsStr == "" {
argsStr = "{}"
}
return ToolCall{
return []ToolCall{{
ID: "call_" + GenerateUUID()[:8],
Type: "function",
Function: ToolCallFunction{
Name: fnName,
Arguments: argsStr,
},
}, true
}}, true
}
return ToolCall{}, false
return nil, false
}
func ExtractToolCallBlocks(content string) (blocks []string, remaining string) {
s := content
remaining = content
for strings.Contains(s, "<tool_call>") {
sIdx := strings.Index(s, "<tool_call>")
rest := s[sIdx+len("<tool_call>"):]
for _, pair := range ToolTagPairs {
for strings.Contains(remaining, pair.Start) {
sIdx := strings.Index(remaining, pair.Start)
rest := remaining[sIdx+len(pair.Start):]
relNextSIdx := strings.Index(rest, "<tool_call>")
var nextSIdx int
if relNextSIdx != -1 {
nextSIdx = sIdx + len("<tool_call>") + relNextSIdx
} else {
nextSIdx = -1
}
relNextSIdx := strings.Index(rest, pair.Start)
var nextSIdx int
if relNextSIdx != -1 {
nextSIdx = sIdx + len(pair.Start) + relNextSIdx
} else {
nextSIdx = -1
}
relEIdx := strings.Index(rest, "</tool_call>")
var eIdx int
if relEIdx != -1 {
eIdx = sIdx + len("<tool_call>") + relEIdx
} else {
eIdx = -1
}
relEIdx := strings.Index(rest, pair.End)
var eIdx int
if relEIdx != -1 {
eIdx = sIdx + len(pair.Start) + relEIdx
} else {
eIdx = -1
}
var blockText string
var blockEndPos int
var blockText string
if eIdx != -1 && (nextSIdx == -1 || eIdx < nextSIdx) {
blockEndPos := eIdx + len(pair.End)
blockText = remaining[sIdx:blockEndPos]
remaining = strings.TrimSpace(remaining[:sIdx] + remaining[blockEndPos:])
} else if nextSIdx != -1 {
blockEndPos := nextSIdx
blockText = remaining[sIdx:blockEndPos]
remaining = strings.TrimSpace(remaining[:sIdx] + remaining[blockEndPos:])
} else {
blockText = remaining[sIdx:]
remaining = strings.TrimSpace(remaining[:sIdx])
}
if eIdx != -1 && (nextSIdx == -1 || eIdx < nextSIdx) {
blockEndPos = eIdx + len("</tool_call>")
blockText = s[sIdx:blockEndPos]
s = s[blockEndPos:]
} else if nextSIdx != -1 {
blockEndPos = nextSIdx
blockText = s[sIdx:blockEndPos]
s = s[blockEndPos:]
} else {
blockText = s[sIdx:]
s = ""
}
blocks = append(blocks, blockText)
}
for strings.Contains(remaining, "<tool_call>") {
st := strings.Index(remaining, "<tool_call>")
rest := remaining[st+len("<tool_call>"):]
relNext := strings.Index(rest, "<tool_call>")
var nextSt int
if relNext != -1 {
nextSt = st + len("<tool_call>") + relNext
} else {
nextSt = -1
}
relEn := strings.Index(rest, "</tool_call>")
var en int
if relEn != -1 {
en = st + len("<tool_call>") + relEn
} else {
en = -1
}
if en != -1 && (nextSt == -1 || en < nextSt) {
remaining = strings.TrimSpace(remaining[:st] + remaining[en+len("</tool_call>"):])
} else if nextSt != -1 {
remaining = strings.TrimSpace(remaining[:st] + remaining[nextSt:])
} else {
remaining = strings.TrimSpace(remaining[:st])
blocks = append(blocks, blockText)
}
}
@@ -800,8 +912,8 @@ func DetectToolCalls(content string) ([]ToolCall, string, bool) {
var calls []ToolCall
for _, block := range blocks {
if toolCall, ok := parseXMLToolCall(block); ok {
calls = append(calls, toolCall)
if tcs, ok := parseXMLToolCall(block); ok {
calls = append(calls, tcs...)
}
}
@@ -809,8 +921,8 @@ func DetectToolCalls(content string) ([]ToolCall, string, bool) {
return calls, remaining, true
}
if tc, ok := parseSingleToolCall(strings.TrimSpace(content)); ok {
return []ToolCall{tc}, "", true
if tcs, ok := parseMultipleToolCalls(strings.TrimSpace(content)); ok && len(tcs) > 0 {
return tcs, "", true
}
return nil, content, false
@@ -955,12 +1067,24 @@ func NewStreamThinkingFilter() *StreamThinkingFilter {
}
func hasPrefixOf(target string, prefixes []string) int {
maxMatch := 0
for _, p := range prefixes {
if strings.HasSuffix(target, p) {
return len(p)
if strings.HasSuffix(target, p) && len(p) > maxMatch {
maxMatch = len(p)
}
}
return 0
return maxMatch
}
func hasSuffixPrefixOf(target string, tag string) int {
maxMatch := 0
for i := 1; i < len(tag); i++ {
p := tag[:i]
if strings.HasSuffix(target, p) && len(p) > maxMatch {
maxMatch = len(p)
}
}
return maxMatch
}
func (f *StreamThinkingFilter) Feed(chunk string, onContent func(string), onReasoning func(string)) {
@@ -1034,11 +1158,13 @@ func (f *StreamThinkingFilter) Flush(onContent func(string), onReasoning func(st
// ---------------------------------------------------------------------------
type StreamToolCallFilter struct {
inToolCall bool
buf string
toolCallBuf string
toolIndex int
emittedCall bool
inToolCall bool
buf string
toolCallBuf string
toolIndex int
emittedCall bool
activePair ToolTagPair
activeEndTag string
}
func NewStreamToolCallFilter() *StreamToolCallFilter {
@@ -1047,22 +1173,31 @@ func NewStreamToolCallFilter() *StreamToolCallFilter {
func (f *StreamToolCallFilter) Feed(chunk string, onContent func(string), onToolCall func(ToolCall)) {
f.buf += chunk
toolStartTag := "<tool_call>"
toolEndTag := "</tool_call>"
startPrefixes := []string{"<", "<t", "<to", "<too", "<tool", "<tool_", "<tool_c", "<tool_ca", "<tool_cal", "<tool_call"}
endPrefixes := []string{"<", "</", "</t", "</to", "</too", "</tool", "</tool_", "</tool_c", "</tool_ca", "</tool_cal", "</tool_call"}
for len(f.buf) > 0 {
if !f.inToolCall {
if idx := strings.Index(f.buf, toolStartTag); idx != -1 {
before := f.buf[:idx]
earliestIdx := -1
var matchedPair ToolTagPair
for _, pair := range ToolTagPairs {
if idx := strings.Index(f.buf, pair.Start); idx != -1 {
if earliestIdx == -1 || idx < earliestIdx {
earliestIdx = idx
matchedPair = pair
}
}
}
if earliestIdx != -1 {
before := f.buf[:earliestIdx]
if before != "" {
onContent(before)
}
f.inToolCall = true
f.buf = f.buf[idx+len(toolStartTag):]
} else if matchLen := hasPrefixOf(f.buf, startPrefixes); matchLen > 0 {
f.activePair = matchedPair
f.activeEndTag = matchedPair.End
f.buf = f.buf[earliestIdx+len(matchedPair.Start):]
} else if matchLen := hasPrefixOf(f.buf, toolStartPrefixes); matchLen > 0 {
safe := f.buf[:len(f.buf)-matchLen]
if safe != "" {
onContent(safe)
@@ -1075,28 +1210,32 @@ func (f *StreamToolCallFilter) Feed(chunk string, onContent func(string), onTool
break
}
} else {
if idx := strings.Index(f.buf, toolEndTag); idx != -1 {
if idx := strings.Index(f.buf, f.activeEndTag); idx != -1 {
f.toolCallBuf += f.buf[:idx]
f.buf = f.buf[idx+len(toolEndTag):]
f.buf = f.buf[idx+len(f.activeEndTag):]
f.inToolCall = false
if tc, ok := parseSingleToolCall(f.toolCallBuf); ok {
idxCopy := f.toolIndex
tc.Index = &idxCopy
f.toolIndex++
f.emittedCall = true
onToolCall(tc)
} else if tc2, ok2 := parseXMLToolCall("<tool_call>" + f.toolCallBuf + "</tool_call>"); ok2 {
idxCopy := f.toolIndex
tc2.Index = &idxCopy
f.toolIndex++
f.emittedCall = true
onToolCall(tc2)
if tcs, ok := parseMultipleToolCalls(f.toolCallBuf); ok && len(tcs) > 0 {
for _, tc := range tcs {
idxCopy := f.toolIndex
tc.Index = &idxCopy
f.toolIndex++
f.emittedCall = true
onToolCall(tc)
}
} else if tcs2, ok2 := parseXMLToolCall(f.activePair.Start + f.toolCallBuf + f.activePair.End); ok2 && len(tcs2) > 0 {
for _, tc := range tcs2 {
idxCopy := f.toolIndex
tc.Index = &idxCopy
f.toolIndex++
f.emittedCall = true
onToolCall(tc)
}
} else {
onContent("<tool_call>" + f.toolCallBuf + "</tool_call>")
onContent(f.activePair.Start + f.toolCallBuf + f.activePair.End)
}
f.toolCallBuf = ""
} else if matchLen := hasPrefixOf(f.buf, endPrefixes); matchLen > 0 {
} else if matchLen := hasSuffixPrefixOf(f.buf, f.activeEndTag); matchLen > 0 {
safe := f.buf[:len(f.buf)-matchLen]
f.toolCallBuf += safe
f.buf = f.buf[len(f.buf)-matchLen:]
@@ -1112,18 +1251,24 @@ func (f *StreamToolCallFilter) Feed(chunk string, onContent func(string), onTool
func (f *StreamToolCallFilter) Flush(onContent func(string), onToolCall func(ToolCall)) {
if f.inToolCall && len(f.toolCallBuf) > 0 {
if tc, ok := parseSingleToolCall(f.toolCallBuf); ok {
idxCopy := f.toolIndex
tc.Index = &idxCopy
f.emittedCall = true
onToolCall(tc)
} else if tc2, ok2 := parseXMLToolCall("<tool_call>" + f.toolCallBuf + "</tool_call>"); ok2 {
idxCopy := f.toolIndex
tc2.Index = &idxCopy
f.emittedCall = true
onToolCall(tc2)
if tcs, ok := parseMultipleToolCalls(f.toolCallBuf); ok && len(tcs) > 0 {
for _, tc := range tcs {
idxCopy := f.toolIndex
tc.Index = &idxCopy
f.toolIndex++
f.emittedCall = true
onToolCall(tc)
}
} else if tcs2, ok2 := parseXMLToolCall(f.activePair.Start + f.toolCallBuf + f.activePair.End); ok2 && len(tcs2) > 0 {
for _, tc := range tcs2 {
idxCopy := f.toolIndex
tc.Index = &idxCopy
f.toolIndex++
f.emittedCall = true
onToolCall(tc)
}
} else {
onContent("<tool_call>" + f.toolCallBuf)
onContent(f.activePair.Start + f.toolCallBuf)
}
f.toolCallBuf = ""
}
@@ -1268,21 +1413,18 @@ func (d *SpaceDiscovery) GetModelList() []ModelItem {
return items
}
// InspectSpace queries Gradio's /gradio_api/info, /config, and HuggingFace Space APIs
// to build an adaptive schema mapping for any Gradio space.
func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscovery, error) {
cleanURL := strings.TrimRight(rawURL, "/")
if !strings.HasPrefix(cleanURL, "http://") && !strings.HasPrefix(cleanURL, "https://") {
func NewDefaultSpaceDiscovery(spaceURL string) *SpaceDiscovery {
cleanURL := strings.TrimRight(spaceURL, "/")
if cleanURL != "" && !strings.HasPrefix(cleanURL, "http://") && !strings.HasPrefix(cleanURL, "https://") {
cleanURL = "https://" + cleanURL
}
discovery := &SpaceDiscovery{
SpaceURL: cleanURL,
APIPrefix: "/gradio_api",
Endpoint: "/chat_fn",
CleanEndpoint: "chat_fn",
Protocol: "call",
TotalInputs: 1,
return &SpaceDiscovery{
SpaceURL: cleanURL,
APIPrefix: "/gradio_api",
Endpoint: "/chat_fn",
CleanEndpoint: "chat_fn",
Protocol: "call",
TotalInputs: 1,
HistoryIndex: -1,
MessageIndex: 0,
SystemIndex: -1,
@@ -1295,6 +1437,17 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
HistoryFormat: "messages",
LastDiscovered: time.Now(),
}
}
// InspectSpace queries Gradio's /gradio_api/info, /config, and HuggingFace Space APIs
// to build an adaptive schema mapping for any Gradio space.
func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscovery, error) {
cleanURL := strings.TrimRight(rawURL, "/")
if !strings.HasPrefix(cleanURL, "http://") && !strings.HasPrefix(cleanURL, "https://") {
cleanURL = "https://" + cleanURL
}
discovery := NewDefaultSpaceDiscovery(cleanURL)
// 1. Try fetching /gradio_api/info or /info
var infoResp GradioAPIInfoResponse
@@ -1532,16 +1685,19 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
}
}
// Check parameters in bestEndpointInfo for history support
// Check parameters in bestEndpointInfo for input indices and history support
if bestEndpointInfo != nil {
if discovery.TotalInputs < len(bestEndpointInfo.Parameters) {
discovery.TotalInputs = len(bestEndpointInfo.Parameters)
}
for idx, p := range bestEndpointInfo.Parameters {
pName := strings.ToLower(p.ParameterName)
if strings.Contains(pName, "message") && discovery.MessageIndex == 0 {
discovery.MessageIndex = idx
} else if strings.Contains(pName, "history") {
discovery.HistoryIndex = idx
} else if strings.Contains(pName, "system") {
if strings.Contains(pName, "system") {
discovery.SystemIndex = idx
} 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") {
discovery.MessageIndex = idx
} else if strings.Contains(pName, "think_level") || strings.Contains(pName, "thinking_level") {
discovery.ThinkLevelIndex = idx
} else if strings.Contains(pName, "functions") || strings.Contains(pName, "tools") {
@@ -1660,20 +1816,10 @@ func (g *GradioGateway) GetDiscovery(spaceURL, userAgent string) *SpaceDiscovery
}
// Fallback discovery
fallback := &SpaceDiscovery{
SpaceURL: cleanTarget,
APIPrefix: "/gradio_api",
Endpoint: "/chat_fn",
CleanEndpoint: "chat_fn",
Protocol: "call",
TotalInputs: 2,
HistoryIndex: -1,
MessageIndex: 0,
SystemIndex: -1,
PrimaryModel: "gradio-chat",
Models: []string{"gradio-chat"},
LastDiscovered: time.Now(),
}
fallback := NewDefaultSpaceDiscovery(cleanTarget)
fallback.TotalInputs = 2
fallback.PrimaryModel = "gradio-chat"
fallback.Models = []string{"gradio-chat"}
g.discoveries[cleanTarget] = fallback
return fallback
}
@@ -1703,13 +1849,27 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
var nonSystem []ChatMessage
for _, m := range transformed {
cStr := m.GetContentString()
if m.Role == "system" && systemPromptStr == "" {
systemPromptStr = cStr
if m.Role == "system" {
if systemPromptStr == "" {
systemPromptStr = cStr
} else {
systemPromptStr += "\n\n" + cStr
}
} else {
nonSystem = append(nonSystem, m)
}
}
// 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 = ""
}
}
if len(nonSystem) > 0 {
for i := 0; i < len(nonSystem)-1; i++ {
m := nonSystem[i]
@@ -1803,7 +1963,15 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
}
sb.WriteString(fmt.Sprintf("%s: %s\n\n", roleLabel, m.GetContentString()))
}
sb.WriteString(lastUserMessage)
lastRoleLabel := "User"
if len(nonSystem) > 0 && nonSystem[len(nonSystem)-1].Role == "assistant" {
lastRoleLabel = "Assistant"
}
if sb.Len() > 0 {
sb.WriteString(fmt.Sprintf("%s: %s", lastRoleLabel, lastUserMessage))
} else {
sb.WriteString(lastUserMessage)
}
promptMessageText = sb.String()
}
}