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