mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-03-27 17:29:39 +08:00
Feat: Initialize context engine CLI (#13776)
### What problem does this PR solve? - Add multiple output format to ragflow_cli - Initialize contextengine to Go module - ls datasets/ls files - cat file - search -d dir -q query issue: #13714 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
312
internal/cli/contextengine/engine.go
Normal file
312
internal/cli/contextengine/engine.go
Normal file
@ -0,0 +1,312 @@
|
||||
//
|
||||
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
package contextengine
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Engine is the core of the Context Engine
|
||||
// It manages providers and routes commands to the appropriate provider
|
||||
type Engine struct {
|
||||
providers []Provider
|
||||
}
|
||||
|
||||
// NewEngine creates a new Context Engine
|
||||
func NewEngine() *Engine {
|
||||
return &Engine{
|
||||
providers: make([]Provider, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterProvider registers a provider with the engine
|
||||
func (e *Engine) RegisterProvider(provider Provider) {
|
||||
e.providers = append(e.providers, provider)
|
||||
}
|
||||
|
||||
// GetProviders returns all registered providers
|
||||
func (e *Engine) GetProviders() []ProviderInfo {
|
||||
infos := make([]ProviderInfo, 0, len(e.providers))
|
||||
for _, p := range e.providers {
|
||||
infos = append(infos, ProviderInfo{
|
||||
Name: p.Name(),
|
||||
Description: p.Description(),
|
||||
})
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
// Execute executes a command and returns the result
|
||||
func (e *Engine) Execute(ctx stdctx.Context, cmd *Command) (*Result, error) {
|
||||
switch cmd.Type {
|
||||
case CommandList:
|
||||
return e.List(ctx, cmd.Path, parseListOptions(cmd.Params))
|
||||
case CommandSearch:
|
||||
return e.Search(ctx, cmd.Path, parseSearchOptions(cmd.Params))
|
||||
case CommandCat:
|
||||
_, err := e.Cat(ctx, cmd.Path)
|
||||
return nil, err
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown command type: %s", cmd.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// resolveProvider finds the provider for a given path
|
||||
func (e *Engine) resolveProvider(path string) (Provider, string, error) {
|
||||
path = normalizePath(path)
|
||||
|
||||
for _, provider := range e.providers {
|
||||
if provider.Supports(path) {
|
||||
// Parse the subpath relative to the provider root
|
||||
// Get provider name to calculate subPath
|
||||
providerName := provider.Name()
|
||||
var subPath string
|
||||
if path == providerName {
|
||||
subPath = ""
|
||||
} else if strings.HasPrefix(path, providerName+"/") {
|
||||
subPath = path[len(providerName)+1:]
|
||||
} else {
|
||||
subPath = path
|
||||
}
|
||||
return provider, subPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If no provider supports this path, check if FileProvider can handle it as a fallback
|
||||
// This allows paths like "myskills" to be treated as "files/myskills"
|
||||
if fileProvider := e.getFileProvider(); fileProvider != nil {
|
||||
// Check if the path looks like a file manager path (single component, not matching other providers)
|
||||
parts := SplitPath(path)
|
||||
if len(parts) > 0 && parts[0] != "datasets" {
|
||||
return fileProvider, path, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, "", fmt.Errorf("%s: %s", ErrProviderNotFound, path)
|
||||
}
|
||||
|
||||
// List lists nodes at the given path
|
||||
// If path is empty, returns:
|
||||
// 1. Built-in providers (e.g., datasets)
|
||||
// 2. Top-level directories from files provider (if any)
|
||||
func (e *Engine) List(ctx stdctx.Context, path string, opts *ListOptions) (*Result, error) {
|
||||
// Normalize path
|
||||
path = normalizePath(path)
|
||||
|
||||
// If path is empty, return list of providers and files root directories
|
||||
if path == "" || path == "/" {
|
||||
return e.listRoot(ctx, opts)
|
||||
}
|
||||
|
||||
provider, subPath, err := e.resolveProvider(path)
|
||||
if err != nil {
|
||||
// If not found, try to find in files provider as a fallback
|
||||
// This allows "ls myfolder" to work as "ls files/myfolder"
|
||||
if fileProvider := e.getFileProvider(); fileProvider != nil {
|
||||
result, ferr := fileProvider.List(ctx, path, opts)
|
||||
if ferr == nil {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider.List(ctx, subPath, opts)
|
||||
}
|
||||
|
||||
// listRoot returns the root listing:
|
||||
// 1. Built-in providers (datasets, etc.)
|
||||
// 2. Top-level folders from files provider (file_manager)
|
||||
func (e *Engine) listRoot(ctx stdctx.Context, opts *ListOptions) (*Result, error) {
|
||||
nodes := make([]*Node, 0)
|
||||
|
||||
// Add built-in providers first (like datasets)
|
||||
for _, p := range e.providers {
|
||||
// Skip files provider from this list - we'll add its children instead
|
||||
if p.Name() == "files" {
|
||||
continue
|
||||
}
|
||||
nodes = append(nodes, &Node{
|
||||
Name: p.Name(),
|
||||
Path: "/" + p.Name(),
|
||||
Type: NodeTypeDirectory,
|
||||
CreatedAt: time.Now(),
|
||||
Metadata: map[string]interface{}{
|
||||
"description": p.Description(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Add top-level folders from files provider (file_manager)
|
||||
if fileProvider := e.getFileProvider(); fileProvider != nil {
|
||||
filesResult, err := fileProvider.List(ctx, "", opts)
|
||||
if err == nil {
|
||||
for _, node := range filesResult.Nodes {
|
||||
// Only add folders (directories), not files
|
||||
if node.Type == NodeTypeDirectory {
|
||||
// Ensure path doesn't have /files/ prefix for display
|
||||
node.Path = strings.TrimPrefix(node.Path, "files/")
|
||||
node.Path = strings.TrimPrefix(node.Path, "/")
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Result{
|
||||
Nodes: nodes,
|
||||
Total: len(nodes),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getFileProvider returns the files provider if registered
|
||||
func (e *Engine) getFileProvider() Provider {
|
||||
for _, p := range e.providers {
|
||||
if p.Name() == "files" {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search searches for nodes matching the query
|
||||
func (e *Engine) Search(ctx stdctx.Context, path string, opts *SearchOptions) (*Result, error) {
|
||||
provider, subPath, err := e.resolveProvider(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider.Search(ctx, subPath, opts)
|
||||
}
|
||||
|
||||
// Cat retrieves the content of a file/document
|
||||
func (e *Engine) Cat(ctx stdctx.Context, path string) ([]byte, error) {
|
||||
provider, subPath, err := e.resolveProvider(path)
|
||||
if err != nil {
|
||||
// If not found, try to find in files provider as a fallback
|
||||
// This allows "cat myfolder/file.txt" to work as "cat files/myfolder/file.txt"
|
||||
if fileProvider := e.getFileProvider(); fileProvider != nil {
|
||||
return fileProvider.Cat(ctx, path)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider.Cat(ctx, subPath)
|
||||
}
|
||||
|
||||
// ParsePath parses a path and returns path information
|
||||
func (e *Engine) ParsePath(path string) (*PathInfo, error) {
|
||||
path = normalizePath(path)
|
||||
components := SplitPath(path)
|
||||
|
||||
if len(components) == 0 {
|
||||
return nil, fmt.Errorf("empty path")
|
||||
}
|
||||
|
||||
providerName := components[0]
|
||||
isRoot := len(components) == 1
|
||||
|
||||
// Find the provider
|
||||
var provider Provider
|
||||
for _, p := range e.providers {
|
||||
if p.Name() == providerName || strings.HasPrefix(path, p.Name()) {
|
||||
provider = p
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if provider == nil {
|
||||
return nil, fmt.Errorf("%s: %s", ErrProviderNotFound, path)
|
||||
}
|
||||
|
||||
info := &PathInfo{
|
||||
Provider: providerName,
|
||||
Path: path,
|
||||
Components: components,
|
||||
IsRoot: isRoot,
|
||||
}
|
||||
|
||||
// Extract resource ID or name if available
|
||||
if len(components) >= 2 {
|
||||
info.ResourceName = components[1]
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// parseListOptions parses command params into ListOptions
|
||||
func parseListOptions(params map[string]interface{}) *ListOptions {
|
||||
opts := &ListOptions{}
|
||||
|
||||
if params == nil {
|
||||
return opts
|
||||
}
|
||||
|
||||
if recursive, ok := params["recursive"].(bool); ok {
|
||||
opts.Recursive = recursive
|
||||
}
|
||||
if limit, ok := params["limit"].(int); ok {
|
||||
opts.Limit = limit
|
||||
}
|
||||
if offset, ok := params["offset"].(int); ok {
|
||||
opts.Offset = offset
|
||||
}
|
||||
if sortBy, ok := params["sort_by"].(string); ok {
|
||||
opts.SortBy = sortBy
|
||||
}
|
||||
if sortOrder, ok := params["sort_order"].(string); ok {
|
||||
opts.SortOrder = sortOrder
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// parseSearchOptions parses command params into SearchOptions
|
||||
func parseSearchOptions(params map[string]interface{}) *SearchOptions {
|
||||
opts := &SearchOptions{}
|
||||
|
||||
if params == nil {
|
||||
return opts
|
||||
}
|
||||
|
||||
if query, ok := params["query"].(string); ok {
|
||||
opts.Query = query
|
||||
}
|
||||
if limit, ok := params["limit"].(int); ok {
|
||||
opts.Limit = limit
|
||||
}
|
||||
if offset, ok := params["offset"].(int); ok {
|
||||
opts.Offset = offset
|
||||
}
|
||||
if recursive, ok := params["recursive"].(bool); ok {
|
||||
opts.Recursive = recursive
|
||||
}
|
||||
if topK, ok := params["top_k"].(int); ok {
|
||||
opts.TopK = topK
|
||||
}
|
||||
if threshold, ok := params["threshold"].(float64); ok {
|
||||
opts.Threshold = threshold
|
||||
}
|
||||
if dirs, ok := params["dirs"].([]string); ok {
|
||||
opts.Dirs = dirs
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
Reference in New Issue
Block a user