158 lines
6.2 KiB
Go
158 lines
6.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/gochat/gochat/internal/llm"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
)
|
|
|
|
// IntentService provides LLM-based intent recognition for customer messages.
|
|
// Reference: Chatwoot Captain::Llm::AssistantChatService intent classification
|
|
type IntentService struct {
|
|
llmProvider llm.Provider
|
|
}
|
|
|
|
// NewIntentService creates a new IntentService.
|
|
func NewIntentService(llmProvider llm.Provider) *IntentService {
|
|
return &IntentService{llmProvider: llmProvider}
|
|
}
|
|
|
|
// IntentType represents the classified intent of a user message.
|
|
type IntentType string
|
|
|
|
const (
|
|
IntentTypeQuestion IntentType = "question" // User asks a question
|
|
IntentTypeComplaint IntentType = "complaint" // User expresses dissatisfaction
|
|
IntentTypeRequest IntentType = "request" // User requests an action/feature
|
|
IntentTypeFeedback IntentType = "feedback" // User provides feedback
|
|
IntentTypeGreeting IntentType = "greeting" // User says hello/greetings
|
|
IntentTypeUrgent IntentType = "urgent" // User expresses urgency
|
|
IntentTypeCancellation IntentType = "cancellation" // User wants to cancel/stop
|
|
IntentTypeBilling IntentType = "billing" // User has billing/payment issue
|
|
IntentTypeTechnical IntentType = "technical" // User has technical/bug issue
|
|
IntentTypeOther IntentType = "other" // Unclassified intent
|
|
)
|
|
|
|
// IntentResult holds the classification result.
|
|
type IntentResult struct {
|
|
Intent IntentType `json:"intent"`
|
|
Confidence float64 `json:"confidence"`
|
|
SubIntents []string `json:"sub_intents,omitempty"`
|
|
SuggestedTone string `json:"suggested_tone,omitempty"` // empathetic, formal, casual
|
|
KeyTopics []string `json:"key_topics,omitempty"`
|
|
}
|
|
|
|
// ClassifyIntentRequest is the DTO for intent classification.
|
|
type ClassifyIntentRequest struct {
|
|
Message string `json:"message" validate:"required"`
|
|
Language string `json:"language,omitempty"` // optional language hint
|
|
}
|
|
|
|
// ClassifyIntent analyzes a user message and returns its intent classification.
|
|
// Uses LLM to determine the primary intent, confidence level, sub-intents,
|
|
// suggested response tone, and key topics extracted from the message.
|
|
func (s *IntentService) ClassifyIntent(ctx context.Context, req *ClassifyIntentRequest) (*IntentResult, error) {
|
|
systemPrompt := buildIntentSystemPrompt()
|
|
|
|
messages := []llm.ChatMessage{
|
|
{Role: "system", Content: systemPrompt},
|
|
{Role: "user", Content: fmt.Sprintf("Classify the intent of this message:\n%s", req.Message)},
|
|
}
|
|
|
|
llmResp, err := s.llmProvider.ChatCompletion(ctx, llm.ChatRequest{
|
|
Model: "gpt-4",
|
|
Messages: messages,
|
|
Temperature: 0.1, // Low temperature for consistent classification
|
|
MaxTokens: 256,
|
|
})
|
|
if err != nil {
|
|
applogger.L().Errorf("ClassifyIntent LLM call: %v", err)
|
|
return nil, fmt.Errorf("intent classification failed: %w", err)
|
|
}
|
|
|
|
if len(llmResp.Choices) == 0 {
|
|
return &IntentResult{Intent: IntentTypeOther, Confidence: 0.0}, nil
|
|
}
|
|
|
|
content := llmResp.Choices[0].Message.Content
|
|
var result IntentResult
|
|
if err := json.Unmarshal([]byte(extractJSON(content)), &result); err != nil {
|
|
// Fallback: try to parse intent from raw text
|
|
result.Intent = parseIntentFromText(content)
|
|
result.Confidence = 0.5
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
// buildIntentSystemPrompt constructs the system prompt for intent classification.
|
|
func buildIntentSystemPrompt() string {
|
|
return `You are an intent classifier for customer support messages. Analyze the message and classify its primary intent.
|
|
|
|
Valid intents: question, complaint, request, feedback, greeting, urgent, cancellation, billing, technical, other
|
|
|
|
For each message, return a JSON object with:
|
|
- "intent": the primary intent (must be one of the valid intents above)
|
|
- "confidence": a float between 0.0 and 1.0 indicating classification confidence
|
|
- "sub_intents": an array of secondary intents if present (optional)
|
|
- "suggested_tone": recommended response tone (empathetic, formal, casual, professional)
|
|
- "key_topics": an array of key topics/subjects mentioned in the message
|
|
|
|
Return ONLY the JSON object, no other text.`
|
|
}
|
|
|
|
// extractJSON tries to extract a JSON object from a string that may contain
|
|
// markdown formatting or other noise around the JSON.
|
|
func extractJSON(s string) string {
|
|
// Try to find JSON between ```json and ``` markers
|
|
if idx := strings.Index(s, "```json"); idx != -1 {
|
|
s = s[idx+7:]
|
|
if end := strings.Index(s, "```"); end != -1 {
|
|
return strings.TrimSpace(s[:end])
|
|
}
|
|
}
|
|
// Try to find JSON between ``` and ```
|
|
if idx := strings.Index(s, "```"); idx != -1 {
|
|
s = s[idx+3:]
|
|
if end := strings.Index(s, "```"); end != -1 {
|
|
return strings.TrimSpace(s[:end])
|
|
}
|
|
}
|
|
// Try to find a JSON object by looking for { ... }
|
|
start := strings.Index(s, "{")
|
|
end := strings.LastIndex(s, "}")
|
|
if start != -1 && end != -1 && end > start {
|
|
return s[start:end+1]
|
|
}
|
|
return s
|
|
}
|
|
|
|
// parseIntentFromText attempts to extract an intent from raw LLM text when
|
|
// JSON parsing fails.
|
|
func parseIntentFromText(text string) IntentType {
|
|
textLower := strings.ToLower(text)
|
|
intentKeywords := map[IntentType][]string{
|
|
IntentTypeQuestion: {"question", "ask", "wonder", "curious", "how", "what", "why", "when", "where"},
|
|
IntentTypeComplaint: {"complaint", "unhappy", "dissatisfied", "disappointed", "angry", "frustrated", "terrible", "awful"},
|
|
IntentTypeRequest: {"request", "please", "want", "need", "can you", "could you", "would like"},
|
|
IntentTypeFeedback: {"feedback", "suggestion", "improvement", "opinion", "review", "rating"},
|
|
IntentTypeGreeting: {"hello", "hi", "hey", "good morning", "good afternoon", "greetings"},
|
|
IntentTypeUrgent: {"urgent", "emergency", "asap", "immediately", "critical", "right now"},
|
|
IntentTypeCancellation: {"cancel", "stop", "unsubscribe", "discontinue", "terminate", "end"},
|
|
IntentTypeBilling: {"billing", "payment", "charge", "invoice", "refund", "price", "cost", "money"},
|
|
IntentTypeTechnical: {"bug", "error", "crash", "broken", "not working", "issue", "problem", "fail"},
|
|
}
|
|
|
|
for intent, keywords := range intentKeywords {
|
|
for _, kw := range keywords {
|
|
if strings.Contains(textLower, kw) {
|
|
return intent
|
|
}
|
|
}
|
|
}
|
|
return IntentTypeOther
|
|
} |