@agentman/chat-widget
Version:
Agentman Chat Widget for easy integration with web applications
649 lines (510 loc) • 16.6 kB
Markdown
# Intelligent Routing System
## Overview
The routing system determines whether MCP responses should be sent to the LLM for processing or rendered directly in the UI. This decision is critical for optimizing costs, improving response times, and providing the best user experience.
## Decision Criteria
### Core Routing Logic
```typescript
// ResponseRouter.ts
export class ResponseRouter {
private config: RoutingConfig;
private contextAnalyzer: ContextAnalyzer;
private costCalculator: CostCalculator;
constructor(config: RoutingConfig) {
this.config = config;
this.contextAnalyzer = new ContextAnalyzer();
this.costCalculator = new CostCalculator();
}
route(response: MCPResponse): RoutingDecision {
// 1. Check explicit routing rules
const explicitRoute = this.checkExplicitRules(response);
if (explicitRoute) return explicitRoute;
// 2. Analyze response characteristics
const analysis = this.analyzeResponse(response);
// 3. Consider user intent
const intent = this.contextAnalyzer.getUserIntent();
// 4. Calculate cost implications
const costAnalysis = this.costCalculator.analyze(response);
// 5. Make routing decision
return this.makeDecision(analysis, intent, costAnalysis);
}
private checkExplicitRules(response: MCPResponse): RoutingDecision | null {
// Always direct for certain UI types
if (this.config.alwaysDirect.includes(response.uiType)) {
return { action: 'direct', reason: 'Explicit rule: always direct' };
}
// Always LLM for certain types
if (this.config.alwaysLLM.includes(response.uiType)) {
return { action: 'llm', reason: 'Explicit rule: always LLM' };
}
// Pattern matching
for (const pattern of this.config.patterns) {
if (this.matchesPattern(response, pattern)) {
return { action: pattern.action, reason: `Pattern match: ${pattern.name}` };
}
return null;
}
private analyzeResponse(response: MCPResponse): ResponseAnalysis {
return {
complexity: this.calculateComplexity(response),
dataSize: this.calculateDataSize(response),
requiresExplanation: this.needsExplanation(response),
hasUserQuestion: this.hasUserQuestion(response),
isActionable: this.isActionable(response),
tokenEstimate: this.estimateTokens(response.structuredContent)
};
}
private makeDecision(
analysis: ResponseAnalysis,
intent: UserIntent,
costAnalysis: CostAnalysis
): RoutingDecision {
// Decision tree
if (analysis.requiresExplanation) {
return { action: 'hybrid', reason: 'Requires explanation with UI' };
}
if (intent.type === 'browse' && !analysis.hasUserQuestion) {
return { action: 'direct', reason: 'Simple browsing, no question' };
}
if (analysis.complexity < this.config.complexityThreshold) {
return { action: 'direct', reason: 'Low complexity response' };
}
if (costAnalysis.estimatedCost > this.config.costThreshold) {
return { action: 'direct', reason: 'Cost optimization' };
}
if (intent.type === 'analysis' || intent.type === 'comparison') {
return { action: 'llm', reason: 'Requires analysis/comparison' };
}
// Default to hybrid for balanced approach
return { action: 'hybrid', reason: 'Default: balanced approach' };
}
}
```
## Routing Rules
### 1. Always Direct to Client
These response types bypass the LLM entirely:
```typescript
const ALWAYS_DIRECT = [
// Forms - user interaction required
'form-lead',
'form-payment',
'form-survey',
'form-choices',
// Simple displays
'success',
'error',
'confirmation',
// Data visualizations
'chart',
'table',
'calendar',
// Media
'image-gallery',
'video-player',
// Simple product displays (≤10 items)
'product-card',
'product-carousel'
];
```
### 2. Always Through LLM
These require LLM processing:
```typescript
const ALWAYS_LLM = [
// Complex analysis
'data-analysis',
'comparison-result',
'recommendation',
// Natural language generation
'explanation',
'summary',
'translation',
// Decision support
'advice',
'diagnosis',
'evaluation'
];
```
### 3. Conditional Routing
Based on context and content:
```typescript
const CONDITIONAL_RULES = [
{
uiType: 'shopify-products',
condition: (response: MCPResponse) => {
const count = response.structuredContent.totalProducts;
const hasQuestion = response.structuredContent.userQuestion;
// Direct if simple browsing
if (count <= 10 && !hasQuestion) return 'direct';
// LLM if comparison needed
if (response.structuredContent.needsComparison) return 'llm';
// Hybrid for large sets with questions
return 'hybrid';
}
},
{
uiType: 'search-results',
condition: (response: MCPResponse) => {
const results = response.structuredContent.resultCount;
// Direct for clear matches
if (results === 1) return 'direct';
// LLM for ambiguous results
if (results > 20) return 'llm';
return 'hybrid';
}
}
];
```
## Implementation
### 1. Routing Configuration
```typescript
interface RoutingConfig {
// Thresholds
complexityThreshold: number; // 0-100 complexity score
costThreshold: number; // Max cost in dollars
tokenThreshold: number; // Max tokens to send to LLM
// Explicit rules
alwaysDirect: string[]; // UI types to always render directly
alwaysLLM: string[]; // UI types to always send to LLM
// Pattern matching
patterns: RoutingPattern[];
// Optimization preferences
optimization: 'cost' | 'speed' | 'quality' | 'balanced';
// Feature flags
features: {
enableHybrid: boolean; // Allow hybrid routing
enableCaching: boolean; // Cache routing decisions
enableLearning: boolean; // Learn from user feedback
};
}
interface RoutingPattern {
name: string;
match: {
uiType?: string | RegExp;
dataSize?: { min?: number; max?: number };
hasField?: string;
customMatcher?: (response: MCPResponse) => boolean;
};
action: 'direct' | 'llm' | 'hybrid';
priority: number;
}
```
### 2. Context Analyzer
```typescript
class ContextAnalyzer {
private conversationHistory: Message[] = [];
private userPreferences: UserPreferences;
getUserIntent(): UserIntent {
const lastMessage = this.getLastUserMessage();
return {
type: this.classifyIntent(lastMessage),
confidence: this.calculateConfidence(),
keywords: this.extractKeywords(lastMessage),
expectsExplanation: this.expectsExplanation(lastMessage),
isFollowUp: this.isFollowUpQuestion()
};
}
private classifyIntent(message: string): IntentType {
// Keywords for different intents
const patterns = {
browse: /show|display|list|view|see/i,
search: /find|search|look for|where/i,
compare: /compare|versus|vs|difference|better/i,
analyze: /analyze|explain|why|how|understand/i,
action: /buy|add|remove|update|change/i
};
for (const [intent, pattern] of Object.entries(patterns)) {
if (pattern.test(message)) {
return intent as IntentType;
}
}
return 'general';
}
private expectsExplanation(message: string): boolean {
const explanationKeywords = [
'why', 'how', 'explain', 'tell me about',
'what is', 'help me understand', 'clarify'
];
return explanationKeywords.some(keyword =>
message.toLowerCase().includes(keyword)
);
}
private isFollowUpQuestion(): boolean {
if (this.conversationHistory.length < 2) return false;
const lastResponse = this.conversationHistory[this.conversationHistory.length - 1];
const timeSinceLastResponse = Date.now() - lastResponse.timestamp;
// Consider it a follow-up if within 30 seconds
return timeSinceLastResponse < 30000;
}
}
```
### 3. Cost Calculator
```typescript
class CostCalculator {
private readonly COST_PER_1K_TOKENS = 0.002; // GPT-4 pricing example
analyze(response: MCPResponse): CostAnalysis {
const structuredTokens = this.estimateTokens(response.structuredContent);
const metaTokens = this.estimateTokens(response._meta);
return {
structuredContentCost: this.calculateCost(structuredTokens),
fullDataCost: this.calculateCost(metaTokens),
savings: this.calculateCost(metaTokens - structuredTokens),
estimatedTokens: structuredTokens,
recommendedRoute: this.recommendBasedOnCost(structuredTokens)
};
}
private estimateTokens(data: any): number {
// More accurate token estimation
const jsonString = JSON.stringify(data);
// Account for different token densities
let tokens = 0;
// Numbers and special characters: ~1 token per 4 chars
const numbers = jsonString.match(/\d+/g) || [];
tokens += numbers.join('').length / 4;
// Regular text: ~1 token per 4 chars
const text = jsonString.replace(/\d+/g, '');
tokens += text.length / 4;
// Add overhead for JSON structure
tokens *= 1.1;
return Math.ceil(tokens);
}
private calculateCost(tokens: number): number {
return (tokens / 1000) * this.COST_PER_1K_TOKENS;
}
private recommendBasedOnCost(tokens: number): 'direct' | 'llm' | 'hybrid' {
if (tokens < 500) return 'llm'; // Small enough, use LLM
if (tokens < 2000) return 'hybrid'; // Medium, use hybrid
return 'direct'; // Too large, go direct
}
}
```
### 4. Hybrid Routing Implementation
```typescript
class HybridRouter {
async executeHybridRoute(response: MCPResponse): Promise<void> {
// 1. Immediately render UI from _meta
const uiPromise = this.renderUI(response._meta);
// 2. Send minimal data to LLM in parallel
const llmPromise = this.getLLMNarrative(response.structuredContent);
// 3. Wait for UI to be ready first (better UX)
await uiPromise;
// 4. Add LLM narrative when ready
llmPromise.then(narrative => {
this.addNarrative(narrative);
}).catch(error => {
// Silent fail - UI is already rendered
console.error('LLM narrative failed:', error);
});
}
private async renderUI(metaData: any): Promise<void> {
const component = this.componentRegistry.create(metaData.uiType, metaData);
const element = component.render();
this.chatWidget.addComponent(element);
}
private async getLLMNarrative(structuredContent: any): Promise<string> {
const prompt = this.buildNarrativePrompt(structuredContent);
const response = await this.llmClient.complete(prompt);
return response.text;
}
private buildNarrativePrompt(content: any): string {
return `
Based on the following search results, provide a brief, helpful summary:
${JSON.stringify(content, null, 2)}
Keep your response concise (2-3 sentences) and focus on key insights.
`;
}
}
```
## Routing Decision Examples
### Example 1: Simple Product Browse
```typescript
// User: "Show me blue shirts"
const response: MCPResponse = {
uiType: 'shopify-products',
structuredContent: {
totalProducts: 8,
query: 'blue shirts',
userQuestion: null
},
_meta: { /* 8 products */ }
};
// Decision: DIRECT
// Reason: Simple browse, low count, no question
```
### Example 2: Complex Comparison
```typescript
// User: "Compare the features of these three laptops and recommend the best for programming"
const response: MCPResponse = {
uiType: 'product-comparison',
structuredContent: {
products: [/* 3 laptops */],
userQuestion: 'recommend best for programming',
needsAnalysis: true
},
_meta: { /* detailed specs */ }
};
// Decision: LLM
// Reason: Requires analysis and recommendation
```
### Example 3: Large Dataset with Question
```typescript
// User: "Why are these shoes popular?"
const response: MCPResponse = {
uiType: 'shopify-products',
structuredContent: {
totalProducts: 47,
query: 'popular shoes',
userQuestion: 'why are these popular'
},
_meta: { /* 47 products */ }
};
// Decision: HYBRID
// Reason: Large dataset + explanation needed
```
## Performance Optimization
### 1. Decision Caching
```typescript
class RoutingCache {
private cache: Map<string, RoutingDecision> = new Map();
private ttl: number = 300000; // 5 minutes
getCachedDecision(response: MCPResponse): RoutingDecision | null {
const key = this.generateCacheKey(response);
const cached = this.cache.get(key);
if (cached && this.isValid(cached)) {
return cached;
}
return null;
}
private generateCacheKey(response: MCPResponse): string {
return `${response.uiType}-${response.structuredContent.totalProducts || 0}-${response.structuredContent.query || ''}`;
}
}
```
### 2. Learning from Feedback
```typescript
class RoutingLearner {
private feedback: Map<string, FeedbackData> = new Map();
recordFeedback(decision: RoutingDecision, feedback: UserFeedback): void {
const key = `${decision.action}-${decision.reason}`;
if (!this.feedback.has(key)) {
this.feedback.set(key, {
positive: 0,
negative: 0,
total: 0
});
}
const data = this.feedback.get(key)!;
data.total++;
if (feedback.satisfied) {
data.positive++;
} else {
data.negative++;
}
// Adjust routing rules based on feedback
if (data.total > 100) {
this.adjustRules(key, data);
}
}
private adjustRules(key: string, data: FeedbackData): void {
const successRate = data.positive / data.total;
if (successRate < 0.7) {
// Poor performance, adjust routing
console.log(`Adjusting routing for ${key}, success rate: ${successRate}`);
// Implement rule adjustment logic
}
}
}
```
## Configuration Examples
### Cost-Optimized Configuration
```typescript
const costOptimizedConfig: RoutingConfig = {
complexityThreshold: 30,
costThreshold: 0.01,
tokenThreshold: 1000,
alwaysDirect: [
'form-', 'chart', 'table', 'success', 'error',
'product-card', 'product-carousel'
],
alwaysLLM: ['analysis', 'recommendation'],
patterns: [
{
name: 'large-product-sets',
match: {
uiType: 'shopify-products',
dataSize: { min: 20 }
},
action: 'direct',
priority: 1
}
],
optimization: 'cost',
features: {
enableHybrid: true,
enableCaching: true,
enableLearning: true
}
};
```
### Quality-Optimized Configuration
```typescript
const qualityOptimizedConfig: RoutingConfig = {
complexityThreshold: 70,
costThreshold: 0.10,
tokenThreshold: 5000,
alwaysDirect: ['form-', 'error'],
alwaysLLM: [
'analysis', 'recommendation', 'comparison',
'shopify-products', 'search-results'
],
patterns: [],
optimization: 'quality',
features: {
enableHybrid: true,
enableCaching: false,
enableLearning: true
}
};
```
## Testing Routing Logic
```typescript
describe('ResponseRouter', () => {
let router: ResponseRouter;
beforeEach(() => {
router = new ResponseRouter(costOptimizedConfig);
});
test('should route forms directly', () => {
const response: MCPResponse = {
uiType: 'form-lead',
structuredContent: {},
_meta: {}
};
const decision = router.route(response);
expect(decision.action).toBe('direct');
});
test('should route large product sets based on config', () => {
const response: MCPResponse = {
uiType: 'shopify-products',
structuredContent: { totalProducts: 50 },
_meta: {}
};
const decision = router.route(response);
expect(decision.action).toBe('direct'); // Based on cost-optimized config
});
test('should use hybrid for medium complexity with questions', () => {
const response: MCPResponse = {
uiType: 'search-results',
structuredContent: {
resultCount: 15,
userQuestion: 'Which is best?'
},
_meta: {}
};
const decision = router.route(response);
expect(decision.action).toBe('hybrid');
});
});
```
## Next Steps
1. See `ARCHITECTURE.md` for system overview
2. Check `MCP_RESPONSE_STRUCTURE.md` for response formats
3. Review `CHAT_WIDGET_IMPLEMENTATION.md` for integration
4. Read `PERFORMANCE.md` for optimization strategies