mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-21 07:01:04 +08:00
Feat: add built in DSL file API (#17003)
### Summary 1. list and get by id API for builtin DSL 2. add DSL default component param values validation 3. remove all hard code keys for parser config
This commit is contained in:
@@ -29,7 +29,6 @@ import (
|
||||
"path/filepath"
|
||||
"ragflow/internal/common"
|
||||
"ragflow/internal/entity"
|
||||
"ragflow/internal/httputil"
|
||||
"ragflow/internal/utility"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -45,7 +44,6 @@ var IMG_BASE64_PREFIX = "data:image/png;base64,"
|
||||
|
||||
// documentServiceIface defines the DocumentService methods used by DocumentHandler.
|
||||
type documentServiceIface interface {
|
||||
CreateDocument(req *service.CreateDocumentRequest) (*entity.Document, error)
|
||||
GetDocumentByID(id string) (*service.DocumentResponse, error)
|
||||
UpdateDocument(id string, req *service.UpdateDocumentRequest) error
|
||||
DeleteDocument(id string) error
|
||||
@@ -98,41 +96,6 @@ func NewDocumentHandler(documentService documentServiceIface, datasetService *se
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDocument create document
|
||||
// @Summary Create Document
|
||||
// @Description Create new document
|
||||
// @Tags documents
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body service.CreateDocumentRequest true "document info"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/v1/documents [post]
|
||||
func (h *DocumentHandler) CreateDocument(c *gin.Context) {
|
||||
_, errorCode, errorMessage := GetUser(c)
|
||||
if errorCode != common.CodeSuccess {
|
||||
common.ErrorWithCode(c, errorCode, errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
var req service.CreateDocumentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
document, err := h.documentService.CreateDocument(&req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithData(c, document, "created successfully")
|
||||
}
|
||||
|
||||
// GetDocumentByID get document by ID
|
||||
// @Summary Get Document Info
|
||||
// @Description Get document details by ID
|
||||
@@ -987,15 +950,15 @@ func (h *DocumentHandler) uploadWebDocument(c *gin.Context, kb *entity.Knowledge
|
||||
|
||||
// mapDocKeysWithRunStatus renames a freshly-created document's raw keys to the
|
||||
// public response shape (chunk_num→chunk_count, token_num→token_count,
|
||||
// kb_id→dataset_id, parser_id→chunk_method) and reports run as a label.
|
||||
// kb_id→dataset_id) and reports run as a label.
|
||||
// Mirrors Python map_doc_keys_with_run_status / map_doc_keys.
|
||||
func mapDocKeysWithRunStatus(raw map[string]interface{}) map[string]interface{} {
|
||||
out := map[string]interface{}{
|
||||
"chunk_count": raw["chunk_num"],
|
||||
"token_count": raw["token_num"],
|
||||
"dataset_id": raw["kb_id"],
|
||||
"chunk_method": raw["parser_id"],
|
||||
"run": "UNSTART",
|
||||
"chunk_count": raw["chunk_num"],
|
||||
"token_count": raw["token_num"],
|
||||
"dataset_id": raw["kb_id"],
|
||||
"parser_id": raw["parser_id"],
|
||||
"run": "UNSTART",
|
||||
}
|
||||
for _, k := range []string{"id", "name", "type", "size", "suffix", "source_type", "created_by", "parser_config", "location", "pipeline_id", "content_hash"} {
|
||||
if v, ok := raw[k]; ok {
|
||||
@@ -1058,7 +1021,6 @@ func mapDocumentListItem(doc *entity.DocumentListItem, metaFields map[string]int
|
||||
"suffix": doc.Suffix,
|
||||
"run": mapRunStatus(doc.Run),
|
||||
"status": stringValue(doc.Status),
|
||||
"chunk_method": doc.ParserID,
|
||||
"parser_id": doc.ParserID,
|
||||
"pipeline_id": stringValue(doc.PipelineID),
|
||||
"pipeline_name": stringValue(doc.PipelineName),
|
||||
@@ -1409,7 +1371,7 @@ func (h *DocumentHandler) ListIngestionTasks(c *gin.Context) {
|
||||
|
||||
parseResult, err = h.documentService.ListIngestionTasks(userID, req.DatasetID, 0, 0)
|
||||
if err != nil {
|
||||
common.ResponseWithCodeData(c, httputil.IngestionTaskErrorCode(err), nil, err.Error())
|
||||
common.ResponseWithCodeData(c, IngestionTaskErrorCode(err), nil, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1459,7 +1421,7 @@ func (h *DocumentHandler) StopIngestionTasks(c *gin.Context) {
|
||||
|
||||
parseResult, err := h.documentService.StopIngestionTasks(req.Tasks, userID)
|
||||
if err != nil {
|
||||
common.ResponseWithCodeData(c, httputil.IngestionTaskErrorCode(err), nil, err.Error())
|
||||
common.ResponseWithCodeData(c, IngestionTaskErrorCode(err), nil, err.Error())
|
||||
return
|
||||
}
|
||||
common.SuccessWithData(c, parseResult, "success")
|
||||
@@ -1485,7 +1447,7 @@ func (h *DocumentHandler) RemoveIngestionTasks(c *gin.Context) {
|
||||
|
||||
deletedTasks, err := h.documentService.RemoveIngestionTasks(req.Tasks, userID)
|
||||
if err != nil {
|
||||
common.ResponseWithCodeData(c, httputil.IngestionTaskErrorCode(err), nil, err.Error())
|
||||
common.ResponseWithCodeData(c, IngestionTaskErrorCode(err), nil, err.Error())
|
||||
return
|
||||
}
|
||||
common.SuccessWithData(c, deletedTasks, "success")
|
||||
|
||||
@@ -138,9 +138,6 @@ func (f *fakeDocumentService) DownloadDocument(datasetID, docID string) (*servic
|
||||
FileName: "doc.pdf",
|
||||
}, nil
|
||||
}
|
||||
func (f *fakeDocumentService) CreateDocument(req *service.CreateDocumentRequest) (*entity.Document, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeDocumentService) GetDocumentByID(id string) (*service.DocumentResponse, error) {
|
||||
if f.docErr != nil {
|
||||
return nil, f.docErr
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"ragflow/internal/common"
|
||||
"ragflow/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
@@ -66,3 +68,20 @@ func HandleNoRoute(c *gin.Context) {
|
||||
"error": "Not Found",
|
||||
})
|
||||
}
|
||||
|
||||
// IngestionTaskErrorCode maps ingestion-task service errors to common.ErrorCode
|
||||
// for HTTP responses.
|
||||
func IngestionTaskErrorCode(err error) common.ErrorCode {
|
||||
var transitionErr *service.InvalidTaskTransitionError
|
||||
if errors.As(err, &transitionErr) {
|
||||
return common.CodeConflict
|
||||
}
|
||||
var conflictErr *service.TaskStatusConflictError
|
||||
if errors.As(err, &conflictErr) {
|
||||
return common.CodeConflict
|
||||
}
|
||||
if errors.Is(err, common.ErrTaskNotFound) {
|
||||
return common.CodeNotFound
|
||||
}
|
||||
return common.CodeExceptionError
|
||||
}
|
||||
|
||||
43
internal/handler/error_test.go
Normal file
43
internal/handler/error_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"ragflow/internal/common"
|
||||
"ragflow/internal/service"
|
||||
)
|
||||
|
||||
func TestIngestionTaskErrorCode(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
err error
|
||||
want common.ErrorCode
|
||||
}{
|
||||
{
|
||||
name: "invalid transition",
|
||||
err: &service.InvalidTaskTransitionError{TaskID: "task-1", From: common.CREATED, To: common.COMPLETED},
|
||||
want: common.CodeConflict,
|
||||
},
|
||||
{
|
||||
name: "status conflict",
|
||||
err: &service.TaskStatusConflictError{TaskID: "task-1", ExpectedFrom: common.CREATED, AttemptedTo: common.RUNNING, ActualCurrent: common.STOPPING},
|
||||
want: common.CodeConflict,
|
||||
},
|
||||
{
|
||||
name: "task not found",
|
||||
err: common.ErrTaskNotFound,
|
||||
want: common.CodeNotFound,
|
||||
},
|
||||
{
|
||||
name: "fallback",
|
||||
err: common.ErrInvalidToken,
|
||||
want: common.CodeExceptionError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
if got := IngestionTaskErrorCode(tc.err); got != tc.want {
|
||||
t.Fatalf("%s: got %d, want %d", tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,11 +24,12 @@ import (
|
||||
pipelinepkg "ragflow/internal/ingestion/pipeline"
|
||||
)
|
||||
|
||||
// builtinPipelineLister is the registry surface PipelineHandler needs.
|
||||
// builtinPipelineRegistry is the registry surface PipelineHandler needs.
|
||||
// Defined as an interface so handler tests can inject a fake without
|
||||
// touching the embedded template FS.
|
||||
type builtinPipelineLister interface {
|
||||
List() []*pipelinepkg.BuiltinPipelineMeta
|
||||
type builtinPipelineRegistry interface {
|
||||
List() *pipelinepkg.BuiltinPipelineListResponse
|
||||
Get(ref string) (*pipelinepkg.BuiltinPipeline, bool)
|
||||
}
|
||||
|
||||
// PipelineHandler handles pipeline endpoints.
|
||||
@@ -36,7 +37,7 @@ type builtinPipelineLister interface {
|
||||
// Built-in templates are shipped with the binary; user-defined templates
|
||||
// (canvas DSL) may be added later.
|
||||
type PipelineHandler struct {
|
||||
registry builtinPipelineLister
|
||||
registry builtinPipelineRegistry
|
||||
}
|
||||
|
||||
// NewPipelineHandler builds a PipelineHandler backed by the embedded
|
||||
@@ -59,7 +60,10 @@ func NewPipelineHandler() *PipelineHandler {
|
||||
// This is public static data, so no auth is required.
|
||||
func (h *PipelineHandler) ListPipelines(c *gin.Context) {
|
||||
if h == nil || h.registry == nil {
|
||||
common.SuccessWithData(c, []*pipelinepkg.BuiltinPipelineMeta{}, "success")
|
||||
common.SuccessWithData(c, &pipelinepkg.BuiltinPipelineListResponse{
|
||||
BuiltinPipelines: []*pipelinepkg.BuiltinPipelineMeta{},
|
||||
Total: 0,
|
||||
}, "success")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -70,3 +74,22 @@ func (h *PipelineHandler) ListPipelines(c *gin.Context) {
|
||||
|
||||
common.SuccessWithData(c, h.registry.List(), "success")
|
||||
}
|
||||
|
||||
// GetPipeline GET /api/v1/pipelines/:id
|
||||
// Returns a single built-in pipeline template with its DSL.
|
||||
// This is public static data, so no auth is required.
|
||||
func (h *PipelineHandler) GetPipeline(c *gin.Context) {
|
||||
if h == nil || h.registry == nil {
|
||||
common.ErrorWithCode(c, common.CodeDataError, "pipeline not found")
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
tpl, ok := h.registry.Get(id)
|
||||
if !ok || tpl == nil {
|
||||
common.ErrorWithCode(c, common.CodeDataError, "pipeline not found")
|
||||
return
|
||||
}
|
||||
|
||||
common.SuccessWithData(c, map[string]any{"dsl": tpl.DSL}, "success")
|
||||
}
|
||||
|
||||
@@ -27,12 +27,29 @@ import (
|
||||
pipelinepkg "ragflow/internal/ingestion/pipeline"
|
||||
)
|
||||
|
||||
// fakePipelineLister is a test double for builtinPipelineLister.
|
||||
type fakePipelineLister struct {
|
||||
// fakePipelineRegistry is a test double for builtinPipelineRegistry.
|
||||
type fakePipelineRegistry struct {
|
||||
items []*pipelinepkg.BuiltinPipelineMeta
|
||||
}
|
||||
|
||||
func (f fakePipelineLister) List() []*pipelinepkg.BuiltinPipelineMeta { return f.items }
|
||||
func (f fakePipelineRegistry) List() *pipelinepkg.BuiltinPipelineListResponse {
|
||||
return &pipelinepkg.BuiltinPipelineListResponse{
|
||||
BuiltinPipelines: f.items,
|
||||
Total: int64(len(f.items)),
|
||||
}
|
||||
}
|
||||
|
||||
func (f fakePipelineRegistry) Get(ref string) (*pipelinepkg.BuiltinPipeline, bool) {
|
||||
for _, item := range f.items {
|
||||
if item.ParserID == ref {
|
||||
return &pipelinepkg.BuiltinPipeline{
|
||||
BuiltinPipelineMeta: *item,
|
||||
DSL: map[string]any{"components": map[string]any{}},
|
||||
}, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func pipelineCtx() (*gin.Context, *httptest.ResponseRecorder) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
@@ -49,16 +66,14 @@ func TestPipelineHandler_ListPipelines_TypeBuiltin(t *testing.T) {
|
||||
Title: "General",
|
||||
Description: "general desc",
|
||||
Filename: "ingestion_pipeline_general.json",
|
||||
DSL: map[string]any{"components": map[string]any{}},
|
||||
},
|
||||
{
|
||||
ParserID: "book",
|
||||
Title: "Book",
|
||||
Filename: "ingestion_pipeline_book.json",
|
||||
DSL: map[string]any{"components": map[string]any{}},
|
||||
},
|
||||
}
|
||||
h := &PipelineHandler{registry: fakePipelineLister{items: items}}
|
||||
h := &PipelineHandler{registry: fakePipelineRegistry{items: items}}
|
||||
|
||||
c, w := pipelineCtx()
|
||||
h.ListPipelines(c)
|
||||
@@ -68,10 +83,12 @@ func TestPipelineHandler_ListPipelines_TypeBuiltin(t *testing.T) {
|
||||
}
|
||||
var resp struct {
|
||||
Code int `json:"code"`
|
||||
Data []struct {
|
||||
ParserID string `json:"parser_id"`
|
||||
Title string `json:"title"`
|
||||
DSL map[string]any `json:"dsl"`
|
||||
Data struct {
|
||||
Canvas []struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
} `json:"canvas"`
|
||||
Total int64 `json:"total"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
@@ -80,11 +97,14 @@ func TestPipelineHandler_ListPipelines_TypeBuiltin(t *testing.T) {
|
||||
if resp.Code != int(common.CodeSuccess) {
|
||||
t.Fatalf("code = %d, want %d", resp.Code, int(common.CodeSuccess))
|
||||
}
|
||||
if len(resp.Data) != 2 {
|
||||
t.Fatalf("len(data) = %d, want 2", len(resp.Data))
|
||||
if resp.Data.Total != 2 {
|
||||
t.Fatalf("total = %d, want 2", resp.Data.Total)
|
||||
}
|
||||
if resp.Data[0].ParserID != "general" || resp.Data[0].Title != "General" {
|
||||
t.Errorf("data[0] = %+v", resp.Data[0])
|
||||
if len(resp.Data.Canvas) != 2 {
|
||||
t.Fatalf("len(canvas) = %d, want 2", len(resp.Data.Canvas))
|
||||
}
|
||||
if resp.Data.Canvas[0].ID != "general" || resp.Data.Canvas[0].Title != "General" {
|
||||
t.Errorf("canvas[0] = %+v", resp.Data.Canvas[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +112,7 @@ func TestPipelineHandler_ListPipelines_NoTypeParam(t *testing.T) {
|
||||
items := []*pipelinepkg.BuiltinPipelineMeta{
|
||||
{ParserID: "general", Title: "General"},
|
||||
}
|
||||
h := &PipelineHandler{registry: fakePipelineLister{items: items}}
|
||||
h := &PipelineHandler{registry: fakePipelineRegistry{items: items}}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
@@ -105,15 +125,21 @@ func TestPipelineHandler_ListPipelines_NoTypeParam(t *testing.T) {
|
||||
t.Fatalf("status = %d, want 200", w.Code)
|
||||
}
|
||||
var resp struct {
|
||||
Data []struct {
|
||||
ParserID string `json:"parser_id"`
|
||||
Data struct {
|
||||
Canvas []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"canvas"`
|
||||
Total int64 `json:"total"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if len(resp.Data) != 1 {
|
||||
t.Fatalf("len(data) = %d, want 1 (no type param defaults to builtin)", len(resp.Data))
|
||||
if resp.Data.Total != 1 {
|
||||
t.Fatalf("total = %d, want 1", resp.Data.Total)
|
||||
}
|
||||
if len(resp.Data.Canvas) != 1 {
|
||||
t.Fatalf("len(canvas) = %d, want 1 (no type param defaults to builtin)", len(resp.Data.Canvas))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,19 +161,21 @@ func TestPipelineHandler_ListPipelines_RealRegistry(t *testing.T) {
|
||||
h.ListPipelines(c)
|
||||
|
||||
var resp struct {
|
||||
Data []struct {
|
||||
ParserID string `json:"parser_id"`
|
||||
Data struct {
|
||||
Canvas []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"canvas"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if len(resp.Data) == 0 {
|
||||
if len(resp.Data.Canvas) == 0 {
|
||||
t.Fatal("expected non-empty builtin pipeline list from real registry")
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, item := range resp.Data {
|
||||
seen[item.ParserID] = true
|
||||
for _, item := range resp.Data.Canvas {
|
||||
seen[item.ID] = true
|
||||
}
|
||||
if !seen["general"] {
|
||||
t.Error("real registry listing missing general")
|
||||
@@ -156,3 +184,65 @@ func TestPipelineHandler_ListPipelines_RealRegistry(t *testing.T) {
|
||||
t.Error("alias naive must be hidden from listing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipelineHandler_GetPipeline(t *testing.T) {
|
||||
items := []*pipelinepkg.BuiltinPipelineMeta{
|
||||
{ParserID: "general", Title: "General"},
|
||||
}
|
||||
h := &PipelineHandler{registry: fakePipelineRegistry{items: items}}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/pipelines/general", nil)
|
||||
c.Params = gin.Params{{Key: "id", Value: "general"}}
|
||||
|
||||
h.GetPipeline(c)
|
||||
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("status = %d, want 200", w.Code)
|
||||
}
|
||||
var resp struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
DSL map[string]any `json:"dsl"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if resp.Code != int(common.CodeSuccess) {
|
||||
t.Fatalf("code = %d, want %d", resp.Code, int(common.CodeSuccess))
|
||||
}
|
||||
if resp.Data.DSL == nil {
|
||||
t.Fatal("dsl should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipelineHandler_GetPipeline_NotFound(t *testing.T) {
|
||||
items := []*pipelinepkg.BuiltinPipelineMeta{
|
||||
{ParserID: "general", Title: "General"},
|
||||
}
|
||||
h := &PipelineHandler{registry: fakePipelineRegistry{items: items}}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/pipelines/nonexistent", nil)
|
||||
c.Params = gin.Params{{Key: "id", Value: "nonexistent"}}
|
||||
|
||||
h.GetPipeline(c)
|
||||
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("status = %d, want 200 (error is in body code)", w.Code)
|
||||
}
|
||||
var resp struct {
|
||||
Code int `json:"code"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if resp.Code == int(common.CodeSuccess) {
|
||||
t.Fatal("expected error code for nonexistent pipeline")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user