fix: fallback on invalid ai table output
This commit is contained in:
+138
-3
@@ -268,6 +268,7 @@ func (s *Service) AnalyzeTable(ctx context.Context, input TableAnalysisInput) (T
|
||||
if err != nil {
|
||||
return TableAnalysisResult{}, err
|
||||
}
|
||||
usage := usageFromResponse(resp)
|
||||
var out struct {
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary"`
|
||||
@@ -277,18 +278,152 @@ func (s *Service) AnalyzeTable(ctx context.Context, input TableAnalysisInput) (T
|
||||
Interpretation string `json:"interpretation"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(ExtractJSONObject(resp.Content)), &out); err != nil {
|
||||
return TableAnalysisResult{}, fmt.Errorf("%w: %v", ErrBadOutput, err)
|
||||
return fallbackTableAnalysis(input, usage, "AI 输出格式异常,已使用兜底解读。"), nil
|
||||
}
|
||||
out.Title = firstNonEmpty(out.Title, input.Topic, input.FileName, "智能解读报告")
|
||||
if strings.TrimSpace(out.Interpretation) == "" {
|
||||
return TableAnalysisResult{}, ErrBadOutput
|
||||
return fallbackTableAnalysis(input, usage, "AI 输出缺少核心解读,已使用兜底解读。"), nil
|
||||
}
|
||||
return TableAnalysisResult{
|
||||
Title: out.Title, Metrics: out.Metrics, Preview: out.Preview, Alerts: out.Alerts,
|
||||
Interpretation: strings.TrimSpace(out.Interpretation), Summary: strings.TrimSpace(out.Summary), Usage: usageFromResponse(resp),
|
||||
Interpretation: strings.TrimSpace(out.Interpretation), Summary: strings.TrimSpace(out.Summary), Usage: usage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fallbackTableAnalysis(input TableAnalysisInput, usage Usage, reason string) TableAnalysisResult {
|
||||
title := firstNonEmpty(input.Topic, input.FileName, "智能表格解读报告")
|
||||
preview := fallbackTablePreview(input.Profile)
|
||||
metrics := fallbackTableMetrics(input, preview)
|
||||
alerts := []TableAlert{{
|
||||
Level: "warning",
|
||||
Title: "已启用兜底解读",
|
||||
Description: reason,
|
||||
}}
|
||||
if !input.Profile.Parsed {
|
||||
alerts = append(alerts, TableAlert{
|
||||
Level: "warning",
|
||||
Title: "表格内容未解析",
|
||||
Description: firstNonEmpty(input.Profile.Message, "当前无法读取表格内容,仅基于文件元信息生成解读。"),
|
||||
})
|
||||
}
|
||||
interpretation := fallbackTableInterpretation(input, preview, reason)
|
||||
return TableAnalysisResult{
|
||||
Title: title,
|
||||
Summary: excerptPlain(interpretation, 120),
|
||||
Metrics: metrics,
|
||||
Preview: preview,
|
||||
Alerts: alerts,
|
||||
Interpretation: interpretation,
|
||||
Usage: usage,
|
||||
}
|
||||
}
|
||||
|
||||
func fallbackTablePreview(profile TableProfile) TablePreview {
|
||||
var columns []struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
_ = decodeProfileAny(profile.Columns, &columns)
|
||||
names := make([]string, 0, len(columns))
|
||||
for _, column := range columns {
|
||||
if name := strings.TrimSpace(column.Name); name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
var rows [][]string
|
||||
_ = decodeProfileAny(profile.SampleRows, &rows)
|
||||
if len(rows) > 5 {
|
||||
rows = rows[:5]
|
||||
}
|
||||
return TablePreview{Columns: names, Rows: rows, TotalRows: profile.RowCount}
|
||||
}
|
||||
|
||||
func fallbackTableMetrics(input TableAnalysisInput, preview TablePreview) []TableMetric {
|
||||
metrics := []TableMetric{
|
||||
{Name: "文件类型", Value: strings.ToUpper(input.FileType), ChangeText: "来自上传元信息", Trend: "flat"},
|
||||
{Name: "文件大小", Value: fmt.Sprintf("%d bytes", input.FileSize), ChangeText: "来自上传元信息", Trend: "flat"},
|
||||
}
|
||||
if input.Profile.Parsed {
|
||||
metrics = append(metrics,
|
||||
TableMetric{Name: "数据行数", Value: fmt.Sprintf("%d", preview.TotalRows), ChangeText: "来自表格解析", Trend: "flat"},
|
||||
TableMetric{Name: "字段数", Value: fmt.Sprintf("%d", len(preview.Columns)), ChangeText: "来自表格解析", Trend: "flat"},
|
||||
)
|
||||
} else {
|
||||
metrics = append(metrics, TableMetric{Name: "解析状态", Value: "未解析", ChangeText: firstNonEmpty(input.Profile.Message, "source file is not available locally"), Trend: "flat"})
|
||||
}
|
||||
var stats map[string]struct {
|
||||
Count int `json:"count"`
|
||||
Sum float64 `json:"sum"`
|
||||
Avg float64 `json:"avg"`
|
||||
Min float64 `json:"min"`
|
||||
Max float64 `json:"max"`
|
||||
}
|
||||
if err := decodeProfileAny(input.Profile.Stats, &stats); err == nil {
|
||||
for name, stat := range stats {
|
||||
if len(metrics) >= 6 {
|
||||
break
|
||||
}
|
||||
metrics = append(metrics, TableMetric{
|
||||
Name: name,
|
||||
Value: fmt.Sprintf("平均 %.2f", stat.Avg),
|
||||
ChangeText: fmt.Sprintf("样本 %d,范围 %.2f-%.2f", stat.Count, stat.Min, stat.Max),
|
||||
Trend: "flat",
|
||||
})
|
||||
}
|
||||
}
|
||||
return metrics
|
||||
}
|
||||
|
||||
func fallbackTableInterpretation(input TableAnalysisInput, preview TablePreview, reason string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(reason)
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString("文件《")
|
||||
b.WriteString(firstNonEmpty(input.FileName, input.FileID, "未命名文件"))
|
||||
b.WriteString("》已进入智能表格解读流程。")
|
||||
if input.Profile.Parsed {
|
||||
b.WriteString(fmt.Sprintf("当前已解析到 %d 行数据、%d 个字段。", preview.TotalRows, len(preview.Columns)))
|
||||
if len(preview.Columns) > 0 {
|
||||
b.WriteString("字段包括:")
|
||||
b.WriteString(strings.Join(preview.Columns, "、"))
|
||||
b.WriteString("。")
|
||||
}
|
||||
b.WriteString("建议结合字段含义继续查看数值分布、异常值和业务口径。")
|
||||
} else {
|
||||
b.WriteString("当前未能读取表格内容,报告仅基于文件名、类型和大小生成。")
|
||||
if msg := strings.TrimSpace(input.Profile.Message); msg != "" {
|
||||
b.WriteString("未解析原因:")
|
||||
b.WriteString(msg)
|
||||
b.WriteString("。")
|
||||
}
|
||||
b.WriteString("建议确认文件已上传完成、格式正确后重新发起解读。")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func decodeProfileAny(value any, target any) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, target)
|
||||
}
|
||||
|
||||
func excerptPlain(value string, maxRunes int) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if maxRunes <= 0 {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(value)
|
||||
if len(runes) <= maxRunes {
|
||||
return value
|
||||
}
|
||||
return string(runes[:maxRunes])
|
||||
}
|
||||
|
||||
func (s *Service) SummarizeMeeting(ctx context.Context, input MeetingSummaryInput) (MeetingSummaryResult, error) {
|
||||
if s == nil || s.client == nil {
|
||||
return MeetingSummaryResult{}, ErrNotConfigured
|
||||
|
||||
Reference in New Issue
Block a user