fix(ai): 修复 OpenAI 结构化输出解析问题并重构代码结构

- 修复 OpenAI structured output 的 properties 包装层解析问题
- 重构 parseCustomSchemaResult 函数,提高代码可维护性:
  - 拆分为多个职责单一的小函数
  - 消除重复的字段提取逻辑
  - 采用清晰的策略模式处理不同解析场景
- 增强测试用例,添加具体的数值和结构验证
- 保持完全向后兼容,所有现有测试通过

Fixes: TestQueryFunctionality/ComprehensiveAnalysis 测试失败问题
This commit is contained in:
lilong.129
2025-06-11 11:15:02 +08:00
parent caf75b087b
commit 50414ec74d
3 changed files with 131 additions and 95 deletions

View File

@@ -1 +1 @@
v5.0.0-beta-2506102252
v5.0.0-beta-2506111115

View File

@@ -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

View File

@@ -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)
})
}