From 9f2a5068f8f80a501ce409a840a8380db013fc3b Mon Sep 17 00:00:00 2001 From: Luxferre Date: Mon, 7 Sep 2026 11:16:09 +0300 Subject: [PATCH] Support Gradio 6 Chatbot format and multi-output spaces like tdecae-personal-chatbot --- gr2gw.go | 349 ++++++++++++++++++++++++++++++++++---------------- gr2gw_test.go | 7 + 2 files changed, 249 insertions(+), 107 deletions(-) diff --git a/gr2gw.go b/gr2gw.go index 6c6e21e..bafdc14 100644 --- a/gr2gw.go +++ b/gr2gw.go @@ -73,14 +73,14 @@ type ChatMessage struct { Name string `json:"name,omitempty"` } -func (m *ChatMessage) GetContentString() string { - if m.Content == nil { +func extractContentString(val interface{}) string { + if val == nil { return "" } - if str, ok := m.Content.(string); ok { + if str, ok := val.(string); ok { return str } - if parts, ok := m.Content.([]interface{}); ok { + if parts, ok := val.([]interface{}); ok { var sb strings.Builder for _, p := range parts { if str, ok := p.(string); ok { @@ -88,16 +88,29 @@ func (m *ChatMessage) GetContentString() string { } else if itemMap, ok := p.(map[string]interface{}); ok { if textVal, ok := itemMap["text"].(string); ok { sb.WriteString(textVal) + } else if valStr, ok := itemMap["value"].(string); ok { + sb.WriteString(valStr) } } } return sb.String() } - b, err := json.Marshal(m.Content) + if itemMap, ok := val.(map[string]interface{}); ok { + if textVal, ok := itemMap["text"].(string); ok { + return textVal + } else if valStr, ok := itemMap["value"].(string); ok { + return valStr + } + } + b, err := json.Marshal(val) if err == nil { return string(b) } - return fmt.Sprintf("%v", m.Content) + return fmt.Sprintf("%v", val) +} + +func (m *ChatMessage) GetContentString() string { + return extractContentString(m.Content) } type ChatCompletionRequest struct { @@ -1520,6 +1533,8 @@ type GradioParamInfo struct { ParameterName string `json:"parameter_name"` ParameterDefault interface{} `json:"parameter_default,omitempty"` Component string `json:"component"` + Type interface{} `json:"type,omitempty"` + PythonType interface{} `json:"python_type,omitempty"` } type GradioEndpointInfo struct { @@ -1828,20 +1843,32 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover score := 0 lowerName := strings.ToLower(epName) - if strings.Contains(lowerName, "chat") { + // Penalize non-generation/reset/clear/init endpoints or 0-parameter endpoints + if len(epInfo.Parameters) == 0 { + score -= 500 + } + if strings.Contains(lowerName, "clear") || strings.Contains(lowerName, "reset") || strings.Contains(lowerName, "init") || strings.Contains(lowerName, "undo") || strings.Contains(lowerName, "delete") { + score -= 500 + } + + if strings.Contains(lowerName, "chat") || strings.Contains(lowerName, "conversation") || strings.Contains(lowerName, "dialogue") { score += 100 } - if strings.Contains(lowerName, "predict") || strings.Contains(lowerName, "respond") || strings.Contains(lowerName, "generate") { - score += 50 + if strings.Contains(lowerName, "answer") || strings.Contains(lowerName, "respond") || strings.Contains(lowerName, "generate") || strings.Contains(lowerName, "predict") || strings.Contains(lowerName, "completion") { + score += 60 + } + if strings.Contains(lowerName, "ask") || strings.Contains(lowerName, "query") || strings.Contains(lowerName, "question") || strings.Contains(lowerName, "talk") { + score += 40 } for _, p := range epInfo.Parameters { pLower := strings.ToLower(p.ParameterName) - if strings.Contains(pLower, "message") || strings.Contains(pLower, "text") || strings.Contains(pLower, "prompt") { + pComp := strings.ToLower(p.Component) + if strings.Contains(pLower, "message") || strings.Contains(pLower, "text") || strings.Contains(pLower, "prompt") || strings.Contains(pLower, "query") || strings.Contains(pLower, "question") || strings.Contains(pLower, "input") || pComp == "textbox" { score += 40 } - if strings.Contains(pLower, "history") || strings.Contains(pLower, "chat") { - score += 20 + if strings.Contains(pLower, "history") || strings.Contains(pLower, "chat") || strings.Contains(pLower, "messages") || strings.Contains(pLower, "conversation") || pComp == "chatbot" { + score += 30 } } @@ -1943,6 +1970,7 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover } for idx, p := range bestEndpointInfo.Parameters { pName := strings.ToLower(p.ParameterName) + pComp := strings.ToLower(p.Component) if strings.Contains(pName, "system") { discovery.SystemIndex = idx if p.ParameterDefault != nil && discovery.DefaultSystemPrompt == "" { @@ -1950,9 +1978,20 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover 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") || strings.Contains(pName, "conversation") || strings.Contains(pName, "messages") || pComp == "chatbot" { discovery.HistoryIndex = idx - } else if strings.Contains(pName, "message") || (strings.Contains(pName, "prompt") && !strings.Contains(pName, "system")) || strings.Contains(pName, "text") { + bType, _ := json.Marshal(p.Type) + bPyType, _ := json.Marshal(p.PythonType) + pPyType := strings.ToLower(string(bPyType)) + bTypeStr := strings.ToLower(string(bType)) + if strings.Contains(pPyType, "list[tuple[") || strings.Contains(pPyType, "list[list[") || strings.Contains(bTypeStr, "tuple") { + discovery.HistoryFormat = "pairs" + } else if strings.Contains(pPyType, "textmessage") || strings.Contains(pPyType, "dict(text: str") || strings.Contains(bTypeStr, "textmessage") || strings.Contains(bTypeStr, "chatbotdatamessages") { + discovery.HistoryFormat = "gradio_messages" + } else if strings.HasPrefix(configResp.Version, "5.") || strings.HasPrefix(configResp.Version, "6.") { + discovery.HistoryFormat = "gradio_messages" + } + } else if strings.Contains(pName, "message") || (strings.Contains(pName, "prompt") && !strings.Contains(pName, "system")) || strings.Contains(pName, "text") || strings.Contains(pName, "query") || strings.Contains(pName, "question") || strings.Contains(pName, "input") || pComp == "textbox" { discovery.MessageIndex = idx } else if strings.Contains(pName, "think_level") || strings.Contains(pName, "thinking_level") { discovery.ThinkLevelIndex = idx @@ -1972,6 +2011,7 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover if discovery.FunctionsJSONIndex != -1 || discovery.ThinkLevelIndex != -1 || strings.Contains(cleanURL, "hy3") || strings.Contains(cleanURL, "hunyuan") { discovery.IsHunyuan3 = true + discovery.HistoryFormat = "messages" discovery.Models = append(discovery.Models, "hy3", "hunyuan3", "tencent/Hy3") if discovery.PrimaryModel == "gradio-chat" || discovery.PrimaryModel == "" { discovery.PrimaryModel = "hy3" @@ -2301,6 +2341,26 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet pairs = append(pairs, []string{u, a}) } data[disc.HistoryIndex] = pairs + } else if disc.HistoryFormat == "gradio_messages" { + var gMsgs []map[string]interface{} + for _, item := range historyArray { + role, _ := item["role"].(string) + cStr := "" + if s, ok := item["content"].(string); ok { + cStr = s + } + gMsg := map[string]interface{}{ + "role": role, + "content": []map[string]string{ + {"text": cStr, "type": "text"}, + }, + } + gMsgs = append(gMsgs, gMsg) + } + if gMsgs == nil { + gMsgs = []map[string]interface{}{} + } + data[disc.HistoryIndex] = gMsgs } else { data[disc.HistoryIndex] = historyArray } @@ -2393,107 +2453,182 @@ func ParseGradioStreamOutput(rawJSON string) GradioOutputFrame { return frame } - // Check if v[0] is an inner slice (e.g. Hy3: [[content, reasoning, tool_calls, history]]) - if inner, ok := v[0].([]interface{}); ok { - if len(inner) >= 3 { - s0, _ := inner[0].(string) - s1, _ := inner[1].(string) - frame.Content = s0 - frame.Reasoning = s1 - if inner[2] != nil { - b, err := json.Marshal(inner[2]) - if err == nil { - var tcs []ToolCall - if json.Unmarshal(b, &tcs) == nil && len(tcs) > 0 { - frame.ToolCalls = tcs - } - } - } - frame.OK = true - return frame - } - - if len(inner) == 2 { - s0, ok0 := inner[0].(string) - s1, ok1 := inner[1].(string) - if ok0 && ok1 { - frame.Content = s0 - frame.Reasoning = s1 - frame.OK = true - return frame - } - - // Check if inner is a chat pair: ["user msg", "assistant msg"] - if ok1 { - frame.Content = s1 - frame.OK = true - return frame - } - } - - // Check if inner is a list of chat message maps: [{"role":..., "content":...}, ...] - // or list of pairs: [["u", "a"], ...] - if len(inner) > 0 { - lastItem := inner[len(inner)-1] - if m, ok := lastItem.(map[string]interface{}); ok { - if c, ok := m["content"].(string); ok { - frame.Content = c - frame.OK = true - } - if r, ok := m["reasoning_content"].(string); ok { - frame.Reasoning = r - } - if tcsRaw, ok := m["tool_calls"].([]interface{}); ok && len(tcsRaw) > 0 { - b, err := json.Marshal(tcsRaw) - if err == nil { - var tcs []ToolCall - if err := json.Unmarshal(b, &tcs); err == nil { - frame.ToolCalls = tcs - } - } - } - if frame.OK { - return frame - } - } else if pair, ok := lastItem.([]interface{}); ok && len(pair) >= 2 { - if aStr, ok := pair[1].(string); ok { - frame.Content = aStr - frame.OK = true - return frame - } - } - } - } - - // Check if v[0] is string (standard single output e.g. ["content", null]) - if s, ok := v[0].(string); ok { - frame.Content = s - frame.OK = true - return frame - } - - // Check if v is a flat list of messages: [{"role": "assistant", ...}] - lastItem := v[len(v)-1] - if m, ok := lastItem.(map[string]interface{}); ok { - if c, ok := m["content"].(string); ok { - frame.Content = c - frame.OK = true - } - if r, ok := m["reasoning_content"].(string); ok { - frame.Reasoning = r - } - if tcsRaw, ok := m["tool_calls"].([]interface{}); ok && len(tcsRaw) > 0 { - b, err := json.Marshal(tcsRaw) + // 1. Check if v[0] is an inner slice with len >= 3 (e.g. Hy3: [[content, reasoning, tool_calls, history]]) + if inner, ok := v[0].([]interface{}); ok && len(inner) >= 3 { + s0, _ := inner[0].(string) + s1, _ := inner[1].(string) + frame.Content = s0 + frame.Reasoning = s1 + if inner[2] != nil { + b, err := json.Marshal(inner[2]) if err == nil { var tcs []ToolCall - if err := json.Unmarshal(b, &tcs); err == nil { + if json.Unmarshal(b, &tcs) == nil && len(tcs) > 0 { frame.ToolCalls = tcs } } } - if frame.OK { + frame.OK = true + return frame + } + + // 2. Check if any element of v is a Chatbot message list or Chatbot pair list + for _, elem := range v { + if msgList, ok := elem.([]interface{}); ok && len(msgList) > 0 { + allMaps := true + var maps []map[string]interface{} + for _, item := range msgList { + if m, ok := item.(map[string]interface{}); ok { + if _, hasRole := m["role"]; hasRole { + maps = append(maps, m) + continue + } + } + allMaps = false + break + } + if allMaps && len(maps) > 0 { + var targetMsg map[string]interface{} + for i := len(maps) - 1; i >= 0; i-- { + if r, _ := maps[i]["role"].(string); r == "assistant" { + targetMsg = maps[i] + break + } + } + if targetMsg == nil { + targetMsg = maps[len(maps)-1] + } + + cText := extractContentString(targetMsg["content"]) + frame.Content = cText + + if r, ok := targetMsg["reasoning_content"].(string); ok { + frame.Reasoning = r + } else if meta, ok := targetMsg["metadata"].(map[string]interface{}); ok { + if logStr, ok := meta["log"].(string); ok && logStr != "" { + frame.Reasoning = logStr + } + } + + if tcsRaw, ok := targetMsg["tool_calls"].([]interface{}); ok && len(tcsRaw) > 0 { + b, err := json.Marshal(tcsRaw) + if err == nil { + var tcs []ToolCall + if json.Unmarshal(b, &tcs) == nil { + frame.ToolCalls = tcs + } + } + } + + if len(frame.ToolCalls) == 0 { + tcs, clean, has := DetectToolCalls(cText) + if has && len(tcs) > 0 { + frame.ToolCalls = tcs + frame.Content = clean + } + } + + frame.OK = true + return frame + } + + // Check if elements are pairs [user, assistant] + allPairs := true + var pairs [][]interface{} + for _, item := range msgList { + if p, ok := item.([]interface{}); ok && len(p) == 2 { + pairs = append(pairs, p) + continue + } + allPairs = false + break + } + if allPairs && len(pairs) > 0 { + lastPair := pairs[len(pairs)-1] + cText := extractContentString(lastPair[1]) + frame.Content = cText + tcs, clean, has := DetectToolCalls(cText) + if has && len(tcs) > 0 { + frame.ToolCalls = tcs + frame.Content = clean + } + frame.OK = true + return frame + } + } + } + + // 3. Check if v itself is a list of message maps: [{"role": "assistant", ...}] + allMaps := true + var maps []map[string]interface{} + for _, item := range v { + if m, ok := item.(map[string]interface{}); ok { + if _, hasRole := m["role"]; hasRole { + maps = append(maps, m) + continue + } + } + allMaps = false + break + } + if allMaps && len(maps) > 0 { + var targetMsg map[string]interface{} + for i := len(maps) - 1; i >= 0; i-- { + if r, _ := maps[i]["role"].(string); r == "assistant" { + targetMsg = maps[i] + break + } + } + if targetMsg == nil { + targetMsg = maps[len(maps)-1] + } + cText := extractContentString(targetMsg["content"]) + frame.Content = cText + if r, ok := targetMsg["reasoning_content"].(string); ok { + frame.Reasoning = r + } + if tcsRaw, ok := targetMsg["tool_calls"].([]interface{}); ok && len(tcsRaw) > 0 { + b, err := json.Marshal(tcsRaw) + if err == nil { + var tcs []ToolCall + if json.Unmarshal(b, &tcs) == nil { + frame.ToolCalls = tcs + } + } + } + if len(frame.ToolCalls) == 0 { + tcs, clean, has := DetectToolCalls(cText) + if has && len(tcs) > 0 { + frame.ToolCalls = tcs + frame.Content = clean + } + } + frame.OK = true + return frame + } + + // 4. Check if v[0] is an inner pair [content, reasoning] + if inner, ok := v[0].([]interface{}); ok && len(inner) == 2 { + s0, ok0 := inner[0].(string) + s1, ok1 := inner[1].(string) + if ok0 && ok1 { + frame.Content = s0 + frame.Reasoning = s1 + frame.OK = true return frame } + if ok1 { + frame.Content = s1 + frame.OK = true + return frame + } + } + + // 5. Check if v[0] is a non-empty string or single output + if s, ok := v[0].(string); ok && (s != "" || len(v) == 1) { + frame.Content = s + frame.OK = true + return frame } case map[string]interface{}: diff --git a/gr2gw_test.go b/gr2gw_test.go index 337a6a1..87d63c6 100644 --- a/gr2gw_test.go +++ b/gr2gw_test.go @@ -298,6 +298,13 @@ func TestParseGradioStreamOutput(t *testing.T) { if !frame7.OK || frame7.Content != "Mocked answer" || frame7.Reasoning != "Mocked reasoning" || len(frame7.ToolCalls) != 0 { t.Errorf("unexpected frame7: %+v", frame7) } + + // 8. Gradio 6 Chatbot multi-output format with TextMessage list + g6Raw := `["", [{"role": "user", "content": [{"text": "weather in Tokyo?", "type": "text"}]}, {"role": "assistant", "content": [{"text": "\n{\"name\": \"get_weather\", \"arguments\": {\"city\": \"Tokyo\"}}\n", "type": "text"}]}]]` + frame8 := ParseGradioStreamOutput(g6Raw) + if !frame8.OK || len(frame8.ToolCalls) != 1 || frame8.ToolCalls[0].Function.Name != "get_weather" { + t.Errorf("unexpected frame8: %+v", frame8) + } } func TestHunyuan3BuildPayload(t *testing.T) {