Skip to content

Commit ec428bc

Browse files
GOavi101Avishek Goswami
andauthored
[Feat] align Classification API server with signal-driven architecture (#1032)
* [Feat] align Classification API server with signal-driven architecture - Refactor ClassifyIntent() to use signal-driven methods (EvaluateAllSignals, EvaluateDecisionWithEngine) - Add MatchedSignals and DecisionResult to IntentResponse for richer API responses - Update ClassifyIntentUnified() to include signal information from legacy classifier - Ensure both ext-proc and API servers use identical classification logic Changes: - Added MatchedSignals and DecisionResult types to services package - Updated IntentResponse with optional signal-driven fields - Refactored ClassifyIntent to use signal evaluation + decision engine - Updated ClassifyIntentUnified to include signals when available - Fixed config access to use IntelligentRouting.Decisions consistently - Added helper method buildIntentResponseFromSignals() Signed-off-by: Avishek Goswami <[email protected]> * [Feat] Remove legacy ClassifyIntentUnified and simplify API handler - Remove ClassifyIntentUnified() method (101 lines of legacy code) - Simplify handleIntentClassification() to always use signal-driven ClassifyIntent() - Remove related test cases for ClassifyIntentUnified - All intent classification now consistently uses signal-driven architecture - Unified classifier still available for batch operations (separate use case) Signed-off-by: Avishek Goswami <[email protected]> --------- Signed-off-by: Avishek Goswami <[email protected]> Co-authored-by: Avishek Goswami <[email protected]>
1 parent b2e79c4 commit ec428bc

File tree

3 files changed

+129
-115
lines changed

3 files changed

+129
-115
lines changed

src/semantic-router/pkg/apiserver/route_classify.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,8 @@ func (s *ClassificationAPIServer) handleIntentClassification(w http.ResponseWrit
2222
return
2323
}
2424

25-
// Use unified classifier if available, otherwise fall back to legacy
26-
var response *services.IntentResponse
27-
var err error
28-
29-
if s.classificationSvc.HasUnifiedClassifier() {
30-
response, err = s.classificationSvc.ClassifyIntentUnified(req)
31-
} else {
32-
response, err = s.classificationSvc.ClassifyIntent(req)
33-
}
34-
25+
// Use signal-driven classification (always uses signal-driven architecture)
26+
response, err := s.classificationSvc.ClassifyIntent(req)
3527
if err != nil {
3628
s.writeErrorResponse(w, http.StatusInternalServerError, "CLASSIFICATION_ERROR", err.Error())
3729
return

src/semantic-router/pkg/services/classification.go

Lines changed: 127 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
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
173191
type 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
188277
func (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
552609
func (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
598660
func (s *ClassificationService) GetConfig() *config.RouterConfig {
599661
s.configMutex.RLock()

src/semantic-router/pkg/services/classification_test.go

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -183,46 +183,6 @@ func TestClassificationService_ClassifySecurityUnified_ErrorCases(t *testing.T)
183183
})
184184
}
185185

186-
func TestClassificationService_ClassifyIntentUnified_ErrorCases(t *testing.T) {
187-
t.Run("Unified_classifier_not_available_fallback", func(t *testing.T) {
188-
// This should fallback to the legacy ClassifyIntent method
189-
service := &ClassificationService{
190-
unifiedClassifier: nil,
191-
classifier: nil, // This will return placeholder response, not error
192-
}
193-
194-
req := IntentRequest{Text: "test"}
195-
result, err := service.ClassifyIntentUnified(req)
196-
if err != nil {
197-
t.Errorf("Unexpected error: %v", err)
198-
}
199-
if result == nil {
200-
t.Error("Expected non-nil result")
201-
}
202-
// Should get placeholder response from legacy classifier
203-
if result.Classification.Category != "general" {
204-
t.Errorf("Expected placeholder category 'general', got '%s'", result.Classification.Category)
205-
}
206-
if result.RoutingDecision != "placeholder_response" {
207-
t.Errorf("Expected placeholder routing decision, got '%s'", result.RoutingDecision)
208-
}
209-
})
210-
211-
t.Run("Classifier_not_initialized", func(t *testing.T) {
212-
classifier := &classification.UnifiedClassifier{}
213-
service := &ClassificationService{
214-
unifiedClassifier: classifier,
215-
}
216-
217-
req := IntentRequest{Text: "test"}
218-
_, err := service.ClassifyIntentUnified(req)
219-
if err == nil {
220-
t.Error("Expected error for uninitialized classifier")
221-
}
222-
// The actual error will come from the unified classifier
223-
})
224-
}
225-
226186
// Test data structures and basic functionality
227187
func TestClassificationService_BasicFunctionality(t *testing.T) {
228188
t.Run("Service_creation", func(t *testing.T) {

0 commit comments

Comments
 (0)