feat: add router package with heuristic strategy

This commit is contained in:
2026-05-05 10:37:36 -04:00
parent dc9af4d79c
commit d1f7a57f58
2 changed files with 149 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
package router
import (
"context"
"encoding/json"
"strings"
"gophergate/internal/db"
)
// HeuristicRule defines a pattern-based routing rule.
type HeuristicRule struct {
Pattern string `json:"pattern"`
TargetIdx int `json:"target"`
CaseSensitive bool `json:"case_sensitive,omitempty"`
}
func routeHeuristic(group db.ModelGroup, targets []string, userMessage string) (*Decision, error) {
selected := targets[0]
reason := "default (first target)"
// If heuristic_rules is set, use them
if group.HeuristicRules != nil && *group.HeuristicRules != "" {
var rules []HeuristicRule
if err := json.Unmarshal([]byte(*group.HeuristicRules), &rules); err == nil {
searchMsg := userMessage
for _, rule := range rules {
pattern := rule.Pattern
msg := searchMsg
if !rule.CaseSensitive {
pattern = strings.ToLower(pattern)
msg = strings.ToLower(msg)
}
if strings.Contains(msg, pattern) {
if rule.TargetIdx >= 0 && rule.TargetIdx < len(targets) {
selected = targets[rule.TargetIdx]
reason = "matched heuristic rule: " + rule.Pattern
break
}
}
}
}
}
// Built-in fallback heuristics
if reason == "default (first target)" && len(targets) > 1 {
msgLower := strings.ToLower(userMessage)
complexIndicators := []string{
"step by step", "explain in detail", "reason through",
"think carefully", "analyze", "debug", "write code",
"implement", "refactor", "architecture",
}
for _, indicator := range complexIndicators {
if strings.Contains(msgLower, indicator) {
selected = targets[len(targets)-1]
reason = "complex task indicator: " + indicator
break
}
}
}
return &Decision{
SelectedModel: selected,
Strategy: "heuristic",
Reason: reason,
}, nil
}
// routeClassifier is a stub — real implementation in classifier.go (Task 3).
// Falls back to heuristic routing for now.
func routeClassifier(ctx context.Context, classify ClassifierFunc, group db.ModelGroup, targets []string, userMessage string) (*Decision, error) {
return routeHeuristic(group, targets, userMessage)
}
+76
View File
@@ -0,0 +1,76 @@
package router
import (
"context"
"encoding/json"
"fmt"
"gophergate/internal/db"
)
// Decision holds the result of a routing decision.
type Decision struct {
SelectedModel string `json:"selected_model"`
Strategy string `json:"strategy"` // "heuristic" or "classifier"
Reason string `json:"reason"`
}
// ClassifierFunc is the callback for classifier-based routing.
type ClassifierFunc func(ctx context.Context, selectorModel, systemPrompt, userMessage string) (string, error)
// Router resolves model groups to concrete models.
type Router struct {
groups map[string]db.ModelGroup
classify ClassifierFunc
}
// New creates a Router. classify may be nil if no classifier groups exist.
func New(groups []db.ModelGroup, classify ClassifierFunc) *Router {
r := &Router{
groups: make(map[string]db.ModelGroup),
classify: classify,
}
for _, g := range groups {
r.groups[g.ID] = g
}
return r
}
// IsGroup returns true if the model name is a group ID.
func (r *Router) IsGroup(modelID string) bool {
_, ok := r.groups[modelID]
return ok
}
// Route resolves a group to a concrete model.
func (r *Router) Route(ctx context.Context, groupID string, userMessage string) (*Decision, error) {
group, ok := r.groups[groupID]
if !ok {
return nil, fmt.Errorf("unknown model group: %s", groupID)
}
var targets []string
if err := json.Unmarshal([]byte(group.Targets), &targets); err != nil || len(targets) == 0 {
return nil, fmt.Errorf("invalid or empty targets for group %s", groupID)
}
switch group.Strategy {
case "heuristic":
return routeHeuristic(group, targets, userMessage)
case "classifier":
if r.classify == nil {
return routeHeuristic(group, targets, userMessage)
}
return routeClassifier(ctx, r.classify, group, targets, userMessage)
default:
return nil, fmt.Errorf("unknown strategy: %s", group.Strategy)
}
}
// Reload replaces the group definitions without recreating the router.
func (r *Router) Reload(groups []db.ModelGroup) {
r.groups = make(map[string]db.ModelGroup)
for _, g := range groups {
r.groups[g.ID] = g
}
}