// Package prebuilt provides pre-built components for common Agent Harness patterns. package prebuilt import ( "context" "fmt" "ragflow/internal/harness/graph/runnable" ) // ReactAgentConfig holds configuration for a ReAct agent. type ReactAgentConfig struct { // Tools available to the agent Tools []Tool // LLM model to use Model LLM // System prompt SystemPrompt string // Maximum iterations MaxIterations int // Stop condition StopCondition func(*ReActState) bool } // ReActState represents the state of a ReAct agent. type ReActState struct { // Input from user Input string // Current thought Thought string // Current action Action string // Observation from action Observation string // Final answer Answer string // Iteration count Iteration int // Tool calls history ToolCalls []ToolCall } // Tool represents a tool that can be called by the agent. type Tool struct { Name string Description string Function func(context.Context, map[string]interface{}) (interface{}, error) Schema map[string]interface{} } // ToolCall represents a call to a tool. type ToolCall struct { ToolName string Input map[string]interface{} Output interface{} Error error } // LLM represents a language model. type LLM interface { Generate(ctx context.Context, messages []map[string]interface{}) (string, error) GenerateStream(ctx context.Context, messages []map[string]interface{}) (<-chan string, error) } // NewReactAgent creates a new ReAct (Reasoning + Acting) agent. func NewReactAgent(config ReactAgentConfig) (runnable.Runnable[map[string]interface{}, map[string]interface{}], error) { if len(config.Tools) == 0 { return nil, fmt.Errorf("at least one tool is required") } if config.Model == nil { return nil, fmt.Errorf("model is required") } if config.MaxIterations <= 0 { config.MaxIterations = 10 } // Create the agent as a runnable agent := runnable.NewRunnableFunc( func(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) { state := &ReActState{ Input: fmt.Sprintf("%v", input["input"]), Iteration: 0, ToolCalls: make([]ToolCall, 0), } for state.Iteration < config.MaxIterations { // Check stop condition if config.StopCondition != nil && config.StopCondition(state) { break } // Generate thought thought, err := config.Model.Generate(ctx, buildMessages(state, config.SystemPrompt)) if err != nil { return nil, fmt.Errorf("failed to generate thought: %w", err) } state.Thought = thought // Parse action from thought (simplified) action := parseAction(thought) state.Action = action if action == "ANSWER" { // Extract answer state.Answer = extractAnswer(thought) break } // Execute tool toolOutput, err := executeTool(ctx, action, input, config.Tools) state.Observation = fmt.Sprintf("%v", toolOutput) state.ToolCalls = append(state.ToolCalls, ToolCall{ ToolName: action, Input: input, Output: toolOutput, Error: err, }) if err != nil { state.Observation = fmt.Sprintf("Tool error: %v", err) } state.Iteration++ } return map[string]interface{}{ "output": state.Answer, "thoughts": state.Thought, "iterations": state.Iteration, "tool_calls": state.ToolCalls, "final_state": state, }, nil }, runnable.WithName[map[string]interface{}, map[string]interface{}]("react_agent"), runnable.WithDescription[map[string]interface{}, map[string]interface{}]("ReAct agent with tools"), ) return agent, nil } // ToolNode creates a node that executes a tool. func ToolNode(tool Tool) runnable.Runnable[map[string]interface{}, map[string]interface{}] { return runnable.NewRunnableFunc( func(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) { output, err := tool.Function(ctx, input) if err != nil { return nil, fmt.Errorf("tool %s failed: %w", tool.Name, err) } return map[string]interface{}{ "tool": tool.Name, "input": input, "output": output, "success": true, "metadata": map[string]interface{}{"tool_schema": tool.Schema}, }, nil }, runnable.WithName[map[string]interface{}, map[string]interface{}](fmt.Sprintf("tool_%s", tool.Name)), runnable.WithDescription[map[string]interface{}, map[string]interface{}](tool.Description), ) } // ValidationNode creates a node that validates input. func ValidationNode( validateFunc func(map[string]interface{}) error, errorMessage string, ) runnable.Runnable[map[string]interface{}, map[string]interface{}] { return runnable.NewRunnableFunc( func(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) { if err := validateFunc(input); err != nil { return nil, fmt.Errorf("%s: %w", errorMessage, err) } // Pass through input if valid return input, nil }, runnable.WithName[map[string]interface{}, map[string]interface{}]("validation_node"), runnable.WithDescription[map[string]interface{}, map[string]interface{}]("Input validation node"), ) } // ConditionalNode creates a node that routes based on a condition. func ConditionalNode( condition func(map[string]interface{}) string, branches map[string]runnable.Runnable[map[string]interface{}, map[string]interface{}], defaultBranch string, ) runnable.Runnable[map[string]interface{}, map[string]interface{}] { return runnable.NewRunnableFunc( func(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) { branchName := condition(input) branch, exists := branches[branchName] if !exists { if defaultBranch == "" { return nil, fmt.Errorf("no branch for condition '%s' and no default branch", branchName) } branch = branches[defaultBranch] if branch == nil { return nil, fmt.Errorf("default branch '%s' not found", defaultBranch) } } return branch.Invoke(ctx, input) }, runnable.WithName[map[string]interface{}, map[string]interface{}]("conditional_node"), runnable.WithDescription[map[string]interface{}, map[string]interface{}]("Conditional routing node"), ) } // TransformNode creates a node that transforms input. func TransformNode( transformFunc func(map[string]interface{}) (map[string]interface{}, error), ) runnable.Runnable[map[string]interface{}, map[string]interface{}] { return runnable.NewRunnableFunc( func(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) { return transformFunc(input) }, runnable.WithName[map[string]interface{}, map[string]interface{}]("transform_node"), runnable.WithDescription[map[string]interface{}, map[string]interface{}]("Input transformation node"), ) } // Helper functions func buildMessages(state *ReActState, systemPrompt string) []map[string]interface{} { messages := make([]map[string]interface{}, 0) if systemPrompt != "" { messages = append(messages, map[string]interface{}{ "role": "system", "content": systemPrompt, }) } messages = append(messages, map[string]interface{}{ "role": "user", "content": state.Input, }) if state.Thought != "" { messages = append(messages, map[string]interface{}{ "role": "assistant", "content": state.Thought, }) } if state.Observation != "" { messages = append(messages, map[string]interface{}{ "role": "system", "content": state.Observation, }) } return messages } func parseAction(thought string) string { // Simplified parsing - in reality would use more sophisticated parsing if len(thought) > 10 && thought[:5] == "THINK" { return "THINK" } if len(thought) > 10 && thought[:6] == "ACTION" { // Extract tool name return "TOOL_CALL" } if len(thought) > 10 && thought[:6] == "ANSWER" { return "ANSWER" } return "THINK" } func extractAnswer(thought string) string { // Simplified extraction return thought } func executeTool(ctx context.Context, action string, input map[string]interface{}, tools []Tool) (interface{}, error) { // Find the tool for _, tool := range tools { if tool.Name == action { return tool.Function(ctx, input) } } return nil, fmt.Errorf("tool not found: %s", action) }