Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion backend/internal/application/processing/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ func fileProcessingStatusFromFileObject(fileObj *domainconversation.FileObject)
CompletedAt: fileObj.ProcessingCompletedAt,
UpdatedAt: fileObj.UpdatedAt,
}
dto.ErrorMessage = HumanizeFileProcessingError(dto.FileCategory, dto.ErrorCode, dto.ErrorMessage)
dto.ErrorMessage = ResolveFileProcessingErrorMessage(dto.FileCategory, dto.ProcessingStatus, dto.ErrorCode, dto.ErrorMessage)
return dto
}

Expand Down Expand Up @@ -1438,6 +1438,15 @@ func processingErrorSummary(err error) string {
return HumanizeFileProcessingError("", code, "")
}

// ResolveFileProcessingErrorMessage formats response errors without inventing a failure
// for files that have no processing error code. Legacy failed rows still get a safe fallback.
func ResolveFileProcessingErrorMessage(fileCategory, processingStatus, code, message string) string {
if !strings.EqualFold(strings.TrimSpace(processingStatus), "failed") && strings.TrimSpace(code) == "" {
return ""
}
return HumanizeFileProcessingError(fileCategory, code, message)
}

func HumanizeFileProcessingError(fileCategory string, code string, _ string) string {
normalizedCode := strings.ToLower(strings.TrimSpace(code))
if normalizedCode == "" {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package processing

import (
"testing"

model "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/conversation"
)

func TestFileProcessingStatusDoesNotInventFailureMessages(t *testing.T) {
for _, category := range []string{"pdf", "document", "image"} {
for _, status := range []string{"uploaded", "queued", "extracting", "embedding", "ready"} {
t.Run(category+"/"+status, func(t *testing.T) {
file := &model.FileObject{
FileCategory: category, ProcessingStatus: status,
ProcessingReady: status == "ready", ExtractStatus: "ready", EmbedStatus: "none",
ExtractEngine: "mineru", ExtractChars: 638, ExtractPages: 1,
}
got := fileProcessingStatusFromFileObject(file)
if got.ErrorCode != "" || got.ErrorMessage != "" {
t.Fatalf("file without an error acquired a failure: code=%q message=%q", got.ErrorCode, got.ErrorMessage)
}
})
}
}
}

func TestFileProcessingStatusEmptyAndEmbeddingFailureStaySeparate(t *testing.T) {
for _, file := range []model.FileObject{
{FileCategory: "pdf", ProcessingStatus: "ready", ExtractStatus: "empty", EmbedStatus: "empty"},
{FileCategory: "pdf", ProcessingStatus: "ready", ProcessingReady: true, ExtractStatus: "ready", EmbedStatus: "failed", EmbedError: "embedding unavailable"},
{FileCategory: "pdf", ProcessingStatus: "ready", ProcessingReady: true, ProcessingErrorCode: " \t", ProcessingErrorMessage: " \n"},
} {
got := fileProcessingStatusFromFileObject(&file)
if got.ErrorMessage != "" {
t.Fatalf("non-processing failure acquired a processing error: %q", got.ErrorMessage)
}
if got.EmbedError != file.EmbedError {
t.Fatalf("embedding error changed: %q", got.EmbedError)
}
}
}

func TestFileProcessingStatusKeepsSafeFailureMessages(t *testing.T) {
for _, tc := range []struct {
name, status, code, message, want string
}{
{"coded failure", "failed", "extract_timeout", "", "文件提取超时,请稍后重试,或缩小文件后重试。"},
{"legacy failure", "failed", "", "", "PDF 未提取到可读文本。该文件可能是扫描件、图片型 PDF、加密 PDF,或文档内容本身不可复制。"},
{"stale message without code", "ready", "", "private upstream diagnostic", ""},
{"padded failed status", " FAILED ", "", "", "PDF 未提取到可读文本。该文件可能是扫描件、图片型 PDF、加密 PDF,或文档内容本身不可复制。"},
{"unknown code", "failed", "unknown_error", "private upstream diagnostic", "文件处理失败,请稍后重试。"},
{"code without failed status", "ready", "extract_timeout", "", "文件提取超时,请稍后重试,或缩小文件后重试。"},
} {
t.Run(tc.name, func(t *testing.T) {
got := fileProcessingStatusFromFileObject(&model.FileObject{
FileCategory: "pdf", ProcessingStatus: tc.status,
ProcessingErrorCode: tc.code, ProcessingErrorMessage: tc.message,
})
if got.ErrorMessage != tc.want {
t.Fatalf("message = %q, want %q", got.ErrorMessage, tc.want)
}
if got.ErrorCode != tc.code {
t.Fatalf("error code changed: %q", got.ErrorCode)
}
})
}
}
4 changes: 2 additions & 2 deletions backend/internal/transport/http/conversation/dto_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ func toFileObjectResponse(item *model.FileObject, capability appembedding.FileVe
ProcessingStatus: item.ProcessingStatus,
ProcessingReady: item.ProcessingReady,
ProcessingErrorCode: item.ProcessingErrorCode,
ProcessingErrorMessage: appprocessing.HumanizeFileProcessingError(item.FileCategory, item.ProcessingErrorCode, item.ProcessingErrorMessage),
ProcessingErrorMessage: appprocessing.ResolveFileProcessingErrorMessage(item.FileCategory, item.ProcessingStatus, item.ProcessingErrorCode, item.ProcessingErrorMessage),
ExtractStatus: item.ExtractStatus,
EmbedStatus: item.EmbedStatus,
EmbedError: item.EmbedError,
Expand Down Expand Up @@ -1478,7 +1478,7 @@ func toFileProcessingStatusResponse(d *appprocessing.FileProcessingStatusDTO) Fi
RAGReady: d.RAGReady,
RAGReason: d.RAGReason,
ErrorCode: d.ErrorCode,
ErrorMessage: appprocessing.HumanizeFileProcessingError(d.FileCategory, d.ErrorCode, d.ErrorMessage),
ErrorMessage: d.ErrorMessage,
ExtractChars: d.ExtractChars,
ExtractPages: d.ExtractPages,
ChunkCount: d.ChunkCount,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package conversation

import (
"testing"

appembedding "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/application/embedding"
appprocessing "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/application/processing"
model "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/conversation"
)

func TestFileResponsesDoNotInventFailureMessages(t *testing.T) {
for _, tc := range []struct {
name, status, extract, embed, code, message, want string
}{
{"successful MinerU PDF", "ready", "ready", "none", "", "", ""},
{"queued PDF", "queued", "none", "none", "", "", ""},
{"extracting PDF", "extracting", "none", "none", "", "", ""},
{"empty PDF", "ready", "empty", "empty", "", "", ""},
{"stalled embedding marked failed", "ready", "ready", "failed", "embed_failed", "embedding stalled", "文件已完成文本提取,但向量化失败。"},
{"whitespace error fields", "ready", "ready", "none", " \t", " \n", ""},
{"stale message without code", "ready", "ready", "none", "", "old diagnostic", ""},
{"actual failure", "failed", "failed", "none", "extract_timeout", "", "文件提取超时,请稍后重试,或缩小文件后重试。"},
{"legacy failure without fields", "failed", "failed", "none", "", "", "PDF 未提取到可读文本。该文件可能是扫描件、图片型 PDF、加密 PDF,或文档内容本身不可复制。"},
} {
t.Run(tc.name, func(t *testing.T) {
file := &model.FileObject{
FileID: "synthetic-pdf", FileCategory: "pdf", MimeType: "application/pdf",
ProcessingStatus: tc.status, ProcessingReady: tc.extract == "ready",
ExtractStatus: tc.extract, ExtractEngine: "mineru", ExtractChars: 638, ExtractPages: 1,
EmbedStatus: tc.embed, ProcessingErrorCode: tc.code, ProcessingErrorMessage: tc.message,
}
object := toFileObjectResponse(file, appembedding.FileVectorizationCapability{})
if object.ProcessingErrorMessage != tc.want {
t.Errorf("file response message = %q, want %q", object.ProcessingErrorMessage, tc.want)
}
status := toFileProcessingStatusResponse(&appprocessing.FileProcessingStatusDTO{
FileID: file.FileID, FileCategory: file.FileCategory, ProcessingStatus: tc.status,
ProcessingReady: file.ProcessingReady, ExtractStatus: tc.extract, EmbedStatus: tc.embed,
ErrorCode: tc.code, ErrorMessage: object.ProcessingErrorMessage, ExtractChars: 638, ExtractPages: 1,
})
if status.ErrorMessage != tc.want {
t.Errorf("processing response message = %q, want %q", status.ErrorMessage, tc.want)
}
if object.ProcessingStatus != tc.status || status.ProcessingStatus != tc.status {
t.Fatal("mapping changed processing status")
}
})
}
}
Loading