mirror of
https://github.com/vxcontrol/langchaingo.git
synced 2026-07-21 00:45:22 -04:00
db2a947ba4
* 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.
202 lines
5.0 KiB
Go
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
|
|
}
|