feat: add repetition detection fallback to orchestrator pipeline
Se añade un mecanismo de detección de repetición para cortar conversaciones circulares entre agentes cuando hablan sin parar. - Nuevo campo RepetitionThreshold en OrchestrationCfg (schema.go). - Función detectRepetition() compara cada nueva respuesta con las anteriores usando similitud de bigramas (Dice coefficient). - Si la similitud supera el umbral (default 0.6), el pipeline se detiene inmediatamente con un log de warning, antes de gastar una llamada LLM en la evaluación de calidad. - Funciones auxiliares: similarity() y makeBigrams() para el cálculo. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -418,10 +418,11 @@ type SpecialMeta struct {
|
||||
|
||||
// OrchestrationCfg configures the multi-bot orchestrator.
|
||||
type OrchestrationCfg struct {
|
||||
MaxIterations int `yaml:"max_iterations"`
|
||||
QualityThreshold float64 `yaml:"quality_threshold"`
|
||||
DelegationTimeout time.Duration `yaml:"delegation_timeout"`
|
||||
Rooms []OrchestratedRoomCfg `yaml:"rooms"`
|
||||
MaxIterations int `yaml:"max_iterations"`
|
||||
QualityThreshold float64 `yaml:"quality_threshold"`
|
||||
DelegationTimeout time.Duration `yaml:"delegation_timeout"`
|
||||
RepetitionThreshold float64 `yaml:"repetition_threshold"` // 0-1: similarity ratio to detect circular conversations
|
||||
Rooms []OrchestratedRoomCfg `yaml:"rooms"`
|
||||
}
|
||||
|
||||
// OrchestratedRoomCfg defines a room managed by the orchestrator.
|
||||
|
||||
@@ -331,6 +331,15 @@ func (o *Orchestrator) Route(ctx context.Context, msgCtx decision.MessageContext
|
||||
"iteration", i,
|
||||
)
|
||||
|
||||
// Fallback: detect circular conversations before quality evaluation
|
||||
if o.detectRepetition(responses) {
|
||||
o.logger.Warn("repetition detected, stopping pipeline to prevent circular conversation",
|
||||
"iteration", i+1,
|
||||
"total_responses", len(responses),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
// Evaluate quality (Fase 3)
|
||||
score := o.evaluate(ctx, msgCtx.Content, response)
|
||||
o.logger.Info("quality evaluated",
|
||||
@@ -462,6 +471,83 @@ func (o *Orchestrator) buildParticipantsList(botIDs []string, exclude string) st
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// detectRepetition checks if a new response is too similar to previous responses,
|
||||
// indicating a circular conversation that should be stopped.
|
||||
// Returns true if the conversation should be terminated.
|
||||
func (o *Orchestrator) detectRepetition(responses []orchestration.BotResponse) bool {
|
||||
if len(responses) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
threshold := o.cfg.Orchestration.RepetitionThreshold
|
||||
if threshold <= 0 {
|
||||
threshold = 0.6 // default
|
||||
}
|
||||
|
||||
latest := responses[len(responses)-1].Text
|
||||
for i := 0; i < len(responses)-1; i++ {
|
||||
if similarity(latest, responses[i].Text) >= threshold {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// similarity computes a simple bigram-based similarity ratio between two strings.
|
||||
// Returns a value between 0.0 (completely different) and 1.0 (identical).
|
||||
func similarity(a, b string) float64 {
|
||||
if a == b {
|
||||
return 1.0
|
||||
}
|
||||
a = strings.ToLower(strings.TrimSpace(a))
|
||||
b = strings.ToLower(strings.TrimSpace(b))
|
||||
if a == b {
|
||||
return 1.0
|
||||
}
|
||||
if len(a) < 2 || len(b) < 2 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
bigramsA := makeBigrams(a)
|
||||
bigramsB := makeBigrams(b)
|
||||
|
||||
// Count intersection
|
||||
intersection := 0
|
||||
for bg, countA := range bigramsA {
|
||||
if countB, ok := bigramsB[bg]; ok {
|
||||
if countA < countB {
|
||||
intersection += countA
|
||||
} else {
|
||||
intersection += countB
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalA := 0
|
||||
for _, c := range bigramsA {
|
||||
totalA += c
|
||||
}
|
||||
totalB := 0
|
||||
for _, c := range bigramsB {
|
||||
totalB += c
|
||||
}
|
||||
|
||||
if totalA+totalB == 0 {
|
||||
return 0.0
|
||||
}
|
||||
return float64(2*intersection) / float64(totalA+totalB)
|
||||
}
|
||||
|
||||
func makeBigrams(s string) map[string]int {
|
||||
runes := []rune(s)
|
||||
bgs := make(map[string]int, len(runes))
|
||||
for i := 0; i < len(runes)-1; i++ {
|
||||
bg := string(runes[i : i+2])
|
||||
bgs[bg]++
|
||||
}
|
||||
return bgs
|
||||
}
|
||||
|
||||
func truncate(s string, n int) string {
|
||||
runes := []rune(s)
|
||||
if len(runes) <= n {
|
||||
|
||||
Reference in New Issue
Block a user