mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-24 17:36:47 +08:00
fix: chat attachments not used in Go mode (#17259)
This commit is contained in:
@@ -846,6 +846,12 @@ func (s *ChatPipelineService) AsyncChat(
|
||||
// If empty_response is not configured, fall through to the LLM call
|
||||
// with an empty knowledge context.
|
||||
//
|
||||
// EXCEPTION: when the user attached files to their message, the
|
||||
// attachment text provides context that should be sent to the LLM
|
||||
// even if KB retrieval returned nothing. In that case we skip the
|
||||
// early return and fall through to the normal LLM call where
|
||||
// attachments are appended to the system prompt.
|
||||
//
|
||||
// Two results are yielded (mirroring Python dialog_service.py):
|
||||
// 1. Final=false — carries the answer text so streaming consumers
|
||||
// actually display the fallback message.
|
||||
@@ -854,7 +860,7 @@ func (s *ChatPipelineService) AsyncChat(
|
||||
// final event (dialog_service.py:807); consumers that only look at
|
||||
// the final event (e.g. the OpenAI-compatible endpoint) would
|
||||
// otherwise see an empty reply.
|
||||
if len(knowledges) == 0 {
|
||||
if len(knowledges) == 0 && attachments == "" {
|
||||
if emptyResp, ok := promptConfig["empty_response"].(string); ok && emptyResp != "" {
|
||||
out <- AsyncChatResult{
|
||||
Answer: emptyResp,
|
||||
|
||||
@@ -95,6 +95,19 @@ func (s *FileService) DownloadAgentFile(tenantID, location string) ([]byte, erro
|
||||
|
||||
// GetFileContents fetches file contents (text + image) from storage
|
||||
// for the given file dicts.
|
||||
//
|
||||
// File dicts are the descriptors returned by the upload_info endpoint
|
||||
// (UploadInfos / storeUploadInfoBlob). They contain:
|
||||
//
|
||||
// - "id": storage location UUID (key in the downloads bucket)
|
||||
// - "created_by": the user ID who owns the downloads bucket
|
||||
// - "name": the original filename
|
||||
// - "mime_type": the content type
|
||||
//
|
||||
// Blobs are stored directly in "{created_by}-downloads/{id}" in object
|
||||
// storage WITHOUT a corresponding File entity row in the database.
|
||||
// Mirrors Python's FileService.get_files → get_blob(user_id, file_id).
|
||||
//
|
||||
// - raw=false: images returned as base64 data URIs in images; non-images parsed and returned as text.
|
||||
// - raw=true: images returned as raw bytes in images; non-images parsed and returned as text.
|
||||
func (s *FileService) GetFileContents(ctx context.Context, uid string, fileDicts []map[string]interface{}, raw bool) (texts []string, images []string, err error) {
|
||||
@@ -108,28 +121,36 @@ func (s *FileService) GetFileContents(ctx context.Context, uid string, fileDicts
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
file, ferr := s.fileDAO.GetByID(id)
|
||||
if ferr != nil || file == nil || file.Location == nil || *file.Location == "" {
|
||||
continue
|
||||
name, _ := fd["name"].(string)
|
||||
mimeType, _ := fd["mime_type"].(string)
|
||||
createdBy, _ := fd["created_by"].(string)
|
||||
if createdBy == "" {
|
||||
createdBy = uid
|
||||
}
|
||||
if !s.checkFilePerm(s.fileDAO, file, uid) {
|
||||
// Permission: only the owner can access their uploads bucket.
|
||||
if createdBy != uid {
|
||||
return nil, nil, fmt.Errorf("No authorization.")
|
||||
}
|
||||
data, derr := storageImpl.Get(file.ParentID, *file.Location)
|
||||
|
||||
data, derr := storageImpl.Get(createdBy+"-downloads", id)
|
||||
if derr != nil || len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
ft := utility.FilenameType(file.Name)
|
||||
|
||||
ft := utility.FilenameType(name)
|
||||
if ft == utility.FileTypeVISUAL {
|
||||
if raw {
|
||||
images = append(images, string(data))
|
||||
} else {
|
||||
ext := utility.GetFileExtension(file.Name)
|
||||
mime := utility.GetContentType(ext, string(ft))
|
||||
images = append(images, "data:"+mime+";base64,"+base64.StdEncoding.EncodeToString(data))
|
||||
mediaType := strings.ToLower(strings.TrimSpace(strings.Split(mimeType, ";")[0]))
|
||||
if mediaType == "" {
|
||||
ext := utility.GetFileExtension(name)
|
||||
mediaType = utility.GetContentType(ext, string(ft))
|
||||
}
|
||||
images = append(images, "data:"+mediaType+";base64,"+base64.StdEncoding.EncodeToString(data))
|
||||
}
|
||||
} else {
|
||||
texts = append(texts, parseFileContent(ctx, file.Name, data))
|
||||
texts = append(texts, parseFileContent(ctx, name, data))
|
||||
}
|
||||
}
|
||||
return texts, images, nil
|
||||
|
||||
@@ -11,9 +11,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"ragflow/internal/dao"
|
||||
"ragflow/internal/entity"
|
||||
"ragflow/internal/storage"
|
||||
@@ -100,108 +97,23 @@ func (f *fakeStorage) Move(srcBucket, srcPath, destBucket, destPath string) bool
|
||||
|
||||
func (f *fakeStorage) Close() error { return nil }
|
||||
|
||||
func setupFileContentPermissionDB(t *testing.T, accessible bool) {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
TranslateError: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open sqlite: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(
|
||||
&entity.File{},
|
||||
&entity.File2Document{},
|
||||
&entity.Document{},
|
||||
&entity.Knowledgebase{},
|
||||
&entity.Tenant{},
|
||||
&entity.UserTenant{},
|
||||
); err != nil {
|
||||
t.Fatalf("failed to migrate file content tables: %v", err)
|
||||
}
|
||||
|
||||
location := "doc.txt"
|
||||
if err := db.Create(&entity.File{
|
||||
ID: "file-1",
|
||||
ParentID: "bucket-1",
|
||||
TenantID: "owner-user",
|
||||
Name: "doc.txt",
|
||||
Location: &location,
|
||||
Type: "file",
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("insert file: %v", err)
|
||||
}
|
||||
if err := db.Create(&entity.Document{
|
||||
ID: "doc-1",
|
||||
KbID: "kb-owner",
|
||||
ParserID: "naive",
|
||||
ParserConfig: entity.JSONMap{},
|
||||
Status: sptr(string(entity.StatusValid)),
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("insert document: %v", err)
|
||||
}
|
||||
fileID := "file-1"
|
||||
docID := "doc-1"
|
||||
if err := db.Create(&entity.File2Document{
|
||||
ID: "f2d-1",
|
||||
FileID: &fileID,
|
||||
DocumentID: &docID,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("insert file2document: %v", err)
|
||||
}
|
||||
if err := db.Create(&entity.Knowledgebase{
|
||||
ID: "kb-owner",
|
||||
TenantID: "tenant-owner",
|
||||
Name: "owner-kb",
|
||||
EmbdID: "embd-1",
|
||||
CreatedBy: "owner-user",
|
||||
Permission: string(entity.TenantPermissionTeam),
|
||||
Status: sptr(string(entity.StatusValid)),
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("insert knowledgebase: %v", err)
|
||||
}
|
||||
if err := db.Create(&entity.Tenant{
|
||||
ID: "tenant-owner",
|
||||
LLMID: "llm-1",
|
||||
EmbdID: "embd-1",
|
||||
ASRID: "asr-1",
|
||||
Status: sptr(string(entity.StatusValid)),
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("insert tenant: %v", err)
|
||||
}
|
||||
if accessible {
|
||||
if err := db.Create(&entity.UserTenant{
|
||||
ID: "ut-user-1",
|
||||
UserID: "user-1",
|
||||
TenantID: "tenant-owner",
|
||||
Role: "normal",
|
||||
Status: sptr(string(entity.StatusValid)),
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("insert user_tenant: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
orig := dao.DB
|
||||
dao.DB = db
|
||||
t.Cleanup(func() { dao.DB = orig })
|
||||
}
|
||||
|
||||
func TestFileService_GetFileContents_NotAccessible(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
setupFileContentPermissionDB(t, false)
|
||||
|
||||
orig := testFilePerm
|
||||
testFilePerm = func(_ *dao.FileDAO, _ *entity.File, _ string) bool { return false }
|
||||
t.Cleanup(func() { testFilePerm = orig })
|
||||
|
||||
mockStorage := &fakeStorage{blob: []byte("secret")}
|
||||
memory := storage.NewMemoryStorage()
|
||||
if err := memory.Put("other-user-downloads", "loc-1", []byte("secret")); err != nil {
|
||||
t.Fatalf("put: %v", err)
|
||||
}
|
||||
factory := storage.GetStorageFactory()
|
||||
originalStorage := factory.GetStorage()
|
||||
factory.SetStorage(mockStorage)
|
||||
factory.SetStorage(memory)
|
||||
t.Cleanup(func() { factory.SetStorage(originalStorage) })
|
||||
|
||||
svc := testFileService()
|
||||
texts, images, err := svc.GetFileContents(ctx, "user-1", []map[string]interface{}{{"id": "file-1"}}, false)
|
||||
texts, images, err := svc.GetFileContents(t.Context(), "user-1", []map[string]interface{}{{
|
||||
"id": "loc-1",
|
||||
"name": "secret.txt",
|
||||
"mime_type": "text/plain",
|
||||
"created_by": "other-user",
|
||||
}}, false)
|
||||
if err == nil {
|
||||
t.Fatal("expected authorization error")
|
||||
}
|
||||
@@ -211,23 +123,25 @@ func TestFileService_GetFileContents_NotAccessible(t *testing.T) {
|
||||
if len(texts) != 0 || len(images) != 0 {
|
||||
t.Fatalf("expected no content, got texts=%v images=%v", texts, images)
|
||||
}
|
||||
if mockStorage.getCalls != 0 {
|
||||
t.Fatalf("storage should not be read without permission, got %d calls", mockStorage.getCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileService_GetFileContents_Accessible(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
setupFileContentPermissionDB(t, true)
|
||||
|
||||
mockStorage := &fakeStorage{blob: []byte("allowed content")}
|
||||
memory := storage.NewMemoryStorage()
|
||||
if err := memory.Put("user-1-downloads", "loc-1", []byte("allowed content")); err != nil {
|
||||
t.Fatalf("put: %v", err)
|
||||
}
|
||||
factory := storage.GetStorageFactory()
|
||||
originalStorage := factory.GetStorage()
|
||||
factory.SetStorage(mockStorage)
|
||||
factory.SetStorage(memory)
|
||||
t.Cleanup(func() { factory.SetStorage(originalStorage) })
|
||||
|
||||
svc := testFileService()
|
||||
texts, images, err := svc.GetFileContents(ctx, "user-1", []map[string]interface{}{{"id": "file-1"}}, false)
|
||||
texts, images, err := svc.GetFileContents(t.Context(), "user-1", []map[string]interface{}{{
|
||||
"id": "loc-1",
|
||||
"name": "doc.txt",
|
||||
"mime_type": "text/plain",
|
||||
"created_by": "user-1",
|
||||
}}, false)
|
||||
if err != nil {
|
||||
t.Fatalf("GetFileContents failed: %v", err)
|
||||
}
|
||||
@@ -237,12 +151,6 @@ func TestFileService_GetFileContents_Accessible(t *testing.T) {
|
||||
if len(texts) != 1 || !strings.Contains(texts[0], "allowed content") {
|
||||
t.Fatalf("unexpected texts: %v", texts)
|
||||
}
|
||||
if mockStorage.getCalls != 1 {
|
||||
t.Fatalf("storage get calls = %d, want 1", mockStorage.getCalls)
|
||||
}
|
||||
if mockStorage.lastBucket != "bucket-1" || mockStorage.lastFnm != "doc.txt" {
|
||||
t.Fatalf("storage read %s/%s, want bucket-1/doc.txt", mockStorage.lastBucket, mockStorage.lastFnm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileService_ParseAgentUploads_TextAndImageInRequestOrder(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user