mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-24 20:06:21 +02:00
203 lines
5.4 KiB
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
|
|
}
|