committtttt
This commit is contained in:
436
backend/internal/conversation/conversation.go
Normal file
436
backend/internal/conversation/conversation.go
Normal file
@@ -0,0 +1,436 @@
|
||||
package conversation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"customer-support-system/internal/ai"
|
||||
"customer-support-system/internal/database"
|
||||
"customer-support-system/internal/models"
|
||||
"customer-support-system/pkg/logger"
|
||||
)
|
||||
|
||||
// ConversationService handles conversation operations
|
||||
type ConversationService struct {
|
||||
db *gorm.DB
|
||||
aiService *ai.AIService
|
||||
}
|
||||
|
||||
// NewConversationService creates a new conversation service
|
||||
func NewConversationService() *ConversationService {
|
||||
return &ConversationService{
|
||||
db: database.GetDB(),
|
||||
aiService: ai.NewAIService(),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateConversation creates a new conversation
|
||||
func (s *ConversationService) CreateConversation(userID uint, req *models.CreateConversationRequest) (*models.Conversation, error) {
|
||||
conversation := models.Conversation{
|
||||
Title: req.Title,
|
||||
UserID: userID,
|
||||
Department: req.Department,
|
||||
Priority: req.Priority,
|
||||
Tags: req.Tags,
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
if err := s.db.Create(&conversation).Error; err != nil {
|
||||
logger.WithError(err).WithField("user_id", userID).Error("Failed to create conversation")
|
||||
return nil, fmt.Errorf("failed to create conversation: %w", err)
|
||||
}
|
||||
|
||||
logger.WithField("conversation_id", conversation.ID).Info("Conversation created successfully")
|
||||
|
||||
return &conversation, nil
|
||||
}
|
||||
|
||||
// GetConversation retrieves a conversation by ID
|
||||
func (s *ConversationService) GetConversation(conversationID uint, userID uint) (*models.Conversation, error) {
|
||||
var conversation models.Conversation
|
||||
if err := s.db.Where("id = ? AND user_id = ?", conversationID, userID).First(&conversation).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("conversation not found")
|
||||
}
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get conversation")
|
||||
return nil, fmt.Errorf("failed to get conversation: %w", err)
|
||||
}
|
||||
|
||||
return &conversation, nil
|
||||
}
|
||||
|
||||
// ListConversations retrieves a list of conversations for a user
|
||||
func (s *ConversationService) ListConversations(userID uint, page, pageSize int, status string) (*models.ConversationListResponse, error) {
|
||||
var conversations []models.Conversation
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&models.Conversation{}).Where("user_id = ?", userID)
|
||||
|
||||
if status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
// Get total count
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
logger.WithError(err).WithField("user_id", userID).Error("Failed to count conversations")
|
||||
return nil, fmt.Errorf("failed to count conversations: %w", err)
|
||||
}
|
||||
|
||||
// Get paginated results
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("last_message_at DESC").Offset(offset).Limit(pageSize).Find(&conversations).Error; err != nil {
|
||||
logger.WithError(err).WithField("user_id", userID).Error("Failed to list conversations")
|
||||
return nil, fmt.Errorf("failed to list conversations: %w", err)
|
||||
}
|
||||
|
||||
return &models.ConversationListResponse{
|
||||
Conversations: conversations,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateConversation updates a conversation
|
||||
func (s *ConversationService) UpdateConversation(conversationID uint, userID uint, req *models.UpdateConversationRequest) (*models.Conversation, error) {
|
||||
var conversation models.Conversation
|
||||
if err := s.db.Where("id = ? AND user_id = ?", conversationID, userID).First(&conversation).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("conversation not found")
|
||||
}
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get conversation")
|
||||
return nil, fmt.Errorf("failed to get conversation: %w", err)
|
||||
}
|
||||
|
||||
// Update fields
|
||||
if req.Title != "" {
|
||||
conversation.Title = req.Title
|
||||
}
|
||||
if req.Status != "" {
|
||||
conversation.Status = req.Status
|
||||
}
|
||||
if req.Department != "" {
|
||||
conversation.Department = req.Department
|
||||
}
|
||||
if req.Priority != "" {
|
||||
conversation.Priority = req.Priority
|
||||
}
|
||||
if req.Tags != "" {
|
||||
conversation.Tags = req.Tags
|
||||
}
|
||||
if req.AgentID != nil {
|
||||
conversation.AgentID = req.AgentID
|
||||
}
|
||||
|
||||
if err := s.db.Save(&conversation).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to update conversation")
|
||||
return nil, fmt.Errorf("failed to update conversation: %w", err)
|
||||
}
|
||||
|
||||
logger.WithField("conversation_id", conversationID).Info("Conversation updated successfully")
|
||||
|
||||
return &conversation, nil
|
||||
}
|
||||
|
||||
// DeleteConversation deletes a conversation
|
||||
func (s *ConversationService) DeleteConversation(conversationID uint, userID uint) error {
|
||||
var conversation models.Conversation
|
||||
if err := s.db.Where("id = ? AND user_id = ?", conversationID, userID).First(&conversation).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return fmt.Errorf("conversation not found")
|
||||
}
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get conversation")
|
||||
return fmt.Errorf("failed to get conversation: %w", err)
|
||||
}
|
||||
|
||||
if err := s.db.Delete(&conversation).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to delete conversation")
|
||||
return fmt.Errorf("failed to delete conversation: %w", err)
|
||||
}
|
||||
|
||||
logger.WithField("conversation_id", conversationID).Info("Conversation deleted successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateMessage creates a new message in a conversation
|
||||
func (s *ConversationService) CreateMessage(conversationID uint, userID uint, req *models.CreateMessageRequest) (*models.Message, error) {
|
||||
// Check if conversation exists and belongs to user
|
||||
var conversation models.Conversation
|
||||
if err := s.db.Where("id = ? AND user_id = ?", conversationID, userID).First(&conversation).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("conversation not found")
|
||||
}
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get conversation")
|
||||
return nil, fmt.Errorf("failed to get conversation: %w", err)
|
||||
}
|
||||
|
||||
// Create message
|
||||
message := models.Message{
|
||||
ConversationID: conversationID,
|
||||
UserID: userID,
|
||||
Content: req.Content,
|
||||
Type: req.Type,
|
||||
Status: "sent",
|
||||
IsAI: false,
|
||||
}
|
||||
|
||||
if err := s.db.Create(&message).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to create message")
|
||||
return nil, fmt.Errorf("failed to create message: %w", err)
|
||||
}
|
||||
|
||||
logger.WithField("message_id", message.ID).Info("Message created successfully")
|
||||
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
// GetMessages retrieves messages in a conversation
|
||||
func (s *ConversationService) GetMessages(conversationID uint, userID uint, page, pageSize int) (*models.MessageListResponse, error) {
|
||||
// Check if conversation exists and belongs to user
|
||||
var conversation models.Conversation
|
||||
if err := s.db.Where("id = ? AND user_id = ?", conversationID, userID).First(&conversation).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("conversation not found")
|
||||
}
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get conversation")
|
||||
return nil, fmt.Errorf("failed to get conversation: %w", err)
|
||||
}
|
||||
|
||||
var messages []models.Message
|
||||
var total int64
|
||||
|
||||
// Get total count
|
||||
if err := s.db.Model(&models.Message{}).Where("conversation_id = ?", conversationID).Count(&total).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to count messages")
|
||||
return nil, fmt.Errorf("failed to count messages: %w", err)
|
||||
}
|
||||
|
||||
// Get paginated results
|
||||
offset := (page - 1) * pageSize
|
||||
if err := s.db.Where("conversation_id = ?", conversationID).Order("created_at ASC").Offset(offset).Limit(pageSize).Find(&messages).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get messages")
|
||||
return nil, fmt.Errorf("failed to get messages: %w", err)
|
||||
}
|
||||
|
||||
return &models.MessageListResponse{
|
||||
Messages: messages,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateMessage updates a message
|
||||
func (s *ConversationService) UpdateMessage(messageID uint, userID uint, req *models.UpdateMessageRequest) (*models.Message, error) {
|
||||
var message models.Message
|
||||
if err := s.db.Where("id = ? AND user_id = ?", messageID, userID).First(&message).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("message not found")
|
||||
}
|
||||
logger.WithError(err).WithField("message_id", messageID).Error("Failed to get message")
|
||||
return nil, fmt.Errorf("failed to get message: %w", err)
|
||||
}
|
||||
|
||||
// Update fields
|
||||
if req.Content != "" {
|
||||
message.Content = req.Content
|
||||
}
|
||||
if req.Status != "" {
|
||||
message.Status = req.Status
|
||||
}
|
||||
|
||||
if err := s.db.Save(&message).Error; err != nil {
|
||||
logger.WithError(err).WithField("message_id", messageID).Error("Failed to update message")
|
||||
return nil, fmt.Errorf("failed to update message: %w", err)
|
||||
}
|
||||
|
||||
logger.WithField("message_id", messageID).Info("Message updated successfully")
|
||||
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
// DeleteMessage deletes a message
|
||||
func (s *ConversationService) DeleteMessage(messageID uint, userID uint) error {
|
||||
var message models.Message
|
||||
if err := s.db.Where("id = ? AND user_id = ?", messageID, userID).First(&message).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return fmt.Errorf("message not found")
|
||||
}
|
||||
logger.WithError(err).WithField("message_id", messageID).Error("Failed to get message")
|
||||
return fmt.Errorf("failed to get message: %w", err)
|
||||
}
|
||||
|
||||
if err := s.db.Delete(&message).Error; err != nil {
|
||||
logger.WithError(err).WithField("message_id", messageID).Error("Failed to delete message")
|
||||
return fmt.Errorf("failed to delete message: %w", err)
|
||||
}
|
||||
|
||||
logger.WithField("message_id", messageID).Info("Message deleted successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendMessageWithAI sends a message and gets an AI response
|
||||
func (s *ConversationService) SendMessageWithAI(conversationID uint, userID uint, content string) (*models.Message, *models.Message, error) {
|
||||
// Check if conversation exists and belongs to user
|
||||
var conversation models.Conversation
|
||||
if err := s.db.Where("id = ? AND user_id = ?", conversationID, userID).First(&conversation).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil, fmt.Errorf("conversation not found")
|
||||
}
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get conversation")
|
||||
return nil, nil, fmt.Errorf("failed to get conversation: %w", err)
|
||||
}
|
||||
|
||||
// Create user message
|
||||
userMessage := models.Message{
|
||||
ConversationID: conversationID,
|
||||
UserID: userID,
|
||||
Content: content,
|
||||
Type: "text",
|
||||
Status: "sent",
|
||||
IsAI: false,
|
||||
}
|
||||
|
||||
if err := s.db.Create(&userMessage).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to create user message")
|
||||
return nil, nil, fmt.Errorf("failed to create user message: %w", err)
|
||||
}
|
||||
|
||||
// Get conversation history
|
||||
var messages []models.Message
|
||||
if err := s.db.Where("conversation_id = ?", conversationID).Order("created_at ASC").Find(&messages).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get conversation history")
|
||||
return nil, nil, fmt.Errorf("failed to get conversation history: %w", err)
|
||||
}
|
||||
|
||||
// Analyze complexity of the message
|
||||
complexity := s.aiService.AnalyzeComplexity(content)
|
||||
|
||||
// Get AI response
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
aiResponse, err := s.aiService.Query(ctx, content, messages, complexity)
|
||||
if err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get AI response")
|
||||
return &userMessage, nil, fmt.Errorf("failed to get AI response: %w", err)
|
||||
}
|
||||
|
||||
// Create AI message
|
||||
aiMessage := models.Message{
|
||||
ConversationID: conversationID,
|
||||
UserID: userID, // AI responses are associated with the user who asked the question
|
||||
Content: aiResponse,
|
||||
Type: "text",
|
||||
Status: "sent",
|
||||
IsAI: true,
|
||||
AIModel: "gpt-4", // This should be determined based on which AI model was used
|
||||
}
|
||||
|
||||
if err := s.db.Create(&aiMessage).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to create AI message")
|
||||
return &userMessage, nil, fmt.Errorf("failed to create AI message: %w", err)
|
||||
}
|
||||
|
||||
logger.WithFields(map[string]interface{}{
|
||||
"conversation_id": conversationID,
|
||||
"user_message_id": userMessage.ID,
|
||||
"ai_message_id": aiMessage.ID,
|
||||
}).Info("AI response created successfully")
|
||||
|
||||
return &userMessage, &aiMessage, nil
|
||||
}
|
||||
|
||||
// GetConversationStats retrieves statistics for a conversation
|
||||
func (s *ConversationService) GetConversationStats(conversationID uint, userID uint) (*models.ConversationStats, error) {
|
||||
// Check if conversation exists and belongs to user
|
||||
var conversation models.Conversation
|
||||
if err := s.db.Where("id = ? AND user_id = ?", conversationID, userID).First(&conversation).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("conversation not found")
|
||||
}
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get conversation")
|
||||
return nil, fmt.Errorf("failed to get conversation: %w", err)
|
||||
}
|
||||
|
||||
var stats models.ConversationStats
|
||||
|
||||
// Get total messages count
|
||||
if err := s.db.Model(&models.Message{}).Where("conversation_id = ?", conversationID).Count(&stats.TotalMessages).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to count messages")
|
||||
return nil, fmt.Errorf("failed to count messages: %w", err)
|
||||
}
|
||||
|
||||
// Get average sentiment
|
||||
var avgSentiment sql.NullFloat64
|
||||
if err := s.db.Model(&models.Message{}).Where("conversation_id = ?", conversationID).Select("AVG(sentiment)").Scan(&avgSentiment).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to calculate average sentiment")
|
||||
return nil, fmt.Errorf("failed to calculate average sentiment: %w", err)
|
||||
}
|
||||
if avgSentiment.Valid {
|
||||
stats.AverageSentiment = avgSentiment.Float64
|
||||
}
|
||||
|
||||
// Get first and last message timestamps
|
||||
var firstMessage, lastMessage models.Message
|
||||
if err := s.db.Where("conversation_id = ?", conversationID).Order("created_at ASC").First(&firstMessage).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get first message")
|
||||
return nil, fmt.Errorf("failed to get first message: %w", err)
|
||||
}
|
||||
stats.FirstMessageAt = firstMessage.CreatedAt
|
||||
|
||||
if err := s.db.Where("conversation_id = ?", conversationID).Order("created_at DESC").First(&lastMessage).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get last message")
|
||||
return nil, fmt.Errorf("failed to get last message: %w", err)
|
||||
}
|
||||
stats.LastMessageAt = lastMessage.CreatedAt
|
||||
|
||||
// Calculate average response time (time between user messages and AI responses)
|
||||
// This is a simplified calculation and could be improved
|
||||
if stats.TotalMessages > 1 {
|
||||
var responseTimes []int64
|
||||
var prevMessage models.Message
|
||||
isUserMessage := false
|
||||
|
||||
var allMessages []models.Message
|
||||
if err := s.db.Where("conversation_id = ?", conversationID).Order("created_at ASC").Find(&allMessages).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get messages for response time calculation")
|
||||
return nil, fmt.Errorf("failed to get messages for response time calculation: %w", err)
|
||||
}
|
||||
|
||||
for _, msg := range allMessages {
|
||||
if !msg.IsAI {
|
||||
if isUserMessage {
|
||||
// Consecutive user messages, skip
|
||||
prevMessage = msg
|
||||
continue
|
||||
}
|
||||
isUserMessage = true
|
||||
prevMessage = msg
|
||||
} else {
|
||||
if isUserMessage {
|
||||
// AI response to user message, calculate response time
|
||||
responseTime := msg.CreatedAt.Sub(prevMessage.CreatedAt).Seconds()
|
||||
responseTimes = append(responseTimes, int64(responseTime))
|
||||
}
|
||||
isUserMessage = false
|
||||
}
|
||||
}
|
||||
|
||||
if len(responseTimes) > 0 {
|
||||
var totalResponseTime int64 = 0
|
||||
for _, rt := range responseTimes {
|
||||
totalResponseTime += rt
|
||||
}
|
||||
stats.ResponseTime = totalResponseTime / int64(len(responseTimes))
|
||||
}
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
Reference in New Issue
Block a user