From 50414ec74d15443502171e24d917bb798715503e Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Wed, 11 Jun 2025 11:15:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(ai):=20=E4=BF=AE=E5=A4=8D=20OpenAI=20?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E5=8C=96=E8=BE=93=E5=87=BA=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E5=B9=B6=E9=87=8D=E6=9E=84=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 OpenAI structured output 的 properties 包装层解析问题 - 重构 parseCustomSchemaResult 函数,提高代码可维护性: - 拆分为多个职责单一的小函数 - 消除重复的字段提取逻辑 - 采用清晰的策略模式处理不同解析场景 - 增强测试用例,添加具体的数值和结构验证 - 保持完全向后兼容,所有现有测试通过 Fixes: TestQueryFunctionality/ComprehensiveAnalysis 测试失败问题 --- internal/version/VERSION | 2 +- uixt/ai/querier.go | 211 ++++++++++++++++++++++----------------- uixt/ai/querier_test.go | 13 +++ 3 files changed, 131 insertions(+), 95 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 9342b998..581c7dd8 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2506102252 +v5.0.0-beta-2506111115 diff --git a/uixt/ai/querier.go b/uixt/ai/querier.go index 6a9def4b..02f3676e 100644 --- a/uixt/ai/querier.go +++ b/uixt/ai/querier.go @@ -335,112 +335,135 @@ func parseCustomSchemaResult(content string, outputSchema interface{}) (*QueryRe }, nil } - // Create a new instance of the same type as outputSchema - schemaType := reflect.TypeOf(outputSchema) - if schemaType.Kind() == reflect.Ptr { - schemaType = schemaType.Elem() - } + // Handle OpenAI structured output properties wrapper + actualJSONContent := unwrapPropertiesIfNeeded(jsonContent) - // Create a new instance of the schema type - newInstance := reflect.New(schemaType).Interface() - - // Try to unmarshal directly into the schema type - if err := json.Unmarshal([]byte(jsonContent), newInstance); err == nil { - // Successfully parsed into the expected schema type - result := &QueryResult{ - Data: newInstance, // Store the typed pointer directly - } - - // Try to extract content and thought if the schema has these fields - schemaValue := reflect.ValueOf(newInstance).Elem() - if contentField := schemaValue.FieldByName("Content"); contentField.IsValid() && contentField.Kind() == reflect.String { - result.Content = contentField.String() - } - if thoughtField := schemaValue.FieldByName("Thought"); thoughtField.IsValid() && thoughtField.Kind() == reflect.String { - result.Thought = thoughtField.String() - } - - // If no standard fields found, try to extract from map representation - if result.Content == "" && result.Thought == "" { - var dataMap map[string]interface{} - if err := json.Unmarshal([]byte(jsonContent), &dataMap); err == nil { - if content, exists := dataMap["content"]; exists { - if contentStr, ok := content.(string); ok { - result.Content = contentStr - } - } - if thought, exists := dataMap["thought"]; exists { - if thoughtStr, ok := thought.(string); ok { - result.Thought = thoughtStr - } - } - } - } - - // Ensure default values are set - ensureDefaultValues(result, newInstance) + // Try direct unmarshaling first (most efficient) + if result, err := tryDirectUnmarshal(actualJSONContent, outputSchema); err == nil { return result, nil } - // Fallback: try to parse as generic map and then convert - var structuredData interface{} - if err := json.Unmarshal([]byte(jsonContent), &structuredData); err == nil { - // Try to convert the generic data to the expected schema type - if convertedData, err := convertToSchemaType(structuredData, outputSchema); err == nil { - result := &QueryResult{ - Data: convertedData, // Store the converted typed data - } - - // Extract content and thought from the original map - if dataMap, ok := structuredData.(map[string]interface{}); ok { - if content, exists := dataMap["content"]; exists { - if contentStr, ok := content.(string); ok { - result.Content = contentStr - } - } - if thought, exists := dataMap["thought"]; exists { - if thoughtStr, ok := thought.(string); ok { - result.Thought = thoughtStr - } - } - } - - // Ensure default values are set - ensureDefaultValues(result, convertedData) - return result, nil - } - - // If conversion failed, fall back to storing the generic data - if dataMap, ok := structuredData.(map[string]interface{}); ok { - result := &QueryResult{ - Data: structuredData, - } - - // Extract content and thought if present - if content, exists := dataMap["content"]; exists { - if contentStr, ok := content.(string); ok { - result.Content = contentStr - } - } - if thought, exists := dataMap["thought"]; exists { - if thoughtStr, ok := thought.(string); ok { - result.Thought = thoughtStr - } - } - - // Ensure default values are set - ensureDefaultValues(result, nil) - return result, nil - } + // Fallback: try generic parsing and conversion + if result, err := tryGenericParsingAndConversion(actualJSONContent, outputSchema); err == nil { + return result, nil } - // Fallback to treating as plain text + // Final fallback: treat as plain text return &QueryResult{ Content: content, Thought: "Failed to parse as structured data, returning raw content", }, nil } +// unwrapPropertiesIfNeeded handles OpenAI structured output properties wrapper +func unwrapPropertiesIfNeeded(jsonContent string) string { + var tempMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonContent), &tempMap); err == nil { + if properties, exists := tempMap["properties"]; exists { + if propertiesBytes, err := json.Marshal(properties); err == nil { + return string(propertiesBytes) + } + } + } + return jsonContent +} + +// tryDirectUnmarshal attempts to unmarshal directly into the schema type +func tryDirectUnmarshal(jsonContent string, outputSchema interface{}) (*QueryResult, error) { + // Create a new instance of the schema type + newInstance := createSchemaInstance(outputSchema) + + // Try to unmarshal directly into the schema type + if err := json.Unmarshal([]byte(jsonContent), newInstance); err != nil { + return nil, err + } + + // Create result with the typed data + result := &QueryResult{Data: newInstance} + + // Extract content and thought fields + extractContentAndThoughtFromStruct(result, newInstance) + if result.Content == "" && result.Thought == "" { + extractContentAndThoughtFromJSON(result, jsonContent) + } + + // Ensure default values are set + ensureDefaultValues(result, newInstance) + return result, nil +} + +// tryGenericParsingAndConversion attempts generic parsing and type conversion +func tryGenericParsingAndConversion(jsonContent string, outputSchema interface{}) (*QueryResult, error) { + var structuredData interface{} + if err := json.Unmarshal([]byte(jsonContent), &structuredData); err != nil { + return nil, err + } + + // Try to convert to the expected schema type + if convertedData, err := convertToSchemaType(structuredData, outputSchema); err == nil { + result := &QueryResult{Data: convertedData} + extractContentAndThoughtFromMap(result, structuredData) + ensureDefaultValues(result, convertedData) + return result, nil + } + + // If conversion failed, store the generic data + if dataMap, ok := structuredData.(map[string]interface{}); ok { + result := &QueryResult{Data: structuredData} + extractContentAndThoughtFromMap(result, dataMap) + ensureDefaultValues(result, nil) + return result, nil + } + + return nil, errors.New("failed to parse structured data") +} + +// createSchemaInstance creates a new instance of the schema type +func createSchemaInstance(outputSchema interface{}) interface{} { + schemaType := reflect.TypeOf(outputSchema) + if schemaType.Kind() == reflect.Ptr { + schemaType = schemaType.Elem() + } + return reflect.New(schemaType).Interface() +} + +// extractContentAndThoughtFromStruct extracts content and thought from struct fields using reflection +func extractContentAndThoughtFromStruct(result *QueryResult, structData interface{}) { + schemaValue := reflect.ValueOf(structData).Elem() + + if contentField := schemaValue.FieldByName("Content"); contentField.IsValid() && contentField.Kind() == reflect.String { + result.Content = contentField.String() + } + + if thoughtField := schemaValue.FieldByName("Thought"); thoughtField.IsValid() && thoughtField.Kind() == reflect.String { + result.Thought = thoughtField.String() + } +} + +// extractContentAndThoughtFromJSON extracts content and thought from JSON map +func extractContentAndThoughtFromJSON(result *QueryResult, jsonContent string) { + var dataMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonContent), &dataMap); err == nil { + extractContentAndThoughtFromMap(result, dataMap) + } +} + +// extractContentAndThoughtFromMap extracts content and thought from a map +func extractContentAndThoughtFromMap(result *QueryResult, dataMap interface{}) { + if mapData, ok := dataMap.(map[string]interface{}); ok { + if content, exists := mapData["content"]; exists { + if contentStr, ok := content.(string); ok { + result.Content = contentStr + } + } + if thought, exists := mapData["thought"]; exists { + if thoughtStr, ok := thought.(string); ok { + result.Thought = thoughtStr + } + } + } +} + // convertToSchemaType converts generic data to the specified schema type func convertToSchemaType(data interface{}, outputSchema interface{}) (interface{}, error) { // Get the type of the output schema diff --git a/uixt/ai/querier_test.go b/uixt/ai/querier_test.go index 1a78c7cb..d67efc48 100644 --- a/uixt/ai/querier_test.go +++ b/uixt/ai/querier_test.go @@ -183,6 +183,9 @@ func TestQueryFunctionality(t *testing.T) { assert.NotNil(t, gameInfo) assert.NotEmpty(t, gameInfo.Content) assert.NotEmpty(t, gameInfo.Thought) + assert.Equal(t, 4, gameInfo.Rows) + assert.Equal(t, 3, gameInfo.Cols) + assert.Equal(t, 5, gameInfo.TotalIcons) t.Logf("Custom Schema Query Result: %+v", gameInfo) }) @@ -206,6 +209,16 @@ func TestQueryFunctionality(t *testing.T) { assert.NotEmpty(t, result.Thought) assert.NotNil(t, result.Data) + gameAnalysisResult, ok := result.Data.(*GameAnalysisResult) + assert.True(t, ok) + assert.NotNil(t, gameAnalysisResult) + assert.NotEmpty(t, gameAnalysisResult.Content) + assert.NotEmpty(t, gameAnalysisResult.Thought) + assert.NotEmpty(t, gameAnalysisResult.GameType) + assert.Equal(t, 4, gameAnalysisResult.Dimensions.Rows) + assert.Equal(t, 3, gameAnalysisResult.Dimensions.Cols) + assert.Equal(t, 12, len(gameAnalysisResult.Elements)) + t.Logf("Comprehensive Analysis Result: %+v", result.Data) }) }