feat: universal heuristic engine for gradio version, flavor, protocol and tool calling resolution
This commit is contained in:
+281
@@ -1597,4 +1597,285 @@ func TestMultimodalStateSpaceMockServerCompletion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGradio3DirectPredictProtocol verifies discovery and chat completion against a Gradio 3 space
|
||||
// where /gradio_api/info returns 404 and the protocol resolves to /run/predict with tuple pairs history.
|
||||
func TestGradio3DirectPredictProtocol(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/gradio_api/info" || r.URL.Path == "/info" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Path == "/config" {
|
||||
cfg := GradioConfigResponse{
|
||||
Version: "3.41.2",
|
||||
Mode: "chat_interface",
|
||||
Title: "Legacy Gradio 3 Chat",
|
||||
Components: []GradioComponent{
|
||||
{ID: 1, Type: "textbox", Props: map[string]interface{}{"label": "Input"}},
|
||||
{ID: 2, Type: "chatbot", Props: map[string]interface{}{"label": "Chatbot"}},
|
||||
},
|
||||
Dependencies: []GradioDependency{
|
||||
{
|
||||
ID: 0,
|
||||
Inputs: []int{1, 2},
|
||||
Outputs: []int{2},
|
||||
},
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(cfg)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Path == "/run/predict" {
|
||||
var body struct {
|
||||
Data []interface{} `json:"data"`
|
||||
FnIndex int `json:"fn_index"`
|
||||
SessionHash string `json:"session_hash"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.FnIndex != 0 {
|
||||
http.Error(w, fmt.Sprintf("expected fn_index 0, got %d", body.FnIndex), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
msg, _ := body.Data[0].(string)
|
||||
reply := "Echo from Gradio 3: " + msg
|
||||
respData := map[string]interface{}{
|
||||
"data": []interface{}{
|
||||
[][]string{
|
||||
{msg, reply},
|
||||
},
|
||||
},
|
||||
"is_generating": false,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(respData)
|
||||
return
|
||||
}
|
||||
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
gw := NewGradioGateway(ts.URL, "", 10*time.Second)
|
||||
disc := gw.GetDiscovery(ts.URL, DefaultUserAgent)
|
||||
|
||||
if disc.GradioVersion != "3.41.2" {
|
||||
t.Errorf("expected GradioVersion 3.41.2, got %s", disc.GradioVersion)
|
||||
}
|
||||
if disc.Protocol != "predict" {
|
||||
t.Errorf("expected Protocol predict, got %s", disc.Protocol)
|
||||
}
|
||||
if disc.Flavor != "ChatInterface" {
|
||||
t.Errorf("expected Flavor ChatInterface, got %s", disc.Flavor)
|
||||
}
|
||||
if disc.HistoryFormat != "pairs" {
|
||||
t.Errorf("expected HistoryFormat pairs, got %s", disc.HistoryFormat)
|
||||
}
|
||||
if disc.FnIndex != 0 {
|
||||
t.Errorf("expected FnIndex 0, got %d", disc.FnIndex)
|
||||
}
|
||||
|
||||
// Non-streaming completion
|
||||
req := ChatCompletionRequest{
|
||||
Messages: []ChatMessage{
|
||||
{Role: "user", Content: "Hello Gradio 3!"},
|
||||
},
|
||||
Stream: false,
|
||||
}
|
||||
b, _ := json.Marshal(req)
|
||||
httpReq := httptest.NewRequest("POST", "/v1/chat/completions", bytes.NewBuffer(b))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
if err := gw.ExecuteChatCompletion(rec, httpReq, req); err != nil {
|
||||
t.Fatalf("ExecuteChatCompletion failed on Gradio 3: %v", err)
|
||||
}
|
||||
|
||||
var res ChatCompletionResponse
|
||||
if err := json.NewDecoder(rec.Body).Decode(&res); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if res.Choices[0].Message.Content != "Echo from Gradio 3: Hello Gradio 3!" {
|
||||
t.Errorf("unexpected completion content: %v", res.Choices[0].Message.Content)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEndpointScoringDisambiguation verifies that chat generation endpoints are selected over
|
||||
// UI utility, reset, voting, and feedback endpoints.
|
||||
func TestEndpointScoringDisambiguation(t *testing.T) {
|
||||
compMap := map[int]GradioComponent{
|
||||
1: {ID: 1, Type: "textbox", Props: map[string]interface{}{"label": "Message"}},
|
||||
2: {ID: 2, Type: "chatbot", Props: map[string]interface{}{"label": "Chat"}},
|
||||
3: {ID: 3, Type: "state", Props: map[string]interface{}{"label": "State"}},
|
||||
}
|
||||
|
||||
chatDep := GradioDependency{
|
||||
ID: 0,
|
||||
Inputs: []int{1, 2, 3},
|
||||
Outputs: []int{2},
|
||||
Types: GradioDependencyTypes{Generator: true},
|
||||
}
|
||||
clearDep := GradioDependency{
|
||||
ID: 1,
|
||||
Inputs: []int{2},
|
||||
Outputs: []int{2},
|
||||
}
|
||||
voteDep := GradioDependency{
|
||||
ID: 2,
|
||||
Inputs: []int{2},
|
||||
Outputs: []int{},
|
||||
}
|
||||
|
||||
chatScore := ScoreCandidateEndpoint("chat", nil, &chatDep, compMap)
|
||||
clearScore := ScoreCandidateEndpoint("clear", nil, &clearDep, compMap)
|
||||
voteScore := ScoreCandidateEndpoint("vote", nil, &voteDep, compMap)
|
||||
resetScore := ScoreCandidateEndpoint("reset_all", nil, &clearDep, compMap)
|
||||
|
||||
if chatScore <= 0 {
|
||||
t.Errorf("expected positive chat score, got %d", chatScore)
|
||||
}
|
||||
if clearScore >= chatScore {
|
||||
t.Errorf("expected chat score > clear score, got chat=%d clear=%d", chatScore, clearScore)
|
||||
}
|
||||
if voteScore >= chatScore {
|
||||
t.Errorf("expected chat score > vote score, got chat=%d vote=%d", chatScore, voteScore)
|
||||
}
|
||||
if resetScore >= chatScore {
|
||||
t.Errorf("expected chat score > reset score, got chat=%d reset=%d", chatScore, resetScore)
|
||||
}
|
||||
}
|
||||
|
||||
// TestToolCallingPictureResolution verifies that tool calling mode is accurately resolved
|
||||
// based on component topology.
|
||||
func TestToolCallingPictureResolution(t *testing.T) {
|
||||
// Case A: Native slot
|
||||
discNative := NewDefaultSpaceDiscovery("https://tencent-hy3.hf.space")
|
||||
discNative.FunctionsJSONIndex = 8
|
||||
discNative.SystemIndex = 2
|
||||
discNative.HistoryIndex = 1
|
||||
discNative.ToolCallMode = "native_slot"
|
||||
if discNative.ToolCallMode != "native_slot" {
|
||||
t.Errorf("expected native_slot, got %s", discNative.ToolCallMode)
|
||||
}
|
||||
|
||||
// Case B: Dedicated system prompt slot
|
||||
discSys := NewDefaultSpaceDiscovery("https://custom-chat.hf.space")
|
||||
discSys.FunctionsJSONIndex = -1
|
||||
discSys.SystemIndex = 2
|
||||
discSys.HistoryIndex = 1
|
||||
discSys.ToolCallMode = "prompt_augmented_system"
|
||||
if discSys.ToolCallMode != "prompt_augmented_system" {
|
||||
t.Errorf("expected prompt_augmented_system, got %s", discSys.ToolCallMode)
|
||||
}
|
||||
|
||||
// Case C: Conversation history (first turn)
|
||||
discFirst := NewDefaultSpaceDiscovery("https://chat-only.hf.space")
|
||||
discFirst.FunctionsJSONIndex = -1
|
||||
discFirst.SystemIndex = -1
|
||||
discFirst.HistoryIndex = 1
|
||||
discFirst.ToolCallMode = "prompt_augmented_first_turn"
|
||||
if discFirst.ToolCallMode != "prompt_augmented_first_turn" {
|
||||
t.Errorf("expected prompt_augmented_first_turn, got %s", discFirst.ToolCallMode)
|
||||
}
|
||||
|
||||
// Case D: Single prompt input
|
||||
discSingle := NewDefaultSpaceDiscovery("https://single-prompt.hf.space")
|
||||
discSingle.FunctionsJSONIndex = -1
|
||||
discSingle.SystemIndex = -1
|
||||
discSingle.HistoryIndex = -1
|
||||
discSingle.ToolCallMode = "prompt_augmented_single_prompt"
|
||||
if discSingle.ToolCallMode != "prompt_augmented_single_prompt" {
|
||||
t.Errorf("expected prompt_augmented_single_prompt, got %s", discSingle.ToolCallMode)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCallToPredictProtocolFallback verifies that if a space reports /call support in /info
|
||||
// but /call returns 404 at runtime, gr2gw gracefully falls back to /run/predict.
|
||||
func TestCallToPredictProtocolFallback(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/gradio_api/info" {
|
||||
info := GradioAPIInfoResponse{
|
||||
NamedEndpoints: map[string]GradioEndpointInfo{
|
||||
"/chat": {
|
||||
Parameters: []GradioParamInfo{
|
||||
{ParameterName: "prompt", Label: "Prompt", Component: "Textbox"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(info)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Path == "/config" {
|
||||
cfg := GradioConfigResponse{
|
||||
Version: "4.20.0",
|
||||
Mode: "interface",
|
||||
Components: []GradioComponent{
|
||||
{ID: 1, Type: "textbox", Props: map[string]interface{}{"label": "Prompt"}},
|
||||
},
|
||||
Dependencies: []GradioDependency{
|
||||
{ID: 0, APIName: "/chat", Inputs: []int{1}, Outputs: []int{1}},
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(cfg)
|
||||
return
|
||||
}
|
||||
|
||||
// /call/chat returns 404 (endpoint disabled or unsupported)
|
||||
if strings.HasPrefix(r.URL.Path, "/gradio_api/call/") || strings.HasPrefix(r.URL.Path, "/call/") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback /run/predict works
|
||||
if r.URL.Path == "/run/predict" || r.URL.Path == "/gradio_api/run/predict" {
|
||||
var body struct {
|
||||
Data []interface{} `json:"data"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&body)
|
||||
msg, _ := body.Data[0].(string)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"data": []interface{}{"Fallback response for: " + msg},
|
||||
"is_generating": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
gw := NewGradioGateway(ts.URL, "", 10*time.Second)
|
||||
|
||||
req := ChatCompletionRequest{
|
||||
Messages: []ChatMessage{
|
||||
{Role: "user", Content: "Testing fallback"},
|
||||
},
|
||||
Stream: false,
|
||||
}
|
||||
b, _ := json.Marshal(req)
|
||||
httpReq := httptest.NewRequest("POST", "/v1/chat/completions", bytes.NewBuffer(b))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
if err := gw.ExecuteChatCompletion(rec, httpReq, req); err != nil {
|
||||
t.Fatalf("ExecuteChatCompletion fallback failed: %v", err)
|
||||
}
|
||||
|
||||
var res ChatCompletionResponse
|
||||
if err := json.NewDecoder(rec.Body).Decode(&res); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if res.Choices[0].Message.Content != "Fallback response for: Testing fallback" {
|
||||
t.Errorf("unexpected content: %v", res.Choices[0].Message.Content)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user