Files
ragflow/internal/cli/contextengine/engine.go
Yingfeng 6e309f9d0a 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)
2026-03-26 21:07:06 +08:00

313 lines
8.2 KiB
Go

//
// 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
}