mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-12 06:35:44 +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
439 lines
14 KiB
Go
439 lines
14 KiB
Go
// Package profile provides a dual-track configuration system for agentcore:
|
|
//
|
|
// - ProviderProfile: controls how an LLM model is constructed per provider
|
|
// (api_key, temperature, max_tokens, api_base, use_responses_api, etc.)
|
|
// - HarnessProfile: controls the agent's runtime behaviour per use-case
|
|
// (system prompt, tool descriptions, middleware exclusions, recursion depth)
|
|
//
|
|
// Usage:
|
|
//
|
|
// // Register once at init time.
|
|
// profile.RegisterProvider("anthropic", &profile.ProviderProfile{
|
|
// InitModel: func(ctx, modelName string, opts map[string]any) (Model, error) {
|
|
// return anthropic.NewModel(modelName, opts["api_key"].(string)), nil
|
|
// },
|
|
// DefaultModel: "claude-sonnet-4-6",
|
|
// })
|
|
// profile.RegisterHarness("coding-agent", &profile.HarnessProfile{
|
|
// BaseSystemPrompt: strPtr("You are an expert software engineer."),
|
|
// MaxIterations: 20,
|
|
// RecursionDepth: 5,
|
|
// })
|
|
//
|
|
// // Create an agent in one call.
|
|
// agent, err := profile.NewAgent(ctx, &profile.AgentConfig{
|
|
// ModelSpec: "anthropic:claude-sonnet-4-6",
|
|
// HarnessProfileName: "coding-agent",
|
|
// Tools: []core.Tool{myTool},
|
|
// })
|
|
//
|
|
// Config precedence (highest wins): user > HarnessProfile > ProviderProfile > defaults.
|
|
package profile
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"ragflow/internal/harness/core"
|
|
"ragflow/internal/harness/core/internal"
|
|
"ragflow/internal/harness/core/middlewares/subagent"
|
|
"ragflow/internal/harness/core/schema"
|
|
)
|
|
|
|
// ========================================================================
|
|
// Phase 1: Core types + global registry
|
|
// ========================================================================
|
|
|
|
// ProviderProfile controls how a model is constructed for a given provider.
|
|
// Each provider (Anthropic, OpenAI, Google, …) registers one ProviderProfile
|
|
// that knows how to build a concrete Model from a model name + options.
|
|
type ProviderProfile struct {
|
|
// Name identifies the provider, e.g. "anthropic", "openai".
|
|
Name string
|
|
|
|
// InitModel creates a Model instance for the given model name and options.
|
|
// opts typically carries "api_key", "temperature", "max_tokens", "api_base", etc.
|
|
InitModel func(ctx context.Context, modelName string, opts map[string]any) (core.Model[*schema.Message], error)
|
|
|
|
// DefaultModel is returned when no model name is specified.
|
|
DefaultModel string
|
|
|
|
// DefaultOpts are the default options passed to InitModel.
|
|
// Can be overridden by HarnessProfile or user config.
|
|
DefaultOpts map[string]any
|
|
}
|
|
|
|
// HarnessProfile controls the agent's runtime behaviour for a specific use-case.
|
|
// The same model can be used with different harness profiles (coding, chat, research, …).
|
|
type HarnessProfile struct {
|
|
// Name identifies the profile, e.g. "coding-agent", "research-agent".
|
|
Name string
|
|
|
|
// BaseSystemPrompt replaces the default system prompt entirely.
|
|
// When nil, the system default is used.
|
|
BaseSystemPrompt *string
|
|
|
|
// SystemPromptSuffix is appended to the system prompt after BaseSystemPrompt.
|
|
SystemPromptSuffix string
|
|
|
|
// ToolDescriptionOverrides replaces the Description() of matching tools.
|
|
// Key = tool name, value = new description.
|
|
ToolDescriptionOverrides map[string]string
|
|
|
|
// ExcludedToolNames removes matching tools from the agent's tool list.
|
|
ExcludedToolNames []string
|
|
|
|
// ExcludedMiddlewareNames removes matching middlewares from the agent's
|
|
// middleware chain. Matching uses fmt.Sprintf("%T", mw) — include the
|
|
// fully qualified type name, e.g. "*subagent.SubAgentMiddleware".
|
|
ExcludedMiddlewareNames []string
|
|
|
|
// ExtraMiddlewares are appended to the agent's middleware chain at the end.
|
|
ExtraMiddlewares []core.ReActMiddleware
|
|
|
|
// MaxIterations overrides the ReAct loop iteration limit. 0 = use default.
|
|
MaxIterations int
|
|
|
|
// RecursionDepth sets the sub-agent recursion depth limit.
|
|
// 0 = unlimited (system default).
|
|
RecursionDepth int
|
|
}
|
|
|
|
// Global registries.
|
|
var (
|
|
providers sync.Map // map[string]*ProviderProfile
|
|
harnesses sync.Map // map[string]*HarnessProfile
|
|
)
|
|
|
|
// RegisterProvider registers a provider profile. Panics on duplicate name.
|
|
func RegisterProvider(p *ProviderProfile) {
|
|
if p == nil {
|
|
panic("profile: RegisterProvider called with nil")
|
|
}
|
|
if p.Name == "" {
|
|
panic("profile: ProviderProfile.Name is required")
|
|
}
|
|
if _, loaded := providers.LoadOrStore(p.Name, p); loaded {
|
|
panic(fmt.Sprintf("profile: provider %q already registered", p.Name))
|
|
}
|
|
}
|
|
|
|
// RegisterHarness registers a harness profile. Panics on duplicate name.
|
|
func RegisterHarness(h *HarnessProfile) {
|
|
if h == nil {
|
|
panic("profile: RegisterHarness called with nil")
|
|
}
|
|
if h.Name == "" {
|
|
panic("profile: HarnessProfile.Name is required")
|
|
}
|
|
if _, loaded := harnesses.LoadOrStore(h.Name, h); loaded {
|
|
panic(fmt.Sprintf("profile: harness profile %q already registered", h.Name))
|
|
}
|
|
}
|
|
|
|
// LookupProvider returns the registered provider, or nil if not found.
|
|
func LookupProvider(name string) *ProviderProfile {
|
|
if v, ok := providers.Load(name); ok {
|
|
return v.(*ProviderProfile)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LookupHarness returns the registered harness profile, or nil if not found.
|
|
func LookupHarness(name string) *HarnessProfile {
|
|
if v, ok := harnesses.Load(name); ok {
|
|
return v.(*HarnessProfile)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ParseModelSpec parses "provider:model" strings.
|
|
// Returns ("anthropic", "claude-sonnet-4-6") from "anthropic:claude-sonnet-4-6".
|
|
// If no colon, returns ("", raw, error).
|
|
func ParseModelSpec(spec string) (provider, model string, err error) {
|
|
parts := strings.SplitN(spec, ":", 2)
|
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
|
return "", "", fmt.Errorf("profile: invalid model spec %q (expected provider:model)", spec)
|
|
}
|
|
return parts[0], parts[1], nil
|
|
}
|
|
|
|
// ========================================================================
|
|
// AgentConfig combines model spec + harness profile + user overrides.
|
|
// ========================================================================
|
|
|
|
// AgentConfig is a high-level declarative config for creating a ReActAgent.
|
|
// It combines model selection (via ModelSpec), runtime behaviour (via
|
|
// HarnessProfileName), and direct user overrides.
|
|
type AgentConfig struct {
|
|
// ModelSpec in "provider:model" format, e.g. "anthropic:claude-sonnet-4-6".
|
|
ModelSpec string
|
|
|
|
// HarnessProfileName selects a registered HarnessProfile.
|
|
// Empty string means no harness profile.
|
|
HarnessProfileName string
|
|
|
|
// ProviderOpts overrides or augments the ProviderProfile's DefaultOpts.
|
|
ProviderOpts map[string]any
|
|
|
|
// Instruction overrides the system prompt. When non-nil, it takes
|
|
// precedence over both HarnessProfile.BaseSystemPrompt and the system default.
|
|
Instruction *string
|
|
|
|
// Tools available to the agent. When non-nil, replaces all previous tool lists.
|
|
Tools []core.Tool
|
|
|
|
// Middlewares to apply. Appended after HarnessProfile.ExtraMiddlewares.
|
|
Middlewares []core.ReActMiddleware
|
|
|
|
// MaxIterations overrides both HarnessProfile and system default.
|
|
MaxIterations int
|
|
|
|
// SubAgentSpecs declares sub-agents. The SubAgentMiddleware is automatically
|
|
// created with recursion depth from the active HarnessProfile.
|
|
SubAgentSpecs []subagent.SubAgentSpec
|
|
}
|
|
|
|
// ========================================================================
|
|
// Phase 3: Override chain with precedence rules
|
|
// ========================================================================
|
|
|
|
// NewAgent creates a ReActAgent from a declarative AgentConfig.
|
|
//
|
|
// Precedence (highest wins): user explicit > HarnessProfile > ProviderProfile > defaults.
|
|
func NewAgent(ctx context.Context, cfg *AgentConfig) (core.Agent, error) {
|
|
if cfg == nil {
|
|
return nil, fmt.Errorf("profile: AgentConfig is nil")
|
|
}
|
|
|
|
// 1. Resolve model from ModelSpec.
|
|
model, err := buildModel(ctx, cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 2. Build ReActConfig via override chain.
|
|
reactCfg := buildReactConfig(ctx, cfg)
|
|
|
|
// 3. Set the resolved model.
|
|
reactCfg.Model = model
|
|
|
|
// 4. Handle HarnessProfile's ExcludedToolNames.
|
|
if harness := lookupHarness(cfg.HarnessProfileName); harness != nil && len(harness.ExcludedToolNames) > 0 {
|
|
excluded := makeMap(harness.ExcludedToolNames)
|
|
filtered := make([]core.Tool, 0, len(reactCfg.Tools))
|
|
for _, t := range reactCfg.Tools {
|
|
if excluded[t.Name()] {
|
|
continue
|
|
}
|
|
filtered = append(filtered, t)
|
|
}
|
|
reactCfg.Tools = filtered
|
|
}
|
|
|
|
// 5. Apply tool description overrides (wrap matching tools).
|
|
if harness := lookupHarness(cfg.HarnessProfileName); harness != nil && len(harness.ToolDescriptionOverrides) > 0 {
|
|
for i, t := range reactCfg.Tools {
|
|
if newDesc, ok := harness.ToolDescriptionOverrides[t.Name()]; ok && newDesc != "" {
|
|
reactCfg.Tools[i] = &descriptionOverrideTool{Tool: t, newDesc: newDesc}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 6. Handle SubAgentSpecs — create SubAgentMiddleware and init.
|
|
if len(cfg.SubAgentSpecs) > 0 {
|
|
subCfg := &subagent.Config{}
|
|
if harness := lookupHarness(cfg.HarnessProfileName); harness != nil && harness.RecursionDepth > 0 {
|
|
subCfg.MaxDepth = harness.RecursionDepth
|
|
}
|
|
saMW := subagent.New(cfg.SubAgentSpecs, subCfg)
|
|
reactCfg.Middlewares = append(reactCfg.Middlewares, saMW)
|
|
saMW.Init(ctx, reactCfg)
|
|
}
|
|
|
|
return core.NewReActAgent(reactCfg), nil
|
|
}
|
|
|
|
// buildModel resolves the model from ModelSpec + ProviderProfile.
|
|
func buildModel(ctx context.Context, cfg *AgentConfig) (core.Model[*schema.Message], error) {
|
|
providerName, modelName, err := ParseModelSpec(cfg.ModelSpec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
provider := LookupProvider(providerName)
|
|
if provider == nil {
|
|
return nil, fmt.Errorf("profile: unknown provider %q (registered: %s)", providerName, listProviders())
|
|
}
|
|
|
|
// Merge options: DefaultOpts ← ProviderOpts.
|
|
opts := copyMap(provider.DefaultOpts)
|
|
for k, v := range cfg.ProviderOpts {
|
|
opts[k] = v
|
|
}
|
|
|
|
m, err := provider.InitModel(ctx, modelName, opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("profile: InitModel(%s, %s): %w", providerName, modelName, err)
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// buildReactConfig applies the override chain for non-model config fields.
|
|
func buildReactConfig(ctx context.Context, cfg *AgentConfig) *core.ReActConfig[*schema.Message] {
|
|
// Start with system defaults.
|
|
result := &core.ReActConfig[*schema.Message]{
|
|
MaxIterations: 10,
|
|
Instruction: internal.DefaultSystemPrompt,
|
|
}
|
|
|
|
harness := lookupHarness(cfg.HarnessProfileName)
|
|
|
|
// Layer 1: HarnessProfile.
|
|
if harness != nil {
|
|
if harness.MaxIterations > 0 {
|
|
result.MaxIterations = harness.MaxIterations
|
|
}
|
|
if harness.BaseSystemPrompt != nil {
|
|
result.Instruction = *harness.BaseSystemPrompt
|
|
}
|
|
result.Instruction += harness.SystemPromptSuffix
|
|
|
|
// Extra middlewares (appended; ExcludedMiddlewareNames applied later).
|
|
result.Middlewares = append(result.Middlewares, harness.ExtraMiddlewares...)
|
|
}
|
|
|
|
// Layer 2: User explicit config (highest priority).
|
|
if cfg.Instruction != nil {
|
|
result.Instruction = *cfg.Instruction
|
|
}
|
|
if cfg.Tools != nil {
|
|
result.Tools = cfg.Tools
|
|
}
|
|
if cfg.MaxIterations > 0 {
|
|
result.MaxIterations = cfg.MaxIterations
|
|
}
|
|
if cfg.Middlewares != nil {
|
|
result.Middlewares = append(result.Middlewares, cfg.Middlewares...)
|
|
}
|
|
|
|
// Apply ExcludedMiddlewareNames from HarnessProfile.
|
|
if harness != nil && len(harness.ExcludedMiddlewareNames) > 0 {
|
|
result.Middlewares = filterMiddlewareByTypeName(result.Middlewares, harness.ExcludedMiddlewareNames)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// ========================================================================
|
|
// Helpers
|
|
// ========================================================================
|
|
|
|
func lookupHarness(name string) *HarnessProfile {
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
return LookupHarness(name)
|
|
}
|
|
|
|
func listProviders() string {
|
|
var names []string
|
|
providers.Range(func(key, _ any) bool {
|
|
names = append(names, key.(string))
|
|
return true
|
|
})
|
|
return strings.Join(names, ", ")
|
|
}
|
|
|
|
func copyMap(src map[string]any) map[string]any {
|
|
if src == nil {
|
|
return make(map[string]any)
|
|
}
|
|
dst := make(map[string]any, len(src))
|
|
for k, v := range src {
|
|
dst[k] = v
|
|
}
|
|
return dst
|
|
}
|
|
|
|
func makeMap(keys []string) map[string]bool {
|
|
m := make(map[string]bool, len(keys))
|
|
for _, k := range keys {
|
|
m[k] = true
|
|
}
|
|
return m
|
|
}
|
|
|
|
// filterMiddlewareByTypeName removes middlewares whose fmt.Sprintf("%T") matches
|
|
// any name in the exclusion list.
|
|
func filterMiddlewareByTypeName(mws []core.ReActMiddleware, exclude []string) []core.ReActMiddleware {
|
|
if len(exclude) == 0 {
|
|
return mws
|
|
}
|
|
excluded := makeMap(exclude)
|
|
filtered := make([]core.ReActMiddleware, 0, len(mws))
|
|
for _, mw := range mws {
|
|
if mw == nil {
|
|
continue
|
|
}
|
|
typeName := fmt.Sprintf("%T", mw)
|
|
if excluded[typeName] {
|
|
continue
|
|
}
|
|
filtered = append(filtered, mw)
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
// descriptionOverrideTool wraps a Tool to override its Description().
|
|
type descriptionOverrideTool struct {
|
|
core.Tool
|
|
newDesc string
|
|
}
|
|
|
|
func (t *descriptionOverrideTool) Description() string { return t.newDesc }
|
|
|
|
// StrPtr is a helper for creating *string literals.
|
|
func StrPtr(s string) *string { return &s }
|
|
|
|
// Validate checks the AgentConfig for common errors and returns them all at once.
|
|
func Validate(cfg *AgentConfig) []error {
|
|
var errs []error
|
|
if cfg == nil {
|
|
return []error{fmt.Errorf("profile: AgentConfig is nil")}
|
|
}
|
|
if cfg.ModelSpec == "" {
|
|
errs = append(errs, fmt.Errorf("profile: ModelSpec is required"))
|
|
} else if _, _, err := ParseModelSpec(cfg.ModelSpec); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
if cfg.HarnessProfileName != "" && LookupHarness(cfg.HarnessProfileName) == nil {
|
|
errs = append(errs, fmt.Errorf("profile: harness profile %q not found", cfg.HarnessProfileName))
|
|
}
|
|
return errs
|
|
}
|
|
|
|
// ClearProviders removes all registered providers. Used in tests for isolation.
|
|
func ClearProviders() {
|
|
providers = sync.Map{}
|
|
}
|
|
|
|
// ClearHarnesses removes all registered harness profiles. Used in tests.
|
|
func ClearHarnesses() {
|
|
harnesses = sync.Map{}
|
|
}
|
|
|
|
// RegisterProviderModel is a convenience wrapper that registers both a provider
|
|
// profile (using ProviderProfile.Name) AND a harness profile for each supported
|
|
// model. This matches deepagents' double-registration pattern.
|
|
//
|
|
// Deprecated: Use separate RegisterProvider and RegisterHarness calls instead.
|
|
func RegisterProviderModel(provider *ProviderProfile, harnessProfiles ...*HarnessProfile) {
|
|
RegisterProvider(provider)
|
|
for _, h := range harnessProfiles {
|
|
RegisterHarness(h)
|
|
}
|
|
}
|