gomcp/internal/domain/router/router.go

203 lines
5.4 KiB
Go

// Package router implements the Neuroplastic Router (DIP H2.2).
//
// The router matches new intents against known patterns stored in the
// Vector Store. It determines the optimal processing path based on
// cosine similarity to previously seen intents.
//
// Routing decisions:
// - High confidence (≥ 0.85): auto-route to matched pattern's action
// - Medium confidence (0.5-0.85): flag for review
// - Low confidence (< 0.5): unknown intent, default-deny
//
// The router learns from every interaction, building a neuroplastic
// map of intent space over time.
package router
import (
"context"
"fmt"
"time"
"github.com/syntrex-lab/gomcp/internal/domain/vectorstore"
)
// Decision represents a routing decision.
type Decision int
const (
DecisionRoute Decision = iota // Auto-route to known action
DecisionReview // Needs human review
DecisionDeny // Unknown intent, default deny
DecisionLearn // New pattern, store and route
)
// String returns the decision name.
func (d Decision) String() string {
switch d {
case DecisionRoute:
return "ROUTE"
case DecisionReview:
return "REVIEW"
case DecisionDeny:
return "DENY"
case DecisionLearn:
return "LEARN"
default:
return "UNKNOWN"
}
}
// Config configures the router.
type Config struct {
// HighConfidence: similarity threshold for auto-routing.
HighConfidence float64 // default: 0.85
// LowConfidence: below this, deny.
LowConfidence float64 // default: 0.50
// AutoLearn: if true, store new intents automatically.
AutoLearn bool // default: true
// MaxSearchResults: how many similar intents to consider.
MaxSearchResults int // default: 3
}
// DefaultConfig returns sensible defaults.
func DefaultConfig() Config {
return Config{
HighConfidence: 0.85,
LowConfidence: 0.50,
AutoLearn: true,
MaxSearchResults: 3,
}
}
// RouteResult holds the routing result.
type RouteResult struct {
Decision string `json:"decision"`
Route string `json:"route,omitempty"`
Confidence float64 `json:"confidence"`
Reason string `json:"reason"`
MatchedID string `json:"matched_id,omitempty"`
Alternatives []vectorstore.SearchResult `json:"alternatives,omitempty"`
LearnedID string `json:"learned_id,omitempty"` // if auto-learned
DurationUs int64 `json:"duration_us"`
}
// Router performs neuroplastic intent routing.
type Router struct {
cfg Config
store *vectorstore.Store
}
// New creates a new neuroplastic router.
func New(store *vectorstore.Store, cfg *Config) *Router {
c := DefaultConfig()
if cfg != nil {
if cfg.HighConfidence > 0 {
c.HighConfidence = cfg.HighConfidence
}
if cfg.LowConfidence > 0 {
c.LowConfidence = cfg.LowConfidence
}
if cfg.MaxSearchResults > 0 {
c.MaxSearchResults = cfg.MaxSearchResults
}
c.AutoLearn = cfg.AutoLearn
}
return &Router{cfg: c, store: store}
}
// Route determines the processing path for an intent vector.
func (r *Router) Route(_ context.Context, text string, vector []float64, verdict string) *RouteResult {
start := time.Now()
result := &RouteResult{}
// Search for similar known intents.
matches := r.store.Search(vector, r.cfg.MaxSearchResults)
if len(matches) == 0 {
// No known patterns — first intent ever.
if r.cfg.AutoLearn {
id := r.store.Add(&vectorstore.IntentRecord{
Text: text,
Vector: vector,
Route: "unknown",
Verdict: verdict,
})
result.Decision = DecisionLearn.String()
result.Route = "unknown"
result.Confidence = 0
result.Reason = "first intent in store, learned as new pattern"
result.LearnedID = id
} else {
result.Decision = DecisionDeny.String()
result.Confidence = 0
result.Reason = "no known patterns"
}
result.DurationUs = time.Since(start).Microseconds()
return result
}
best := matches[0]
result.Confidence = best.Similarity
result.MatchedID = best.Record.ID
if len(matches) > 1 {
result.Alternatives = matches[1:]
}
switch {
case best.Similarity >= r.cfg.HighConfidence:
// High confidence: auto-route.
result.Decision = DecisionRoute.String()
result.Route = best.Record.Route
result.Reason = fmt.Sprintf(
"matched known pattern %q (sim=%.3f)",
best.Record.Route, best.Similarity)
case best.Similarity >= r.cfg.LowConfidence:
// Medium confidence: review.
result.Decision = DecisionReview.String()
result.Route = best.Record.Route
result.Reason = fmt.Sprintf(
"partial match to %q (sim=%.3f), needs review",
best.Record.Route, best.Similarity)
// Auto-learn new pattern in review zone.
if r.cfg.AutoLearn {
id := r.store.Add(&vectorstore.IntentRecord{
Text: text,
Vector: vector,
Route: best.Record.Route + "/pending",
Verdict: verdict,
})
result.LearnedID = id
}
default:
// Low confidence: deny.
result.Decision = DecisionDeny.String()
result.Reason = fmt.Sprintf(
"no confident match (best sim=%.3f < threshold %.3f)",
best.Similarity, r.cfg.LowConfidence)
if r.cfg.AutoLearn {
id := r.store.Add(&vectorstore.IntentRecord{
Text: text,
Vector: vector,
Route: "denied",
Verdict: verdict,
})
result.LearnedID = id
}
}
result.DurationUs = time.Since(start).Microseconds()
return result
}
// GetStore returns the underlying vector store.
func (r *Router) GetStore() *vectorstore.Store {
return r.store
}