Skip to content

Commit c8bbeb8

Browse files
committed
feat: implement hybrid history-aware retriever strategy
Signed-off-by: zhoujinyu <2319109590@qq.com>
1 parent 113a3b6 commit c8bbeb8

11 files changed

Lines changed: 823 additions & 118 deletions

config/config.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1697,6 +1697,7 @@ global:
16971697
fallback_to_empty: true
16981698
advanced_filtering:
16991699
enabled: true
1700+
retrieval_strategy: weighted
17001701
candidate_pool_size: 50
17011702
min_lexical_overlap: 1
17021703
min_combined_score: 0.42
@@ -1710,6 +1711,14 @@ global:
17101711
category_confidence_threshold: 0.7
17111712
allow_tools: [docs.search, tickets.lookup]
17121713
block_tools: [admin.delete]
1714+
hybrid_history:
1715+
history_horizon: 8
1716+
min_history_steps: 1
1717+
history_confidence_threshold: 0
1718+
weight_semantic: 1
1719+
weight_history_transition: 1
1720+
weight_decision_prior: 1
1721+
repetition_penalty_strength: 0
17131722
looper:
17141723
endpoint: http://localhost:8899/v1/chat/completions
17151724
model_endpoints:

src/semantic-router/pkg/config/model_config_types.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -193,15 +193,28 @@ type ToolFilteringWeights struct {
193193
}
194194

195195
type AdvancedToolFilteringConfig struct {
196-
Enabled bool `yaml:"enabled"`
197-
CandidatePoolSize *int `yaml:"candidate_pool_size,omitempty"`
198-
MinLexicalOverlap *int `yaml:"min_lexical_overlap,omitempty"`
199-
MinCombinedScore *float32 `yaml:"min_combined_score,omitempty"`
200-
Weights ToolFilteringWeights `yaml:"weights,omitempty"`
201-
UseCategoryFilter *bool `yaml:"use_category_filter,omitempty"`
202-
CategoryConfidenceThreshold *float32 `yaml:"category_confidence_threshold,omitempty"`
203-
AllowTools []string `yaml:"allow_tools,omitempty"`
204-
BlockTools []string `yaml:"block_tools,omitempty"`
196+
Enabled bool `yaml:"enabled"`
197+
RetrievalStrategy string `yaml:"retrieval_strategy,omitempty"`
198+
CandidatePoolSize *int `yaml:"candidate_pool_size,omitempty"`
199+
MinLexicalOverlap *int `yaml:"min_lexical_overlap,omitempty"`
200+
MinCombinedScore *float32 `yaml:"min_combined_score,omitempty"`
201+
Weights ToolFilteringWeights `yaml:"weights,omitempty"`
202+
UseCategoryFilter *bool `yaml:"use_category_filter,omitempty"`
203+
CategoryConfidenceThreshold *float32 `yaml:"category_confidence_threshold,omitempty"`
204+
AllowTools []string `yaml:"allow_tools,omitempty"`
205+
BlockTools []string `yaml:"block_tools,omitempty"`
206+
HybridHistory *HybridHistoryToolRetrievalConfig `yaml:"hybrid_history,omitempty"`
207+
}
208+
209+
// HybridHistoryToolRetrievalConfig tunes hybrid_history retrieval (semantic + short history + priors + repetition).
210+
type HybridHistoryToolRetrievalConfig struct {
211+
HistoryHorizon *int `yaml:"history_horizon,omitempty"`
212+
MinHistorySteps *int `yaml:"min_history_steps,omitempty"`
213+
HistoryConfidenceThreshold *float32 `yaml:"history_confidence_threshold,omitempty"`
214+
WeightSemantic *float32 `yaml:"weight_semantic,omitempty"`
215+
WeightHistoryTransition *float32 `yaml:"weight_history_transition,omitempty"`
216+
WeightDecisionPrior *float32 `yaml:"weight_decision_prior,omitempty"`
217+
RepetitionPenaltyStrength *float32 `yaml:"repetition_penalty_strength,omitempty"`
205218
}
206219

