Files
ragflow/internal/harness/core/middlewares/skill/skill.go
Yingfeng 706e0d2d06 Refactor harness framework (#16271)
### What problem does this PR solve?

- Tools management
- Pregel engine wrapper for better usage
- UT race
- Coding style

### Type of change

- [x] Refactoring
2026-06-23 20:18:04 +08:00

195 lines
5.4 KiB
Go

// Package skill provides skill loading and execution middleware.
// Skills are defined in SKILL.md files with YAML frontmatter.
package skill
import (
"context"
"fmt"
"strings"
"ragflow/internal/harness/core"
"ragflow/internal/harness/core/schema"
)
// ExecMode defines how a skill is executed.
type ExecMode int
const (
ModeInline ExecMode = iota // Skill content injected into instruction
ModeFork // Skill loaded via a tool
ModeForkWithContext // Skill loaded via a tool with parent context
)
// FileSystemBackend reads skill definitions from a file system.
type FileSystemBackend interface {
Read(path string) (string, error)
List() ([]string, error)
}
// Config defines a single skill.
type Config struct {
Name string
Description string
Content string
ExecutionMode ExecMode
Model string // Model name for fork modes
Agent string // Agent name for fork modes
}
// TypedConfig configures the skill middleware.
type TypedConfig[M core.MessageType] struct {
Skills []Config
Backend FileSystemBackend
CustomSystemPrompt func(name, desc string) string
CustomToolParams func(name string) string
BuildContent func(ctx context.Context, cfg Config) (string, error)
BuildForkMessages func(ctx context.Context, cfg Config, request string) (string, error)
FormatForkResult func(ctx context.Context, result string) (string, error)
}
type middleware[M core.MessageType] struct {
core.BaseMiddleware[M]
cfg *TypedConfig[M]
}
func NewTyped[M core.MessageType](cfg *TypedConfig[M]) core.TypedReActMiddleware[M] {
return &middleware[M]{cfg: cfg}
}
func New(cfg *TypedConfig[*schema.Message]) core.TypedReActMiddleware[*schema.Message] {
return NewTyped[*schema.Message](cfg)
}
func (m *middleware[M]) ContributeTools(ctx context.Context) []core.Tool {
if m.cfg == nil {
return nil
}
var tools []core.Tool
for _, s := range m.loadSkills() {
switch s.ExecutionMode {
case ModeFork, ModeForkWithContext:
tools = append(tools, m.newSkillTool(s))
}
}
return tools
}
func (m *middleware[M]) ContributeToolInfos(ctx context.Context) []*schema.ToolInfo { return nil }
func (m *middleware[M]) ContributeReturnDirectly(ctx context.Context) map[string]bool { return nil }
func (m *middleware[M]) BeforeAgent(ctx context.Context, rc *core.ReActAgentContext) (context.Context, *core.ReActAgentContext, error) {
if m.cfg == nil {
return ctx, rc, nil
}
for _, s := range m.loadSkills() {
if s.ExecutionMode == ModeInline {
rc.Instruction = applyCustomInstruction(rc.Instruction, s, m.cfg.CustomSystemPrompt)
}
}
return ctx, rc, nil
}
// loadSkills returns all skills from config and backend.
func (m *middleware[M]) loadSkills() []Config {
if m.cfg == nil {
return nil
}
skills := m.cfg.Skills
if len(skills) == 0 && m.cfg.Backend != nil {
names, err := m.cfg.Backend.List()
if err == nil {
for _, name := range names {
content, err := m.cfg.Backend.Read(name)
if err != nil {
continue
}
parsed := parseSkill(content)
if parsed != nil {
skills = append(skills, *parsed)
}
}
}
}
return skills
}
func (m *middleware[M]) newSkillTool(s Config) core.Tool {
return core.NewBaseTool("skill_"+s.Name,
fmt.Sprintf("Execute the '%s' skill. %s", s.Name, s.Description),
func(ctx context.Context, args string) (string, error) {
if m.cfg.BuildContent != nil {
content, err := m.cfg.BuildContent(ctx, s)
if err != nil {
return "", err
}
if m.cfg.FormatForkResult != nil {
return m.cfg.FormatForkResult(ctx, content)
}
return content, nil
}
content := s.Content
if content == "" && m.cfg.Backend != nil {
loaded, err := m.cfg.Backend.Read(s.Name)
if err == nil {
content = loaded
}
}
if m.cfg.BuildForkMessages != nil {
result, err := m.cfg.BuildForkMessages(ctx, s, args)
if err != nil {
return "", err
}
return result, nil
}
if m.cfg.FormatForkResult != nil {
return m.cfg.FormatForkResult(ctx, content)
}
return fmt.Sprintf("### Skill: %s\n\n%s\n\nArgs: %s", s.Name, truncate(content, 2000), args), nil
})
}
// ---- Helpers ----
func parseSkill(content string) *Config {
cfg := &Config{ExecutionMode: ModeInline}
content = strings.TrimSpace(content)
// Parse YAML-like frontmatter
if strings.HasPrefix(content, "---") {
parts := strings.SplitN(content[3:], "---", 2)
if len(parts) == 2 {
front := strings.TrimSpace(parts[0])
body := strings.TrimSpace(parts[1])
for _, line := range strings.Split(front, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "name:") {
cfg.Name = strings.TrimSpace(line[5:])
} else if strings.HasPrefix(line, "description:") {
cfg.Description = strings.TrimSpace(line[12:])
} else if strings.HasPrefix(line, "model:") {
cfg.Model = strings.TrimSpace(line[6:])
}
}
cfg.Content = body
return cfg
}
}
// No frontmatter: use full content
cfg.Content = content
return cfg
}
func applyCustomInstruction(instruction string, s Config, customFn func(name, desc string) string) string {
if customFn != nil {
return instruction + "\n\n" + customFn(s.Name, s.Description)
}
return instruction + "\n\n## Skill: " + s.Name + "\n" + truncate(s.Content, 4000)
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "\n...(truncated)"
}