Skip to content

Customer Support Router

This example builds a complete customer support triage system that classifies incoming tickets by intent and urgency, then routes them to appropriate handlers with escalation paths.

┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Validate │────▶│ Classify │────▶│ Sentiment │
│ Input │ │ Intent │ │ Analysis │
└─────────────┘ └─────────────┘ └──────┬──────┘
┌──────────────────────────┼──────────────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Billing │ │ Technical │ │ General │
│ Handler │ │ Handler │ │ Handler │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Priority │ │ Priority │ │ Format │
│ Router │ │ Router │ │ Response │
└─────────────┘ └─────────────┘ └─────────────┘
  • Help desk automation: Automatically categorize and route support tickets
  • Email triage: Sort incoming emails to appropriate departments
  • Chat routing: Direct customer chat sessions to specialized agents
  • Escalation management: Identify urgent issues requiring immediate attention
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/petal-labs/iris/providers/openai"
"github.com/petal-labs/petalflow"
"github.com/petal-labs/petalflow/irisadapter"
)
func main() {
// Initialize Iris provider
provider := openai.New(os.Getenv("OPENAI_API_KEY"))
client := irisadapter.NewProviderAdapter(provider)
// Build and run the support workflow
graph := buildSupportGraph(client)
runSupportWorkflow(graph)
}
func buildSupportGraph(client *irisadapter.ProviderAdapter) petalflow.Graph {
g := petalflow.NewGraph("support-triage")
// Stage 1: Input Validation
validateNode := petalflow.NewGuardianNode("validate_input", petalflow.GuardianNodeConfig{
Checks: []petalflow.GuardCheck{
{Var: "ticket_id", Op: petalflow.OpNotEmpty, Message: "Ticket ID is required"},
{Var: "message", Op: petalflow.OpNotEmpty, Message: "Message content is required"},
{Var: "message", Op: petalflow.OpMaxLength, Value: 50000, Message: "Message exceeds maximum length"},
{Var: "customer_email", Op: petalflow.OpMatches, Value: `^.+@.+\..+$`, Message: "Invalid email format"},
},
OnFail: petalflow.GuardActionRoute,
FailRoute: "validation_error",
ErrorKey: "validation_errors",
})
// Stage 2: Intent Classification (LLM-based)
classifyNode := petalflow.NewLLMRouter("classify_intent", client, petalflow.LLMRouterConfig{
Model: "gpt-4o-mini",
InputKey: "message",
Categories: []petalflow.Category{
{
Name: "billing",
Description: "Billing, payments, invoices, charges, refunds, subscription changes, pricing questions",
Examples: []string{
"I was charged twice this month",
"How do I update my credit card?",
"I want to cancel my subscription",
},
},
{
Name: "technical",
Description: "Technical issues, bugs, errors, API problems, integration help, feature questions",
Examples: []string{
"The API returns a 500 error",
"I can't log into my account",
"How do I integrate with webhooks?",
},
},
{
Name: "general",
Description: "General inquiries, feedback, partnership requests, other topics",
Examples: []string{
"What are your business hours?",
"I'd like to provide feedback",
"Do you offer enterprise plans?",
},
},
},
})
// Stage 3: Sentiment Analysis
sentimentNode := petalflow.NewLLMNode("analyze_sentiment", client, petalflow.LLMNodeConfig{
Model: "gpt-4o-mini",
SystemPrompt: `Analyze customer sentiment. Respond with JSON:
{"sentiment": "positive|neutral|negative|angry", "urgency": "low|medium|high", "escalate": true|false}
Set escalate=true if: customer mentions legal action, competitor switch, social media, or uses profanity.`,
PromptTemplate: "Customer message: {{.Vars.message}}",
OutputKey: "sentiment_analysis",
ResponseFormat: petalflow.ResponseFormatJSON,
})
// Billing Handler
billingHandler := petalflow.NewLLMNode("billing_handler", client, petalflow.LLMNodeConfig{
Model: "gpt-4o",
SystemPrompt: `You are a billing support specialist. Help with:
- Payment issues and refunds
- Subscription management
- Invoice questions
- Pricing clarification
Be empathetic and solution-oriented. If you can't resolve, explain next steps clearly.`,
PromptTemplate: `Customer: {{.Vars.customer_email}}
Ticket: {{.Vars.ticket_id}}
Issue: {{.Vars.message}}
{{if .Vars.customer_history}}Previous interactions: {{.Vars.customer_history}}{{end}}
Provide a helpful response addressing their billing concern.`,
OutputKey: "draft_response",
Temperature: 0.3,
})
// Technical Handler
technicalHandler := petalflow.NewLLMNode("technical_handler", client, petalflow.LLMNodeConfig{
Model: "gpt-4o",
SystemPrompt: `You are a technical support engineer. Help with:
- Bug reports and error troubleshooting
- API integration questions
- Feature usage guidance
- Performance issues
Be precise and technical. Include code examples when helpful.`,
PromptTemplate: `Customer: {{.Vars.customer_email}}
Ticket: {{.Vars.ticket_id}}
Issue: {{.Vars.message}}
{{if .Vars.system_status}}Current system status: {{.Vars.system_status}}{{end}}
Provide technical guidance to resolve their issue.`,
OutputKey: "draft_response",
Temperature: 0.2,
})
// General Handler
generalHandler := petalflow.NewLLMNode("general_handler", client, petalflow.LLMNodeConfig{
Model: "gpt-4o",
SystemPrompt: `You are a customer success representative. Help with:
- General questions about the product
- Feedback acknowledgment
- Partnership inquiries
- Anything not billing or technical
Be friendly and helpful. Connect customers to the right resources.`,
PromptTemplate: `Customer: {{.Vars.customer_email}}
Ticket: {{.Vars.ticket_id}}
Message: {{.Vars.message}}
Provide a helpful, friendly response.`,
OutputKey: "draft_response",
Temperature: 0.5,
})
// Priority Router (after each handler)
priorityRouter := petalflow.NewRuleRouter("priority_router", petalflow.RuleRouterConfig{
Routes: []petalflow.RouteRule{
// Escalate angry customers or those flagged for escalation
{
When: petalflow.RouteCondition{
Or: []petalflow.RouteCondition{
{Var: "sentiment_analysis.escalate", Op: petalflow.OpEquals, Value: true},
{Var: "sentiment_analysis.sentiment", Op: petalflow.OpEquals, Value: "angry"},
},
},
To: "escalate_to_human",
},
// High urgency goes to priority queue
{
When: petalflow.RouteCondition{
Var: "sentiment_analysis.urgency",
Op: petalflow.OpEquals,
Value: "high",
},
To: "priority_queue",
},
// VIP customers get priority
{
When: petalflow.RouteCondition{
Var: "customer_tier",
Op: petalflow.OpIn,
Value: []string{"enterprise", "premium"},
},
To: "priority_queue",
},
},
Default: "standard_queue",
})
// Escalation Handler
escalateNode := petalflow.NewTransformNode("escalate_to_human", petalflow.TransformNodeConfig{
Transform: func(inputs map[string]any) (any, error) {
return map[string]any{
"action": "escalate",
"queue": "human_review",
"reason": "Customer sentiment requires human attention",
"priority": "high",
"draft": inputs["draft_response"],
"sentiment": inputs["sentiment_analysis"],
}, nil
},
InputKeys: []string{"draft_response", "sentiment_analysis"},
OutputKey: "routing_decision",
})
// Priority Queue Handler
priorityQueueNode := petalflow.NewTransformNode("priority_queue", petalflow.TransformNodeConfig{
Transform: func(inputs map[string]any) (any, error) {
return map[string]any{
"action": "send",
"queue": "priority",
"response": inputs["draft_response"],
"auto_send": true,
}, nil
},
InputKeys: []string{"draft_response"},
OutputKey: "routing_decision",
})
// Standard Queue Handler
standardQueueNode := petalflow.NewTransformNode("standard_queue", petalflow.TransformNodeConfig{
Transform: func(inputs map[string]any) (any, error) {
return map[string]any{
"action": "send",
"queue": "standard",
"response": inputs["draft_response"],
"auto_send": true,
}, nil
},
InputKeys: []string{"draft_response"},
OutputKey: "routing_decision",
})
// Validation Error Handler
validationErrorNode := petalflow.NewTransformNode("validation_error", petalflow.TransformNodeConfig{
Transform: func(inputs map[string]any) (any, error) {
errors := inputs["validation_errors"].([]string)
return map[string]any{
"action": "reject",
"errors": errors,
"message": "Please provide valid ticket information",
}, nil
},
InputKeys: []string{"validation_errors"},
OutputKey: "routing_decision",
})
// Format Final Response
formatNode := petalflow.NewTransformNode("format_response", petalflow.TransformNodeConfig{
Transform: func(inputs map[string]any) (any, error) {
decision := inputs["routing_decision"].(map[string]any)
return map[string]any{
"ticket_id": inputs["ticket_id"],
"status": "processed",
"routing": decision,
"processed_at": time.Now().UTC(),
}, nil
},
InputKeys: []string{"routing_decision", "ticket_id"},
OutputKey: "final_result",
})
// Add all nodes
g.AddNode(validateNode)
g.AddNode(classifyNode)
g.AddNode(sentimentNode)
g.AddNode(billingHandler)
g.AddNode(technicalHandler)
g.AddNode(generalHandler)
g.AddNode(priorityRouter)
g.AddNode(escalateNode)
g.AddNode(priorityQueueNode)
g.AddNode(standardQueueNode)
g.AddNode(validationErrorNode)
g.AddNode(formatNode)
// Define edges
g.AddEdge("validate_input", "classify_intent")
g.AddEdge("validate_input", "validation_error") // On validation failure
g.AddEdge("classify_intent", "analyze_sentiment")
// Route from sentiment to appropriate handler based on classification
g.AddEdge("analyze_sentiment", "billing_handler")
g.AddEdge("analyze_sentiment", "technical_handler")
g.AddEdge("analyze_sentiment", "general_handler")
// All handlers go through priority routing
g.AddEdge("billing_handler", "priority_router")
g.AddEdge("technical_handler", "priority_router")
g.AddEdge("general_handler", "priority_router")
// Priority router outcomes
g.AddEdge("priority_router", "escalate_to_human")
g.AddEdge("priority_router", "priority_queue")
g.AddEdge("priority_router", "standard_queue")
// All paths lead to format
g.AddEdge("escalate_to_human", "format_response")
g.AddEdge("priority_queue", "format_response")
g.AddEdge("standard_queue", "format_response")
g.AddEdge("validation_error", "format_response")
g.SetEntry("validate_input")
return g
}
func runSupportWorkflow(graph petalflow.Graph) {
runtime := petalflow.NewRuntime()
// Create envelope with ticket data
env := petalflow.NewEnvelope()
env.SetVar("ticket_id", "TKT-2024-001234")
env.SetVar("customer_email", "jane.doe@example.com")
env.SetVar("customer_tier", "premium")
env.SetVar("message", `I've been charged twice for my subscription this month!
This is the third time this has happened and I'm extremely frustrated.
If this isn't resolved immediately, I'm switching to your competitor.`)
// Event handler for observability
handler := func(event petalflow.Event) {
switch event.Kind {
case petalflow.EventNodeEnd:
log.Printf("%s completed in %v", event.NodeID, event.Duration)
case petalflow.EventRouteDecision:
log.Printf("→ Routed to: %s", event.Data["target"])
case petalflow.EventNodeError:
log.Printf("%s failed: %v", event.NodeID, event.Error)
}
}
// Execute
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
result, err := runtime.Run(ctx, graph, env, petalflow.RunOptions{
EventHandler: handler,
})
if err != nil {
log.Fatalf("Workflow failed: %v", err)
}
// Output result
finalResult := result.GetVar("final_result").(map[string]any)
fmt.Printf("\n=== Ticket Processed ===\n")
fmt.Printf("Ticket ID: %s\n", finalResult["ticket_id"])
fmt.Printf("Status: %s\n", finalResult["status"])
fmt.Printf("Routing: %+v\n", finalResult["routing"])
}
✓ validate_input completed in 1ms
→ Routed to: billing
✓ classify_intent completed in 245ms
✓ analyze_sentiment completed in 312ms
✓ billing_handler completed in 1.2s
→ Routed to: escalate_to_human
✓ priority_router completed in 1ms
✓ escalate_to_human completed in 1ms
✓ format_response completed in 1ms
=== Ticket Processed ===
Ticket ID: TKT-2024-001234
Status: processed
Routing: map[action:escalate queue:human_review reason:Customer sentiment requires human attention priority:high]
// Add SLA metadata based on customer tier
slaNode := petalflow.NewTransformNode("set_sla", petalflow.TransformNodeConfig{
Transform: func(inputs map[string]any) (any, error) {
tier := inputs["customer_tier"].(string)
slaHours := map[string]int{
"enterprise": 1,
"premium": 4,
"standard": 24,
"free": 72,
}
return map[string]any{
"sla_hours": slaHours[tier],
"sla_deadline": time.Now().Add(time.Duration(slaHours[tier]) * time.Hour),
}, nil
},
})
// Detect language and translate if needed
languageNode := petalflow.NewLLMNode("detect_language", client, petalflow.LLMNodeConfig{
Model: "gpt-4o-mini",
PromptTemplate: `Detect the language of this text and respond with JSON:
{"language": "en|es|fr|de|...", "confidence": 0.0-1.0}
Text: {{.Vars.message}}`,
OutputKey: "language_detection",
ResponseFormat: petalflow.ResponseFormatJSON,
})
translateRouter := petalflow.NewRuleRouter("translate_check", petalflow.RuleRouterConfig{
Routes: []petalflow.RouteRule{
{When: petalflow.RouteCondition{Var: "language_detection.language", Op: petalflow.OpNotEquals, Value: "en"}, To: "translate"},
},
Default: "classify_intent",
})