mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-02 08:45:42 +08:00
### What problem does this PR solve? - Tools management - Pregel engine wrapper for better usage - UT race - Coding style ### Type of change - [x] Refactoring
300 lines
9.0 KiB
Go
300 lines
9.0 KiB
Go
// Package coding provides a ready-to-use coding agent built on top of agentcore's
|
|
// ReAct agent, middleware stack, and profile system.
|
|
//
|
|
// It is the agentcore equivalent of deepagents-code — a production-grade coding
|
|
// assistant with file operations, shell security, Git safety, and optional sub-agents.
|
|
//
|
|
// Quick start:
|
|
//
|
|
// agent := coding.New(&coding.Config{
|
|
// Model: myModel,
|
|
// })
|
|
// runner := core.NewTypedRunner(core.RunnerConfig[*schema.Message]{Agent: agent})
|
|
// iter := runner.Run(ctx, []*schema.Message{schema.UserMessage("fix this bug")})
|
|
//
|
|
// With the profile system:
|
|
//
|
|
// coding.RegisterHarnessProfile()
|
|
// agent, _ := profile.NewAgent(ctx, &profile.AgentConfig{
|
|
// ModelSpec: "anthropic:claude-sonnet-4-6",
|
|
// HarnessProfileName: "coding-agent",
|
|
// })
|
|
package coding
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"ragflow/internal/harness/core"
|
|
"ragflow/internal/harness/core/middlewares/filesystem"
|
|
"ragflow/internal/harness/core/middlewares/subagent"
|
|
"ragflow/internal/harness/core/profile"
|
|
"ragflow/internal/harness/core/schema"
|
|
)
|
|
|
|
// Config configures the coding agent.
|
|
type Config struct {
|
|
// Name of the agent. Default: "coding_agent".
|
|
Name string
|
|
|
|
// Model is the chat model. Required (unless using profile system).
|
|
Model core.Model[*schema.Message]
|
|
|
|
// Tools are additional tools to register beyond the built-in coding tools.
|
|
Tools []core.Tool
|
|
|
|
// Instruction overrides the default coding system prompt.
|
|
Instruction string
|
|
|
|
// MaxIterations limits the ReAct loop. Default: 30.
|
|
MaxIterations int
|
|
|
|
// EnableShell when true adds shell execution capability via a local backend.
|
|
// Default: false (read-only file operations).
|
|
EnableShell bool
|
|
|
|
// ShellBackend is the filesystem backend to use for shell execution.
|
|
// When nil and EnableShell is true, a default local shell backend is created.
|
|
ShellBackend filesystem.Backend
|
|
|
|
// ShellAllowList configures which shell commands are allowed.
|
|
// When nil, the default allow-list is used (if EnableShell is true).
|
|
// When EnableShell is false, this field is ignored.
|
|
ShellAllowList *ShellAllowListConfig
|
|
|
|
// FilesystemBackend is the filesystem backend for file operations.
|
|
// When nil, an InMemoryBackend is used (read-only from agent perspective).
|
|
// Use a LocalFilesystemBackend for real file system access.
|
|
FilesystemBackend filesystem.Backend
|
|
|
|
// SubAgentSpecs declares sub-agents for task delegation.
|
|
SubAgentSpecs []subagent.SubAgentSpec
|
|
|
|
// SubAgentConfig configures the SubAgentMiddleware (recursion depth, events).
|
|
SubAgentConfig *subagent.Config
|
|
|
|
// RegisterHarness when true also registers the "coding-agent" harness profile.
|
|
// Default: false.
|
|
RegisterHarness bool
|
|
}
|
|
|
|
// DefaultConfig returns a Config with sensible defaults.
|
|
func DefaultConfig() *Config {
|
|
return &Config{
|
|
Name: "coding_agent",
|
|
MaxIterations: 30,
|
|
EnableShell: false,
|
|
}
|
|
}
|
|
|
|
// New creates a fully-configured coding ReActAgent.
|
|
//
|
|
// The agent includes:
|
|
// - Coding-optimized system prompt (with Git safety rules)
|
|
// - Filesystem middleware (read/write/edit/ls/glob/grep)
|
|
// - Shell allow-list middleware (when EnableShell is true)
|
|
// - SubAgentMiddleware (when SubAgentSpecs is non-empty)
|
|
// - Optional "coding-agent" harness profile registration
|
|
func New(cfg *Config) *core.ReActAgent[*schema.Message] {
|
|
if cfg == nil {
|
|
cfg = DefaultConfig()
|
|
}
|
|
if cfg.MaxIterations <= 0 {
|
|
cfg.MaxIterations = 30
|
|
}
|
|
if cfg.Name == "" {
|
|
cfg.Name = "coding_agent"
|
|
}
|
|
instruction := cfg.Instruction
|
|
if instruction == "" {
|
|
instruction = systemPrompt
|
|
}
|
|
|
|
// Build middleware stack.
|
|
var middlewares []core.ReActMiddleware
|
|
|
|
// 1. Shell allow-list middleware (applied BEFORE filesystem to intercept execute calls).
|
|
if cfg.EnableShell {
|
|
shellCfg := cfg.ShellAllowList
|
|
if shellCfg == nil {
|
|
shellCfg = &ShellAllowListConfig{
|
|
AllowedCommands: DefaultShellAllowList(),
|
|
BlockedCommands: DefaultBlockedCommands(),
|
|
}
|
|
}
|
|
middlewares = append(middlewares, NewShellAllowList(shellCfg))
|
|
}
|
|
|
|
// 2. Filesystem middleware (provides read/write/edit/ls/glob/grep/execute).
|
|
fsCfg := &filesystem.Config{
|
|
Backend: cfg.FilesystemBackend,
|
|
}
|
|
if cfg.EnableShell && cfg.ShellBackend != nil {
|
|
fsCfg.Backend = cfg.ShellBackend
|
|
} else if cfg.EnableShell && cfg.FilesystemBackend == nil {
|
|
// Create default local shell backend.
|
|
fsCfg.Backend = &localShellBackend{}
|
|
}
|
|
middlewares = append(middlewares, filesystem.New(fsCfg))
|
|
|
|
// 3. SubAgentMiddleware (when sub-agents are declared).
|
|
if len(cfg.SubAgentSpecs) > 0 {
|
|
saCfg := cfg.SubAgentConfig
|
|
if saCfg == nil {
|
|
saCfg = &subagent.Config{MaxDepth: 5}
|
|
}
|
|
saMW := subagent.New(cfg.SubAgentSpecs, saCfg)
|
|
middlewares = append(middlewares, saMW)
|
|
|
|
// Build react config — Init() prepares the sub-agent for middleware
|
|
// inheritance, while ToolContributor handles automatic tool injection.
|
|
reactCfg := &core.ReActConfig[*schema.Message]{
|
|
Model: cfg.Model,
|
|
Instruction: instruction,
|
|
MaxIterations: cfg.MaxIterations,
|
|
Middlewares: middlewares,
|
|
Tools: cfg.Tools,
|
|
}
|
|
saMW.Init(context.Background(), reactCfg)
|
|
return core.NewReActAgent(reactCfg)
|
|
}
|
|
|
|
// Build react config without sub-agents.
|
|
reactCfg := &core.ReActConfig[*schema.Message]{
|
|
Model: cfg.Model,
|
|
Instruction: instruction,
|
|
MaxIterations: cfg.MaxIterations,
|
|
Middlewares: middlewares,
|
|
Tools: cfg.Tools,
|
|
}
|
|
if cfg.EnableShell || cfg.FilesystemBackend != nil {
|
|
// Must have at least one tool for ReAct loop.
|
|
if len(reactCfg.Tools) == 0 {
|
|
// Filesystem middleware adds tools via BeforeAgent, but we need
|
|
// at least one tool to trigger the ReAct loop.
|
|
reactCfg.Tools = append(reactCfg.Tools, &execTool{})
|
|
}
|
|
}
|
|
return core.NewReActAgent(reactCfg)
|
|
}
|
|
|
|
// ---- Local shell backend ----
|
|
|
|
// localShellBackend implements filesystem.Backend with local shell execution.
|
|
type localShellBackend struct{}
|
|
|
|
func (b *localShellBackend) Read(path string) (string, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
func (b *localShellBackend) Write(path, content string) error {
|
|
return os.WriteFile(path, []byte(content), 0644)
|
|
}
|
|
|
|
func (b *localShellBackend) Edit(path, old, new string) error {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
content := string(data)
|
|
if !strings.Contains(content, old) {
|
|
return fmt.Errorf("edit_file: string %q not found in %s", old, path)
|
|
}
|
|
content = strings.Replace(content, old, new, 1)
|
|
return os.WriteFile(path, []byte(content), 0644)
|
|
}
|
|
|
|
func (b *localShellBackend) Ls(path string) ([]string, error) {
|
|
entries, err := os.ReadDir(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
names := make([]string, 0, len(entries))
|
|
for _, e := range entries {
|
|
names = append(names, e.Name())
|
|
}
|
|
return names, nil
|
|
}
|
|
|
|
func (b *localShellBackend) Glob(pattern string) ([]string, error) {
|
|
// Simple glob via the shell.
|
|
cmd := exec.Command("sh", "-c", fmt.Sprintf("ls -d %s 2>/dev/null", pattern))
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
lines := strings.TrimSpace(string(out))
|
|
if lines == "" {
|
|
return nil, nil
|
|
}
|
|
return strings.Split(lines, "\n"), nil
|
|
}
|
|
|
|
func (b *localShellBackend) Grep(pattern, path string) (string, error) {
|
|
cmd := exec.Command("grep", "-rn", pattern, path)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
// grep returns exit code 1 when no matches.
|
|
return "", nil
|
|
}
|
|
return string(out), nil
|
|
}
|
|
|
|
func (b *localShellBackend) Execute(command string) (string, error) {
|
|
cmd := exec.Command("sh", "-c", command)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return string(out), fmt.Errorf("execute: %w\n%s", err, string(out))
|
|
}
|
|
return string(out), nil
|
|
}
|
|
|
|
// ---- Dummy tool to bootstrap ReAct loop ----
|
|
|
|
type execTool struct{}
|
|
|
|
func (t *execTool) Name() string { return "_bootstrap_tool" }
|
|
func (t *execTool) Description() string { return "Internal bootstrap tool" }
|
|
func (t *execTool) Invoke(ctx context.Context, args string, opts ...core.ToolOption) (string, error) {
|
|
return "", nil
|
|
}
|
|
func (t *execTool) Stream(ctx context.Context, args string, opts ...core.ToolOption) (*schema.StreamReader[string], error) {
|
|
return schema.StreamReaderFromArray([]string{""}), nil
|
|
}
|
|
|
|
// ---- Harness profile registration ----
|
|
|
|
// HarnessProfile returns a pre-configured HarnessProfile for coding agents.
|
|
// It registers the standard coding agent middleware stack.
|
|
func HarnessProfile() *profile.HarnessProfile {
|
|
return &profile.HarnessProfile{
|
|
Name: "coding-agent",
|
|
BaseSystemPrompt: strPtr(systemPrompt),
|
|
MaxIterations: 30,
|
|
RecursionDepth: 5,
|
|
}
|
|
}
|
|
|
|
// RegisterHarnessProfile registers the "coding-agent" harness profile globally.
|
|
// After calling this, users can create coding agents via profile.NewAgent:
|
|
//
|
|
// agent, _ := profile.NewAgent(ctx, &profile.AgentConfig{
|
|
// ModelSpec: "anthropic:claude-sonnet-4-6",
|
|
// HarnessProfileName: "coding-agent",
|
|
// })
|
|
func RegisterHarnessProfile() {
|
|
if profile.LookupHarness("coding-agent") != nil {
|
|
return // already registered
|
|
}
|
|
profile.RegisterHarness(HarnessProfile())
|
|
}
|
|
|
|
func strPtr(s string) *string { return &s }
|