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 {
|
if err != nil {
|
||||||
return TableAnalysisResult{}, err
|
return TableAnalysisResult{}, err
|
||||||
}
|
}
|
||||||
|
usage := usageFromResponse(resp)
|
||||||
var out struct {
|
var out struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
@@ -277,18 +278,152 @@ func (s *Service) AnalyzeTable(ctx context.Context, input TableAnalysisInput) (T
|
|||||||
Interpretation string `json:"interpretation"`
|
Interpretation string `json:"interpretation"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal([]byte(ExtractJSONObject(resp.Content)), &out); err != nil {
|
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, "智能解读报告")
|
out.Title = firstNonEmpty(out.Title, input.Topic, input.FileName, "智能解读报告")
|
||||||
if strings.TrimSpace(out.Interpretation) == "" {
|
if strings.TrimSpace(out.Interpretation) == "" {
|
||||||
return TableAnalysisResult{}, ErrBadOutput
|
return fallbackTableAnalysis(input, usage, "AI 输出缺少核心解读,已使用兜底解读。"), nil
|
||||||
}
|
}
|
||||||
return TableAnalysisResult{
|
return TableAnalysisResult{
|
||||||
Title: out.Title, Metrics: out.Metrics, Preview: out.Preview, Alerts: out.Alerts,
|
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
|
}, 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) {
|
func (s *Service) SummarizeMeeting(ctx context.Context, input MeetingSummaryInput) (MeetingSummaryResult, error) {
|
||||||
if s == nil || s.client == nil {
|
if s == nil || s.client == nil {
|
||||||
return MeetingSummaryResult{}, ErrNotConfigured
|
return MeetingSummaryResult{}, ErrNotConfigured
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package ai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFallbackTableAnalysisUnparsedProfile(t *testing.T) {
|
||||||
|
result := fallbackTableAnalysis(TableAnalysisInput{
|
||||||
|
Topic: "销售分析",
|
||||||
|
FileID: "file1",
|
||||||
|
FileName: "sales.xlsx",
|
||||||
|
FileType: "xlsx",
|
||||||
|
FileSize: 5149,
|
||||||
|
Profile: TableProfile{Parsed: false, Message: "source file is not available locally"},
|
||||||
|
}, Usage{Model: "deepseek-flash"}, "AI 输出格式异常,已使用兜底解读。")
|
||||||
|
if result.Title != "销售分析" {
|
||||||
|
t.Fatalf("Title=%q, want 销售分析", result.Title)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(result.Interpretation) == "" {
|
||||||
|
t.Fatal("Interpretation is empty")
|
||||||
|
}
|
||||||
|
if len(result.Alerts) < 2 {
|
||||||
|
t.Fatalf("Alerts len=%d, want at least 2", len(result.Alerts))
|
||||||
|
}
|
||||||
|
if got := result.Metrics[len(result.Metrics)-1].Value; got != "未解析" {
|
||||||
|
t.Fatalf("last metric value=%q, want 未解析", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user