mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-03-27 17:29:39 +08:00
### 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)
313 lines
8.2 KiB
Go
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
|
|
}
|