99
1010 "github.com/vllm-project/semantic-router/src/semantic-router/pkg/classification"
1111 "github.com/vllm-project/semantic-router/src/semantic-router/pkg/config"
12+ "github.com/vllm-project/semantic-router/src/semantic-router/pkg/decision"
1213 "github.com/vllm-project/semantic-router/src/semantic-router/pkg/observability/logging"
1314)
1415
@@ -169,12 +170,33 @@ type IntentOptions struct {
169170 IncludeExplanation bool `json:"include_explanation,omitempty"`
170171}
171172
173+ // MatchedSignals represents all matched signals from signal evaluation
174+ type MatchedSignals struct {
175+ Keywords []string `json:"keywords,omitempty"`
176+ Embeddings []string `json:"embeddings,omitempty"`
177+ Domains []string `json:"domains,omitempty"`
178+ FactCheck []string `json:"fact_check,omitempty"`
179+ UserFeedback []string `json:"user_feedback,omitempty"`
180+ Preferences []string `json:"preferences,omitempty"`
181+ }
182+
183+ // DecisionResult represents the result of decision evaluation
184+ type DecisionResult struct {
185+ DecisionName string `json:"decision_name"`
186+ Confidence float64 `json:"confidence"`
187+ MatchedRules []string `json:"matched_rules"`
188+ }
189+
172190// IntentResponse represents the response from intent classification
173191type IntentResponse struct {
174192 Classification Classification `json:"classification"`
175193 Probabilities map [string ]float64 `json:"probabilities,omitempty"`
176194 RecommendedModel string `json:"recommended_model,omitempty"`
177195 RoutingDecision string `json:"routing_decision,omitempty"`
196+
197+ // Signal-driven fields
198+ MatchedSignals * MatchedSignals `json:"matched_signals,omitempty"`
199+ DecisionResult * DecisionResult `json:"decision_result,omitempty"`
178200}
179201
180202// Classification represents basic classification result
@@ -184,7 +206,74 @@ type Classification struct {
184206 ProcessingTimeMs int64 `json:"processing_time_ms"`
185207}
186208
187- // ClassifyIntent performs intent classification
209+ // buildIntentResponseFromSignals builds an IntentResponse from signals and decision result
210+ func (s * ClassificationService ) buildIntentResponseFromSignals (
211+ signals * classification.SignalResults ,
212+ decisionResult * decision.DecisionResult ,
213+ category string ,
214+ confidence float64 ,
215+ processingTime int64 ,
216+ req IntentRequest ,
217+ ) * IntentResponse {
218+ response := & IntentResponse {
219+ Classification : Classification {
220+ Category : category ,
221+ Confidence : confidence ,
222+ ProcessingTimeMs : processingTime ,
223+ },
224+ }
225+
226+ // Add probabilities if requested
227+ if req .Options != nil && req .Options .ReturnProbabilities {
228+ response .Probabilities = map [string ]float64 {
229+ category : confidence ,
230+ }
231+ }
232+
233+ // Add recommended model based on category or decision
234+ if decisionResult != nil && decisionResult .Decision != nil && len (decisionResult .Decision .ModelRefs ) > 0 {
235+ modelRef := decisionResult .Decision .ModelRefs [0 ]
236+ if modelRef .LoRAName != "" {
237+ response .RecommendedModel = modelRef .LoRAName
238+ } else {
239+ response .RecommendedModel = modelRef .Model
240+ }
241+ } else if model := s .getRecommendedModel (category , confidence ); model != "" {
242+ response .RecommendedModel = model
243+ }
244+
245+ // Determine routing decision
246+ if decisionResult != nil && decisionResult .Decision != nil {
247+ response .RoutingDecision = decisionResult .Decision .Name
248+ } else {
249+ response .RoutingDecision = s .getRoutingDecision (confidence , req .Options )
250+ }
251+
252+ // Add signal information
253+ if signals != nil {
254+ response .MatchedSignals = & MatchedSignals {
255+ Keywords : signals .MatchedKeywordRules ,
256+ Embeddings : signals .MatchedEmbeddingRules ,
257+ Domains : signals .MatchedDomainRules ,
258+ FactCheck : signals .MatchedFactCheckRules ,
259+ UserFeedback : signals .MatchedUserFeedbackRules ,
260+ Preferences : signals .MatchedPreferenceRules ,
261+ }
262+ }
263+
264+ // Add decision result
265+ if decisionResult != nil && decisionResult .Decision != nil {
266+ response .DecisionResult = & DecisionResult {
267+ DecisionName : decisionResult .Decision .Name ,
268+ Confidence : decisionResult .Confidence ,
269+ MatchedRules : decisionResult .MatchedRules ,
270+ }
271+ }
272+
273+ return response
274+ }
275+
276+ // ClassifyIntent performs intent classification using signal-driven architecture
188277func (s * ClassificationService ) ClassifyIntent (req IntentRequest ) (* IntentResponse , error ) {
189278 start := time .Now ()
190279
@@ -207,38 +296,43 @@ func (s *ClassificationService) ClassifyIntent(req IntentRequest) (*IntentRespon
207296 }, nil
208297 }
209298
210- // Perform classification using the existing classifier
211- category , confidence , _ , err := s .classifier .ClassifyCategoryWithEntropy (req .Text )
212- if err != nil {
213- return nil , fmt .Errorf ("classification failed: %w" , err )
214- }
215-
216- processingTime := time .Since (start ).Milliseconds ()
299+ // Use signal-driven architecture: evaluate all signals first
300+ signals := s .classifier .EvaluateAllSignals (req .Text )
217301
218- // Build response
219- response := & IntentResponse {
220- Classification : Classification {
221- Category : category ,
222- Confidence : confidence ,
223- ProcessingTimeMs : processingTime ,
224- },
302+ // Evaluate decision with engine (if decisions are configured)
303+ // Pass pre-computed signals to avoid re-evaluation
304+ var decisionResult * decision.DecisionResult
305+ var err error
306+ if s .config != nil && len (s .config .IntelligentRouting .Decisions ) > 0 {
307+ decisionResult , err = s .classifier .EvaluateDecisionWithEngine (signals )
308+ if err != nil {
309+ // Log error but continue with classification
310+ // Note: "no decisions configured" error is expected when decisions list is empty
311+ if ! strings .Contains (err .Error (), "no decisions configured" ) {
312+ logging .Warnf ("Decision evaluation failed, continuing with classification: %v" , err )
313+ }
314+ }
225315 }
226316
227- // Add probabilities if requested
228- if req .Options != nil && req .Options .ReturnProbabilities {
229- // TODO: Implement probability extraction from classifier
230- response .Probabilities = map [string ]float64 {
231- category : confidence ,
317+ // Get category classification (for backward compatibility and when no decision matches)
318+ var category string
319+ var confidence float64
320+ if decisionResult != nil && decisionResult .Decision != nil {
321+ // Use decision name as category
322+ category = decisionResult .Decision .Name
323+ confidence = decisionResult .Confidence
324+ } else {
325+ // Fallback to traditional classification
326+ category , confidence , _ , err = s .classifier .ClassifyCategoryWithEntropy (req .Text )
327+ if err != nil {
328+ return nil , fmt .Errorf ("classification failed: %w" , err )
232329 }
233330 }
234331
235- // Add recommended model based on category
236- if model := s .getRecommendedModel (category , confidence ); model != "" {
237- response .RecommendedModel = model
238- }
332+ processingTime := time .Since (start ).Milliseconds ()
239333
240- // Determine routing decision
241- response . RoutingDecision = s .getRoutingDecision ( confidence , req . Options )
334+ // Build response from signals and decision
335+ response : = s .buildIntentResponseFromSignals ( signals , decisionResult , category , confidence , processingTime , req )
242336
243337 return response , nil
244338}
@@ -508,45 +602,8 @@ func (s *ClassificationService) ClassifyBatchUnifiedWithOptions(texts []string,
508602 return response , nil
509603}
510604
511- // ClassifyIntent with unified classifier support (backward compatibility)
512- func (s * ClassificationService ) ClassifyIntentUnified (req IntentRequest ) (* IntentResponse , error ) {
513- if s .unifiedClassifier != nil {
514- // Use unified classifier for better performance
515- results , err := s .ClassifyBatchUnified ([]string {req .Text })
516- if err != nil {
517- return nil , err
518- }
519-
520- if len (results .IntentResults ) == 0 {
521- return nil , fmt .Errorf ("no classification results" )
522- }
523-
524- // Convert unified result to legacy format
525- intentResult := results .IntentResults [0 ]
526-
527- // Build probabilities map if available
528- var probabilities map [string ]float64
529- if len (intentResult .Probabilities ) > 0 && req .Options != nil && req .Options .ReturnProbabilities {
530- probabilities = make (map [string ]float64 )
531- // For now, just include the main category probability
532- probabilities [intentResult .Category ] = float64 (intentResult .Confidence )
533- }
534-
535- return & IntentResponse {
536- Classification : Classification {
537- Category : intentResult .Category ,
538- Confidence : float64 (intentResult .Confidence ),
539- ProcessingTimeMs : results .ProcessingTimeMs ,
540- },
541- Probabilities : probabilities ,
542- RecommendedModel : s .getRecommendedModel (intentResult .Category , float64 (intentResult .Confidence )),
543- RoutingDecision : s .getRoutingDecision (float64 (intentResult .Confidence ), req .Options ),
544- }, nil
545- }
546-
547- // Fallback to legacy classifier
548- return s .ClassifyIntent (req )
549- }
605+ // NOTE: ClassifyIntentUnified removed - ClassifyIntent now always uses signal-driven architecture
606+ // For batch operations, use ClassifyBatchUnifiedWithOptions()
550607
551608// ClassifyPIIUnified performs PII detection using unified classifier
552609func (s * ClassificationService ) ClassifyPIIUnified (texts []string ) ([]classification.PIIResult , error ) {
@@ -594,6 +651,11 @@ func (s *ClassificationService) GetUnifiedClassifierStats() map[string]interface
594651 return stats
595652}
596653
654+ // GetClassifier returns the classifier instance (for signal-driven methods)
655+ func (s * ClassificationService ) GetClassifier () * classification.Classifier {
656+ return s .classifier
657+ }
658+
597659// GetConfig returns the current configuration
598660func (s * ClassificationService ) GetConfig () * config.RouterConfig {
599661 s .configMutex .RLock ()
0 commit comments