Files
Travis Cline db2a947ba4 agents: fix ChainCallOption silent failure (#1420)
* llms/openai: sanitize HTTP errors to prevent API key exposure (#1393)

Fix security issue where context deadline errors could expose API keys and
sensitive request details in error messages. Added sanitizeHTTPError function
to detect context timeouts and network errors, then return generic error
messages without exposing sensitive information.

Changes:
- Added sanitizeHTTPError() function to sanitize HTTP client errors
- Updated chat.go to use sanitizeHTTPError() for http.Do() errors
- Updated embeddings.go to use sanitizeHTTPError() for http.Do() errors
- Added comprehensive test cases to prevent regression

* agents: fix ChainCallOption silent failure (#1416)

Fix issue where ChainCallOption parameters were silently ignored by Executor.Call() and Agent implementations.

Changes:
- Updated Agent.Plan() interface signature to accept variadic ChainCallOption parameters
- Updated Executor.Call() to accept and propagate options to Agent.Plan()
- Updated Executor.doIteration() to propagate options through the chain
- Updated OneShotZeroAgent.Plan() to accept and pass options to chains.Predict()
- Updated ConversationalAgent.Plan() to accept and pass options to chains.Predict()
- Updated OpenAIFunctionsAgent.Plan() to accept and pass options to LLM.GenerateContent()
- Exported GetLLMCallOptions() function for option conversion (was getLLMCallOptions)
- Updated test mock to match new Agent interface signature

Now users can pass LLM configuration options (temperature, max tokens, etc.) through executors to agents.
2025-10-19 17:03:11 -07:00

202 lines
5.0 KiB
Go

package agents
import (
"context"
"errors"
"fmt"
"strings"
"github.com/tmc/langchaingo/callbacks"
"github.com/tmc/langchaingo/chains"
"github.com/tmc/langchaingo/schema"
"github.com/tmc/langchaingo/tools"
)
const _intermediateStepsOutputKey = "intermediateSteps"
// Executor is the chain responsible for running agents.
type Executor struct {
Agent Agent
Memory schema.Memory
CallbacksHandler callbacks.Handler
ErrorHandler *ParserErrorHandler
MaxIterations int
ReturnIntermediateSteps bool
}
var (
_ chains.Chain = &Executor{}
_ callbacks.HandlerHaver = &Executor{}
)
// NewExecutor creates a new agent executor with an agent and the tools the agent can use.
func NewExecutor(agent Agent, opts ...Option) *Executor {
options := executorDefaultOptions()
for _, opt := range opts {
opt(&options)
}
return &Executor{
Agent: agent,
Memory: options.memory,
MaxIterations: options.maxIterations,
ReturnIntermediateSteps: options.returnIntermediateSteps,
CallbacksHandler: options.callbacksHandler,
ErrorHandler: options.errorHandler,
}
}
func (e *Executor) Call(ctx context.Context, inputValues map[string]any, options ...chains.ChainCallOption) (map[string]any, error) { //nolint:lll
inputs, err := inputsToString(inputValues)
if err != nil {
return nil, err
}
nameToTool := getNameToTool(e.Agent.GetTools())
steps := make([]schema.AgentStep, 0)
for i := 0; i < e.MaxIterations; i++ {
var finish map[string]any
steps, finish, err = e.doIteration(ctx, steps, nameToTool, inputs, options...)
if finish != nil || err != nil {
return finish, err
}
}
if e.CallbacksHandler != nil {
e.CallbacksHandler.HandleAgentFinish(ctx, schema.AgentFinish{
ReturnValues: map[string]any{"output": ErrNotFinished.Error()},
})
}
return e.getReturn(
&schema.AgentFinish{ReturnValues: make(map[string]any)},
steps,
), ErrNotFinished
}
func (e *Executor) doIteration( // nolint
ctx context.Context,
steps []schema.AgentStep,
nameToTool map[string]tools.Tool,
inputs map[string]string,
options ...chains.ChainCallOption,
) ([]schema.AgentStep, map[string]any, error) {
actions, finish, err := e.Agent.Plan(ctx, steps, inputs, options...)
if errors.Is(err, ErrUnableToParseOutput) && e.ErrorHandler != nil {
formattedObservation := err.Error()
if e.ErrorHandler.Formatter != nil {
formattedObservation = e.ErrorHandler.Formatter(formattedObservation)
}
steps = append(steps, schema.AgentStep{
Observation: formattedObservation,
})
return steps, nil, nil
}
if err != nil {
return steps, nil, err
}
if len(actions) == 0 && finish == nil {
return steps, nil, ErrAgentNoReturn
}
if finish != nil {
if e.CallbacksHandler != nil {
e.CallbacksHandler.HandleAgentFinish(ctx, *finish)
}
return steps, e.getReturn(finish, steps), nil
}
for _, action := range actions {
steps, err = e.doAction(ctx, steps, nameToTool, action)
if err != nil {
return steps, nil, err
}
}
return steps, nil, nil
}
func (e *Executor) doAction(
ctx context.Context,
steps []schema.AgentStep,
nameToTool map[string]tools.Tool,
action schema.AgentAction,
) ([]schema.AgentStep, error) {
if e.CallbacksHandler != nil {
e.CallbacksHandler.HandleAgentAction(ctx, action)
}
tool, ok := nameToTool[strings.ToUpper(action.Tool)]
if !ok {
return append(steps, schema.AgentStep{
Action: action,
Observation: fmt.Sprintf("%s is not a valid tool, try another one", action.Tool),
}), nil
}
observation, err := tool.Call(ctx, strings.TrimSuffix(action.ToolInput, "\nObservation:"))
if err != nil {
return nil, err
}
return append(steps, schema.AgentStep{
Action: action,
Observation: observation,
}), nil
}
func (e *Executor) getReturn(finish *schema.AgentFinish, steps []schema.AgentStep) map[string]any {
if e.ReturnIntermediateSteps {
finish.ReturnValues[_intermediateStepsOutputKey] = steps
}
return finish.ReturnValues
}
// GetInputKeys gets the input keys the agent of the executor expects.
// Often "input".
func (e *Executor) GetInputKeys() []string {
return e.Agent.GetInputKeys()
}
// GetOutputKeys gets the output keys the agent of the executor returns.
func (e *Executor) GetOutputKeys() []string {
return e.Agent.GetOutputKeys()
}
func (e *Executor) GetMemory() schema.Memory { //nolint:ireturn
return e.Memory
}
func (e *Executor) GetCallbackHandler() callbacks.Handler { //nolint:ireturn
return e.CallbacksHandler
}
func inputsToString(inputValues map[string]any) (map[string]string, error) {
inputs := make(map[string]string, len(inputValues))
for key, value := range inputValues {
valueStr, ok := value.(string)
if !ok {
return nil, fmt.Errorf("%w: %s", ErrExecutorInputNotString, key)
}
inputs[key] = valueStr
}
return inputs, nil
}
func getNameToTool(t []tools.Tool) map[string]tools.Tool {
if len(t) == 0 {
return nil
}
nameToTool := make(map[string]tools.Tool, len(t))
for _, tool := range t {
nameToTool[strings.ToUpper(tool.Name())] = tool
}
return nameToTool
}