feat: add loop detection

Signed-off-by: mudler <mudler@localai.io>
This commit is contained in:
mudler
2025-04-09 19:13:41 +02:00
parent 1c4ab09335
commit 0eb68b6c20
6 changed files with 59 additions and 0 deletions

View File

@@ -489,6 +489,23 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
} }
} }
// check if the agent is looping over the same action
// if so, we need to stop it
if a.options.loopDetectionSteps > 0 && len(job.GetPastActions()) > 0 {
count := map[string]int{}
for i := len(job.GetPastActions()) - 1; i >= 0; i-- {
pastAction := job.GetPastActions()[i]
if pastAction.Action.Definition().Name == chosenAction.Definition().Name &&
pastAction.Params.String() == actionParams.String() {
count[chosenAction.Definition().Name.String()]++
}
}
if count[chosenAction.Definition().Name.String()] > a.options.loopDetectionSteps {
xlog.Info("Loop detected, stopping agent", "agent", a.Character.Name, "action", chosenAction.Definition().Name)
chosenAction = nil
}
}
//xlog.Debug("Picked action", "agent", a.Character.Name, "action", chosenAction.Definition().Name, "reasoning", reasoning) //xlog.Debug("Picked action", "agent", a.Character.Name, "action", chosenAction.Definition().Name, "reasoning", reasoning)
if chosenAction == nil { if chosenAction == nil {
// If no action was picked up, the reasoning is the message returned by the assistant // If no action was picked up, the reasoning is the message returned by the assistant
@@ -551,6 +568,8 @@ func (a *Agent) consumeJob(job *types.Job, role string) {
return return
} }
job.AddPastAction(chosenAction, &actionParams)
var err error var err error
conv, err = a.handlePlanning(job.GetContext(), job, chosenAction, actionParams, reasoning, pickTemplate, conv) conv, err = a.handlePlanning(job.GetContext(), job, chosenAction, actionParams, reasoning, pickTemplate, conv)
if err != nil { if err != nil {

View File

@@ -126,6 +126,7 @@ var _ = Describe("Agent test", func() {
agent, err := New( agent, err := New(
WithLLMAPIURL(apiURL), WithLLMAPIURL(apiURL),
WithModel(testModel), WithModel(testModel),
WithLoopDetectionSteps(3),
// WithRandomIdentity(), // WithRandomIdentity(),
WithActions(&TestAction{response: map[string]string{ WithActions(&TestAction{response: map[string]string{
"boston": testActionResult, "boston": testActionResult,

View File

@@ -28,6 +28,7 @@ type options struct {
canStopItself bool canStopItself bool
initiateConversations bool initiateConversations bool
loopDetectionSteps int
forceReasoning bool forceReasoning bool
canPlan bool canPlan bool
characterfile string characterfile string
@@ -113,6 +114,13 @@ func WithTimeout(timeout string) Option {
} }
} }
func WithLoopDetectionSteps(steps int) Option {
return func(o *options) error {
o.loopDetectionSteps = steps
return nil
}
}
func WithConversationsPath(path string) Option { func WithConversationsPath(path string) Option {
return func(o *options) error { return func(o *options) error {
o.conversationsPath = path o.conversationsPath = path

View File

@@ -56,6 +56,7 @@ type AgentConfig struct {
EnableKnowledgeBase bool `json:"enable_kb" form:"enable_kb"` EnableKnowledgeBase bool `json:"enable_kb" form:"enable_kb"`
EnableReasoning bool `json:"enable_reasoning" form:"enable_reasoning"` EnableReasoning bool `json:"enable_reasoning" form:"enable_reasoning"`
KnowledgeBaseResults int `json:"kb_results" form:"kb_results"` KnowledgeBaseResults int `json:"kb_results" form:"kb_results"`
LoopDetectionSteps int `json:"loop_detection_steps" form:"loop_detection_steps"`
CanStopItself bool `json:"can_stop_itself" form:"can_stop_itself"` CanStopItself bool `json:"can_stop_itself" form:"can_stop_itself"`
SystemPrompt string `json:"system_prompt" form:"system_prompt"` SystemPrompt string `json:"system_prompt" form:"system_prompt"`
LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"` LongTermMemory bool `json:"long_term_memory" form:"long_term_memory"`
@@ -250,6 +251,15 @@ func NewAgentConfigMeta(
HelpText: "Enable agent to explain its reasoning process", HelpText: "Enable agent to explain its reasoning process",
Tags: config.Tags{Section: "AdvancedSettings"}, Tags: config.Tags{Section: "AdvancedSettings"},
}, },
{
Name: "loop_detection_steps",
Label: "Max Loop Detection Steps",
Type: "number",
DefaultValue: 5,
Min: 1,
Step: 1,
Tags: config.Tags{Section: "AdvancedSettings"},
},
}, },
MCPServers: []config.Field{ MCPServers: []config.Field{
{ {

View File

@@ -461,6 +461,10 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error
opts = append(opts, EnableKnowledgeBaseWithResults(config.KnowledgeBaseResults)) opts = append(opts, EnableKnowledgeBaseWithResults(config.KnowledgeBaseResults))
} }
if config.LoopDetectionSteps > 0 {
opts = append(opts, WithLoopDetectionSteps(config.LoopDetectionSteps))
}
xlog.Info("Starting agent", "name", name, "config", config) xlog.Info("Starting agent", "name", name, "config", config)
agent, err := New(opts...) agent, err := New(opts...)

View File

@@ -20,6 +20,7 @@ type Job struct {
UUID string UUID string
Metadata map[string]interface{} Metadata map[string]interface{}
pastActions []*ActionRequest
nextAction *Action nextAction *Action
nextActionParams *ActionParams nextActionParams *ActionParams
nextActionReasoning string nextActionReasoning string
@@ -28,6 +29,11 @@ type Job struct {
cancel context.CancelFunc cancel context.CancelFunc
} }
type ActionRequest struct {
Action Action
Params *ActionParams
}
type JobOption func(*Job) type JobOption func(*Job)
func WithConversationHistory(history []openai.ChatCompletionMessage) JobOption { func WithConversationHistory(history []openai.ChatCompletionMessage) JobOption {
@@ -82,6 +88,17 @@ func (j *Job) SetNextAction(action *Action, params *ActionParams, reasoning stri
j.nextActionReasoning = reasoning j.nextActionReasoning = reasoning
} }
func (j *Job) AddPastAction(action Action, params *ActionParams) {
j.pastActions = append(j.pastActions, &ActionRequest{
Action: action,
Params: params,
})
}
func (j *Job) GetPastActions() []*ActionRequest {
return j.pastActions
}
func (j *Job) GetNextAction() (*Action, *ActionParams, string) { func (j *Job) GetNextAction() (*Action, *ActionParams, string) {
return j.nextAction, j.nextActionParams, j.nextActionReasoning return j.nextAction, j.nextActionParams, j.nextActionReasoning
} }