Support Gradio 6 Chatbot format and multi-output spaces like tdecae-personal-chatbot

This commit is contained in:
Luxferre
2026-09-07 11:16:09 +03:00
parent c5ea129b44
commit 9f2a5068f8
2 changed files with 249 additions and 107 deletions
+242 -107
View File
@@ -73,14 +73,14 @@ type ChatMessage struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
} }
func (m *ChatMessage) GetContentString() string { func extractContentString(val interface{}) string {
if m.Content == nil { if val == nil {
return "" return ""
} }
if str, ok := m.Content.(string); ok { if str, ok := val.(string); ok {
return str return str
} }
if parts, ok := m.Content.([]interface{}); ok { if parts, ok := val.([]interface{}); ok {
var sb strings.Builder var sb strings.Builder
for _, p := range parts { for _, p := range parts {
if str, ok := p.(string); ok { if str, ok := p.(string); ok {
@@ -88,16 +88,29 @@ func (m *ChatMessage) GetContentString() string {
} else if itemMap, ok := p.(map[string]interface{}); ok { } else if itemMap, ok := p.(map[string]interface{}); ok {
if textVal, ok := itemMap["text"].(string); ok { if textVal, ok := itemMap["text"].(string); ok {
sb.WriteString(textVal) sb.WriteString(textVal)
} else if valStr, ok := itemMap["value"].(string); ok {
sb.WriteString(valStr)
} }
} }
} }
return sb.String() 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 { if err == nil {
return string(b) 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 { type ChatCompletionRequest struct {
@@ -1520,6 +1533,8 @@ type GradioParamInfo struct {
ParameterName string `json:"parameter_name"` ParameterName string `json:"parameter_name"`
ParameterDefault interface{} `json:"parameter_default,omitempty"` ParameterDefault interface{} `json:"parameter_default,omitempty"`
Component string `json:"component"` Component string `json:"component"`
Type interface{} `json:"type,omitempty"`
PythonType interface{} `json:"python_type,omitempty"`
} }
type GradioEndpointInfo struct { type GradioEndpointInfo struct {
@@ -1828,20 +1843,32 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
score := 0 score := 0
lowerName := strings.ToLower(epName) 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 score += 100
} }
if strings.Contains(lowerName, "predict") || strings.Contains(lowerName, "respond") || strings.Contains(lowerName, "generate") { if strings.Contains(lowerName, "answer") || strings.Contains(lowerName, "respond") || strings.Contains(lowerName, "generate") || strings.Contains(lowerName, "predict") || strings.Contains(lowerName, "completion") {
score += 50 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 { for _, p := range epInfo.Parameters {
pLower := strings.ToLower(p.ParameterName) 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 score += 40
} }
if strings.Contains(pLower, "history") || strings.Contains(pLower, "chat") { if strings.Contains(pLower, "history") || strings.Contains(pLower, "chat") || strings.Contains(pLower, "messages") || strings.Contains(pLower, "conversation") || pComp == "chatbot" {
score += 20 score += 30
} }
} }
@@ -1943,6 +1970,7 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
} }
for idx, p := range bestEndpointInfo.Parameters { for idx, p := range bestEndpointInfo.Parameters {
pName := strings.ToLower(p.ParameterName) pName := strings.ToLower(p.ParameterName)
pComp := strings.ToLower(p.Component)
if strings.Contains(pName, "system") { if strings.Contains(pName, "system") {
discovery.SystemIndex = idx discovery.SystemIndex = idx
if p.ParameterDefault != nil && discovery.DefaultSystemPrompt == "" { if p.ParameterDefault != nil && discovery.DefaultSystemPrompt == "" {
@@ -1950,9 +1978,20 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
discovery.DefaultSystemPrompt = 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") || strings.Contains(pName, "conversation") || strings.Contains(pName, "messages") || pComp == "chatbot" {
discovery.HistoryIndex = idx 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 discovery.MessageIndex = idx
} else if strings.Contains(pName, "think_level") || strings.Contains(pName, "thinking_level") { } else if strings.Contains(pName, "think_level") || strings.Contains(pName, "thinking_level") {
discovery.ThinkLevelIndex = idx 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") { if discovery.FunctionsJSONIndex != -1 || discovery.ThinkLevelIndex != -1 || strings.Contains(cleanURL, "hy3") || strings.Contains(cleanURL, "hunyuan") {
discovery.IsHunyuan3 = true discovery.IsHunyuan3 = true
discovery.HistoryFormat = "messages"
discovery.Models = append(discovery.Models, "hy3", "hunyuan3", "tencent/Hy3") discovery.Models = append(discovery.Models, "hy3", "hunyuan3", "tencent/Hy3")
if discovery.PrimaryModel == "gradio-chat" || discovery.PrimaryModel == "" { if discovery.PrimaryModel == "gradio-chat" || discovery.PrimaryModel == "" {
discovery.PrimaryModel = "hy3" discovery.PrimaryModel = "hy3"
@@ -2301,6 +2341,26 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
pairs = append(pairs, []string{u, a}) pairs = append(pairs, []string{u, a})
} }
data[disc.HistoryIndex] = pairs 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 { } else {
data[disc.HistoryIndex] = historyArray data[disc.HistoryIndex] = historyArray
} }
@@ -2393,107 +2453,182 @@ func ParseGradioStreamOutput(rawJSON string) GradioOutputFrame {
return frame return frame
} }
// Check if v[0] is an inner slice (e.g. Hy3: [[content, reasoning, tool_calls, history]]) // 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 { if inner, ok := v[0].([]interface{}); ok && len(inner) >= 3 {
if len(inner) >= 3 { s0, _ := inner[0].(string)
s0, _ := inner[0].(string) s1, _ := inner[1].(string)
s1, _ := inner[1].(string) frame.Content = s0
frame.Content = s0 frame.Reasoning = s1
frame.Reasoning = s1 if inner[2] != nil {
if inner[2] != nil { b, err := json.Marshal(inner[2])
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)
if err == nil { if err == nil {
var tcs []ToolCall var tcs []ToolCall
if err := json.Unmarshal(b, &tcs); err == nil { if json.Unmarshal(b, &tcs) == nil && len(tcs) > 0 {
frame.ToolCalls = tcs 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 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{}: case map[string]interface{}:
+7
View File
@@ -298,6 +298,13 @@ func TestParseGradioStreamOutput(t *testing.T) {
if !frame7.OK || frame7.Content != "Mocked answer" || frame7.Reasoning != "Mocked reasoning" || len(frame7.ToolCalls) != 0 { if !frame7.OK || frame7.Content != "Mocked answer" || frame7.Reasoning != "Mocked reasoning" || len(frame7.ToolCalls) != 0 {
t.Errorf("unexpected frame7: %+v", frame7) 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": "<tool_call>\n{\"name\": \"get_weather\", \"arguments\": {\"city\": \"Tokyo\"}}\n</tool_call>", "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) { func TestHunyuan3BuildPayload(t *testing.T) {