Files
gochat/internal/service/intent_service.go
T
2026-06-04 15:44:48 +08:00

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
}