207220
type ToolsConfig struct {

src/semantic-router/pkg/config/reference_config_global_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@ func assertReferenceConfigIntegrationGlobalCoverage(t testingT, integrations map
145145
assertMapCoversStructFields(t, integrations, reflect.TypeOf(CanonicalIntegrationGlobal{}), "global.integrations")
146146
assertMapCoversStructFields(t, tools, reflect.TypeOf(ToolsConfig{}), "global.integrations.tools")
147147
assertMapCoversStructFields(t, mustMapAt(t, tools, "advanced_filtering"), reflect.TypeOf(AdvancedToolFilteringConfig{}), "global.integrations.tools.advanced_filtering")
148+
assertMapCoversStructFields(
149+
t,
150+
mustMapAt(t, tools, "advanced_filtering", "hybrid_history"),
151+
reflect.TypeOf(HybridHistoryToolRetrievalConfig{}),
152+
"global.integrations.tools.advanced_filtering.hybrid_history",
153+
)
148154
assertMapCoversStructFields(
149155
t,
150156
mustMapAt(t, tools, "advanced_filtering", "weights"),
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package config
2+
3+
import "strings"
4+
5+
const (
6+
// ToolRetrievalStrategyWeighted is the default: embedding + lexical/tag/name/category weights.
7+
ToolRetrievalStrategyWeighted = "weighted"
8+
// ToolRetrievalStrategyHybridHistory combines semantic similarity with short-horizon tool history.
9+
ToolRetrievalStrategyHybridHistory = "hybrid_history"
10+
)
11+
12+
// EffectiveToolRetrievalStrategy returns normalized strategy; empty defaults to weighted.
13+
func EffectiveToolRetrievalStrategy(advanced *AdvancedToolFilteringConfig) string {
14+
if advanced == nil {
15+
return ToolRetrievalStrategyWeighted
16+
}
17+
s := strings.TrimSpace(strings.ToLower(advanced.RetrievalStrategy))
18+
if s == "" {
19+
return ToolRetrievalStrategyWeighted
20+
}
21+
return s
22+
}
23+
24+
// IsHybridHistoryRetrieval reports whether advanced filtering should use hybrid_history ranking.
25+
func IsHybridHistoryRetrieval(advanced *AdvancedToolFilteringConfig) bool {
26+
return EffectiveToolRetrievalStrategy(advanced) == ToolRetrievalStrategyHybridHistory
27+
}
28+
29+
// ResolveHybridHistoryHorizon returns the max assistant tool names to read from history.
30+
func ResolveHybridHistoryHorizon(advanced *AdvancedToolFilteringConfig) int {
31+
const defaultHorizon = 8
32+
if advanced == nil || advanced.HybridHistory == nil || advanced.HybridHistory.HistoryHorizon == nil {
33+
return defaultHorizon
34+
}
35+
h := *advanced.HybridHistory.HistoryHorizon
36+
if h <= 0 {
37+
return defaultHorizon
38+
}
39+
return h
40+
}

src/semantic-router/pkg/config/validator_tool_filtering.go

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,34 @@
11
package config
22

3-
import "fmt"
3+
import (
4+
"fmt"
5+
"strings"
6+
)
47

58
func validateAdvancedToolFilteringConfig(cfg *RouterConfig) error {
69
if cfg == nil || cfg.Tools.AdvancedFiltering == nil {
710
return nil
811
}
9-
1012
advanced := cfg.Tools.AdvancedFiltering
1113
if !advanced.Enabled {
1214
return nil
1315
}
16+
if err := validateAdvancedToolFilteringIntFields(advanced); err != nil {
17+
return err
18+
}
19+
if err := validateAdvancedToolFilteringCoreFloats(advanced); err != nil {
20+
return err
21+
}
22+
if err := validateToolFilteringWeightFloats(advanced.Weights); err != nil {
23+
return err
24+
}
25+
if err := validateRetrievalStrategyValue(advanced.RetrievalStrategy); err != nil {
26+
return err
27+
}
28+
return validateHybridHistorySubconfig(advanced.HybridHistory)
29+
}
1430

31+
func validateAdvancedToolFilteringIntFields(advanced *AdvancedToolFilteringConfig) error {
1532
for _, field := range []struct {
1633
name string
1734
value *int
@@ -23,7 +40,10 @@ func validateAdvancedToolFilteringConfig(cfg *RouterConfig) error {
2340
return err
2441
}
2542
}
43+
return nil
44+
}
2645

46+
func validateAdvancedToolFilteringCoreFloats(advanced *AdvancedToolFilteringConfig) error {
2747
for _, field := range []struct {
2848
name string
2949
value *float32
@@ -35,22 +55,67 @@ func validateAdvancedToolFilteringConfig(cfg *RouterConfig) error {
3555
return err
3656
}
3757
}
58+
return nil
59+
}
3860

61+
func validateToolFilteringWeightFloats(weights ToolFilteringWeights) error {
3962
for _, field := range []struct {
4063
name string
4164
value *float32
4265
}{
43-
{"embed", advanced.Weights.Embed},
44-
{"lexical", advanced.Weights.Lexical},
45-
{"tag", advanced.Weights.Tag},
46-
{"name", advanced.Weights.Name},
47-
{"category", advanced.Weights.Category},
66+
{"embed", weights.Embed},
67+
{"lexical", weights.Lexical},
68+
{"tag", weights.Tag},
69+
{"name", weights.Name},
70+
{"category", weights.Category},
4871
} {
4972
if err := validateAdvancedToolFilteringUnitFloat("weights."+field.name, field.value); err != nil {
5073
return err
5174
}
5275
}
76+
return nil
77+
}
78+
79+
func validateRetrievalStrategyValue(strategy string) error {
80+
s := strings.TrimSpace(strings.ToLower(strategy))
81+
if s == "" {
82+
return nil
83+
}
84+
if s == ToolRetrievalStrategyWeighted || s == ToolRetrievalStrategyHybridHistory {
85+
return nil
86+
}
87+
return fmt.Errorf("tools.advanced_filtering.retrieval_strategy must be %q or %q", ToolRetrievalStrategyWeighted, ToolRetrievalStrategyHybridHistory)
88+
}
5389

90+
func validateHybridHistorySubconfig(h *HybridHistoryToolRetrievalConfig) error {
91+
if h == nil {
92+
return nil
93+
}
94+
for _, field := range []struct {
95+
name string
96+
value *int
97+
}{
98+
{"history_horizon", h.HistoryHorizon},
99+
{"min_history_steps", h.MinHistorySteps},
100+
} {
101+
if err := validateAdvancedToolFilteringNonNegativeInt("hybrid_history."+field.name, field.value); err != nil {
102+
return err
103+
}
104+
}
105+
for _, field := range []struct {
106+
name string
107+
value *float32
108+
}{
109+
{"history_confidence_threshold", h.HistoryConfidenceThreshold},
110+
{"weight_semantic", h.WeightSemantic},
111+
{"weight_history_transition", h.WeightHistoryTransition},
112+
{"weight_decision_prior", h.WeightDecisionPrior},
113+
{"repetition_penalty_strength", h.RepetitionPenaltyStrength},
114+
} {
115+
if err := validateAdvancedToolFilteringUnitFloat("hybrid_history."+field.name, field.value); err != nil {
116+
return err
117+
}
118+
}
54119
return nil
55120
}
56121

0 commit comments

Comments
 (0)