package service import ( "context" "fmt" "strings" "github.com/gochat/gochat/internal/llm" "github.com/gochat/gochat/internal/repository" applogger "github.com/gochat/gochat/pkg/logger" ) // ConversationInsightService provides AI-powered conversation analysis features: // - Participant analysis (role, sentiment, engagement) // - Action items extraction (tasks, deadlines, owners) // - Label/priority suggestion (auto-tagging conversations) // // Reference: Chatwoot Captain::Llm::ConversationInsightService + M12 PRD // --- Request/Response DTOs --- // ParticipantAnalysisRequest is the input for analyzing conversation participants. type ParticipantAnalysisRequest struct { ConversationID uint `json:"conversation_id" validate:"required"` } // ParticipantInfo holds analysis results for a single participant. type ParticipantInfo struct { Name string `json:"name"` Role string `json:"role"` // "customer", "agent", "manager" Sentiment string `json:"sentiment"` // "positive", "neutral", "negative" Topics []string `json:"topics"` // main topics discussed Engagement float64 `json:"engagement"` // engagement score 0-1 } // ParticipantAnalysisResult holds the participant analysis result. type ParticipantAnalysisResult struct { Participants []ParticipantInfo `json:"participants"` Summary string `json:"summary"` // brief overall participant dynamics summary } // ActionItemsRequest is the input for extracting action items from a conversation. type ActionItemsRequest struct { ConversationID uint `json:"conversation_id" validate:"required"` } // ActionItem holds a single action item extracted from a conversation. type ActionItem struct { Description string `json:"description"` Owner string `json:"owner,omitempty"` // person responsible Deadline string `json:"deadline,omitempty"` Priority string `json:"priority"` // "high", "medium", "low" Status string `json:"status"` // "pending", "in_progress", "completed" } // ActionItemsResult holds the extracted action items. type ActionItemsResult struct { Items []ActionItem `json:"items"` } // LabelSuggestionRequest is the input for suggesting labels/priority for a conversation. type LabelSuggestionRequest struct { ConversationID uint `json:"conversation_id" validate:"required"` AssistantID uint `json:"assistant_id,omitempty"` // optional: use assistant guidelines } // InsightLabelSuggestionResult holds suggested labels and priority. type InsightLabelSuggestionResult struct { Labels []string `json:"labels"` Priority string `json:"priority"` // "urgent", "high", "medium", "low" Reason string `json:"reason"` // brief explanation of the suggestion } // ConversationInsightService orchestrates AI-powered conversation analysis. type ConversationInsightService struct { conversationRepo *repository.ConversationRepo messageRepo *repository.MessageRepo assistantRepo *repository.CaptainAssistantRepo llmProvider llm.Provider promptBuilder *SystemPromptBuilder } // NewConversationInsightService creates a new ConversationInsightService. func NewConversationInsightService( conversationRepo *repository.ConversationRepo, messageRepo *repository.MessageRepo, assistantRepo *repository.CaptainAssistantRepo, llmProvider llm.Provider, ) *ConversationInsightService { return &ConversationInsightService{ conversationRepo: conversationRepo, messageRepo: messageRepo, assistantRepo: assistantRepo, llmProvider: llmProvider, promptBuilder: NewSystemPromptBuilder(), } } // --- Participant Analysis --- // Reference: M12 PRD §Captain AI — Participant Analysis // AnalyzeParticipants analyzes the participants in a conversation. func (s *ConversationInsightService) AnalyzeParticipants(ctx context.Context, accountID uint, req *ParticipantAnalysisRequest) (*ParticipantAnalysisResult, error) { // Fetch conversation messages contextStr, err := s.fetchConversationContext(ctx, req.ConversationID) if err != nil { return nil, fmt.Errorf("fetch conversation context: %w", err) } if contextStr == "" { return nil, fmt.Errorf("no messages found for conversation %d", req.ConversationID) } systemPrompt := `You are a conversation analysis AI. Analyze the participants in the following conversation. For each participant, identify: 1. Their role (customer, agent, manager, etc.) 2. Their overall sentiment (positive, neutral, negative) 3. The main topics they discussed (2-3 keywords) 4. Their engagement level (0-1 score based on message frequency and depth) Also provide a brief summary of the overall participant dynamics. Return your analysis as JSON in this exact format: { "participants": [ {"name": "...", "role": "...", "sentiment": "...", "topics": ["..."], "engagement": 0.0} ], "summary": "..." }` llmResp, err := s.llmProvider.ChatCompletion(ctx, llm.ChatRequest{ Model: "gpt-4", Messages: []llm.ChatMessage{ {Role: "system", Content: systemPrompt}, {Role: "user", Content: contextStr}, }, Temperature: 0.3, MaxTokens: 1024, }) if err != nil { applogger.L().Errorf("AnalyzeParticipants LLM: %v", err) return nil, fmt.Errorf("participant analysis failed: %w", err) } if len(llmResp.Choices) == 0 { return nil, fmt.Errorf("no LLM response") } content := llmResp.Choices[0].Message.Content // Try to parse JSON, fallback to text extraction var result ParticipantAnalysisResult if err := parseJSONResponse(content, &result); err != nil { // Fallback: extract structured data from plain text result = extractParticipantInfoFromText(content) } return &result, nil } // --- Action Items Extraction --- // Reference: M12 PRD §Captain AI — Action Items // ExtractActionItems extracts action items from a conversation. func (s *ConversationInsightService) ExtractActionItems(ctx context.Context, accountID uint, req *ActionItemsRequest) (*ActionItemsResult, error) { contextStr, err := s.fetchConversationContext(ctx, req.ConversationID) if err != nil { return nil, fmt.Errorf("fetch conversation context: %w", err) } if contextStr == "" { return nil, fmt.Errorf("no messages found for conversation %d", req.ConversationID) } systemPrompt := `You are an action item extraction AI. Analyze the conversation and extract all actionable items. For each action item, identify: 1. Description: what needs to be done 2. Owner: who is responsible (if mentioned) 3. Deadline: any mentioned deadline 4. Priority: high, medium, or low 5. Status: pending, in_progress, or completed (if mentioned) Return as JSON array: { "items": [ {"description": "...", "owner": "...", "deadline": "...", "priority": "...", "status": "..."} ] }` llmResp, err := s.llmProvider.ChatCompletion(ctx, llm.ChatRequest{ Model: "gpt-4", Messages: []llm.ChatMessage{ {Role: "system", Content: systemPrompt}, {Role: "user", Content: contextStr}, }, Temperature: 0.2, MaxTokens: 512, }) if err != nil { applogger.L().Errorf("ExtractActionItems LLM: %v", err) return nil, fmt.Errorf("action item extraction failed: %w", err) } if len(llmResp.Choices) == 0 { return nil, fmt.Errorf("no LLM response") } var result ActionItemsResult content := llmResp.Choices[0].Message.Content if err := parseJSONResponse(content, &result); err != nil { // Fallback: try to extract items from plain text result = extractActionItemsFromText(content) } return &result, nil } // --- Label/Priority Suggestion --- // Reference: M12 PRD §Captain AI — Recommendation Engine // SuggestLabels suggests labels and priority for a conversation. func (s *ConversationInsightService) SuggestLabels(ctx context.Context, accountID uint, req *LabelSuggestionRequest) (*LabelSuggestionResult, error) { contextStr, err := s.fetchConversationContext(ctx, req.ConversationID) if err != nil { return nil, fmt.Errorf("fetch conversation context: %w", err) } if contextStr == "" { return nil, fmt.Errorf("no messages found for conversation %d", req.ConversationID) } // Optionally use assistant guidelines for context-aware suggestions var assistantPrompt string if req.AssistantID > 0 { assistant, err := s.assistantRepo.GetByID(ctx, req.AssistantID) if err == nil && assistant != nil { cfg, _ := assistant.GetConfig() assistantPrompt = s.promptBuilder.BuildAssistantPrompt(assistant, cfg) } } systemPrompt := `You are a conversation categorization AI. Analyze the conversation and suggest: 1. Appropriate labels/tags (3-5 concise labels that categorize the conversation topic, type, and urgency) 2. Priority level: urgent, high, medium, or low 3. A brief reason explaining why these labels and priority were chosen Return as JSON: { "labels": ["label1", "label2", ...], "priority": "...", "reason": "..." }` if assistantPrompt != "" { systemPrompt = assistantPrompt + "\n\n" + systemPrompt } llmResp, err := s.llmProvider.ChatCompletion(ctx, llm.ChatRequest{ Model: "gpt-4", Messages: []llm.ChatMessage{ {Role: "system", Content: systemPrompt}, {Role: "user", Content: contextStr}, }, Temperature: 0.3, MaxTokens: 256, }) if err != nil { applogger.L().Errorf("SuggestLabels LLM: %v", err) return nil, fmt.Errorf("label suggestion failed: %w", err) } if len(llmResp.Choices) == 0 { return nil, fmt.Errorf("no LLM response") } var result LabelSuggestionResult content := llmResp.Choices[0].Message.Content if err := parseJSONResponse(content, &result); err != nil { result = extractLabelSuggestionFromText(content) } return &result, nil } // --- Helper Methods --- // fetchConversationContext retrieves recent messages and formats them for LLM input. func (s *ConversationInsightService) fetchConversationContext(ctx context.Context, conversationID uint) (string, error) { msgs, _, err := s.messageRepo.FindByConversation(ctx, conversationID, 0, 50) if err != nil { return "", fmt.Errorf("fetch messages: %w", err) } if len(msgs) == 0 { return "", nil } var builder strings.Builder for _, msg := range msgs { sender := "Customer" if msg.SenderType == "agent" || msg.SenderType == "user" { sender = "Agent" } builder.WriteString(fmt.Sprintf("[%s]: %s\n", sender, msg.Content)) } return builder.String(), nil }