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 }