mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-06 20:32:44 +08:00
fix(ai): 修复 OpenAI 结构化输出解析问题并重构代码结构
- 修复 OpenAI structured output 的 properties 包装层解析问题 - 重构 parseCustomSchemaResult 函数,提高代码可维护性: - 拆分为多个职责单一的小函数 - 消除重复的字段提取逻辑 - 采用清晰的策略模式处理不同解析场景 - 增强测试用例,添加具体的数值和结构验证 - 保持完全向后兼容,所有现有测试通过 Fixes: TestQueryFunctionality/ComprehensiveAnalysis 测试失败问题
This commit is contained in:
@@ -1 +1 @@
|
||||
v5.0.0-beta-2506102252
|
||||
v5.0.0-beta-2506111115
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user