package knowledge import ( "database/sql" "fmt" "strings" "gorm.io/gorm" "customer-support-system/internal/database" "customer-support-system/internal/models" "customer-support-system/pkg/logger" ) // KnowledgeService handles knowledge base operations type KnowledgeService struct { db *gorm.DB } // NewKnowledgeService creates a new knowledge service func NewKnowledgeService() *KnowledgeService { return &KnowledgeService{ db: database.GetDB(), } } // CreateKnowledgeEntry creates a new knowledge base entry func (s *KnowledgeService) CreateKnowledgeEntry(userID uint, req *models.CreateKnowledgeBaseRequest) (*models.KnowledgeBase, error) { knowledge := models.KnowledgeBase{ Question: req.Question, Answer: req.Answer, Category: req.Category, Tags: req.Tags, Priority: req.Priority, ViewCount: 0, Helpful: 0, NotHelpful: 0, Active: true, CreatedBy: userID, UpdatedBy: userID, } if err := s.db.Create(&knowledge).Error; err != nil { logger.WithError(err).WithField("user_id", userID).Error("Failed to create knowledge entry") return nil, fmt.Errorf("failed to create knowledge entry: %w", err) } logger.WithField("knowledge_id", knowledge.ID).Info("Knowledge entry created successfully") return &knowledge, nil } // GetKnowledgeEntry retrieves a knowledge base entry by ID func (s *KnowledgeService) GetKnowledgeEntry(knowledgeID uint) (*models.KnowledgeBase, error) { var knowledge models.KnowledgeBase if err := s.db.First(&knowledge, knowledgeID).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, fmt.Errorf("knowledge entry not found") } logger.WithError(err).WithField("knowledge_id", knowledgeID).Error("Failed to get knowledge entry") return nil, fmt.Errorf("failed to get knowledge entry: %w", err) } // Increment view count s.db.Model(&knowledge).Update("view_count", knowledge.ViewCount+1) return &knowledge, nil } // ListKnowledgeEntries retrieves a list of knowledge base entries func (s *KnowledgeService) ListKnowledgeEntries(page, pageSize int, category string, active *bool) (*models.KnowledgeBaseListResponse, error) { var knowledge []models.KnowledgeBase var total int64 query := s.db.Model(&models.KnowledgeBase{}) if category != "" { query = query.Where("category = ?", category) } if active != nil { query = query.Where("active = ?", *active) } // Get total count if err := query.Count(&total).Error; err != nil { logger.WithError(err).Error("Failed to count knowledge entries") return nil, fmt.Errorf("failed to count knowledge entries: %w", err) } // Get paginated results offset := (page - 1) * pageSize if err := query.Order("priority DESC, view_count DESC, created_at DESC").Offset(offset).Limit(pageSize).Find(&knowledge).Error; err != nil { logger.WithError(err).Error("Failed to list knowledge entries") return nil, fmt.Errorf("failed to list knowledge entries: %w", err) } return &models.KnowledgeBaseListResponse{ Entries: knowledge, Total: total, Page: page, PageSize: pageSize, }, nil } // UpdateKnowledgeEntry updates a knowledge base entry func (s *KnowledgeService) UpdateKnowledgeEntry(knowledgeID uint, userID uint, req *models.UpdateKnowledgeBaseRequest) (*models.KnowledgeBase, error) { var knowledge models.KnowledgeBase if err := s.db.First(&knowledge, knowledgeID).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, fmt.Errorf("knowledge entry not found") } logger.WithError(err).WithField("knowledge_id", knowledgeID).Error("Failed to get knowledge entry") return nil, fmt.Errorf("failed to get knowledge entry: %w", err) } // Check if user is the creator or an admin if knowledge.CreatedBy != userID { // Check if user is admin var user models.User if err := s.db.First(&user, userID).Error; err != nil { logger.WithError(err).WithField("user_id", userID).Error("Failed to get user") return nil, fmt.Errorf("failed to get user: %w", err) } if user.Role != "admin" { return nil, fmt.Errorf("unauthorized to update this knowledge entry") } } // Update fields if req.Question != "" { knowledge.Question = req.Question } if req.Answer != "" { knowledge.Answer = req.Answer } if req.Category != "" { knowledge.Category = req.Category } if req.Tags != "" { knowledge.Tags = req.Tags } if req.Priority != 0 { knowledge.Priority = req.Priority } if req.Active != nil { knowledge.Active = *req.Active } knowledge.UpdatedBy = userID if err := s.db.Save(&knowledge).Error; err != nil { logger.WithError(err).WithField("knowledge_id", knowledgeID).Error("Failed to update knowledge entry") return nil, fmt.Errorf("failed to update knowledge entry: %w", err) } logger.WithField("knowledge_id", knowledgeID).Info("Knowledge entry updated successfully") return &knowledge, nil } // DeleteKnowledgeEntry deletes a knowledge base entry func (s *KnowledgeService) DeleteKnowledgeEntry(knowledgeID uint, userID uint) error { var knowledge models.KnowledgeBase if err := s.db.First(&knowledge, knowledgeID).Error; err != nil { if err == gorm.ErrRecordNotFound { return fmt.Errorf("knowledge entry not found") } logger.WithError(err).WithField("knowledge_id", knowledgeID).Error("Failed to get knowledge entry") return fmt.Errorf("failed to get knowledge entry: %w", err) } // Check if user is the creator or an admin if knowledge.CreatedBy != userID { // Check if user is admin var user models.User if err := s.db.First(&user, userID).Error; err != nil { logger.WithError(err).WithField("user_id", userID).Error("Failed to get user") return fmt.Errorf("failed to get user: %w", err) } if user.Role != "admin" { return fmt.Errorf("unauthorized to delete this knowledge entry") } } if err := s.db.Delete(&knowledge).Error; err != nil { logger.WithError(err).WithField("knowledge_id", knowledgeID).Error("Failed to delete knowledge entry") return fmt.Errorf("failed to delete knowledge entry: %w", err) } logger.WithField("knowledge_id", knowledgeID).Info("Knowledge entry deleted successfully") return nil } // SearchKnowledge searches for knowledge base entries func (s *KnowledgeService) SearchKnowledge(query string, page, pageSize int, category string) (*models.KnowledgeBaseSearchResponse, error) { var knowledge []models.KnowledgeBase var total int64 // Build search query searchQuery := s.db.Model(&models.KnowledgeBase{}) if query != "" { // Search in question, answer, and tags searchQuery = searchQuery.Where( "active = ? AND (question ILIKE ? OR answer ILIKE ? OR tags ILIKE ?)", true, "%"+query+"%", "%"+query+"%", "%"+query+"%", ) } else { searchQuery = searchQuery.Where("active = ?", true) } if category != "" { searchQuery = searchQuery.Where("category = ?", category) } // Get total count if err := searchQuery.Count(&total).Error; err != nil { logger.WithError(err).WithField("query", query).Error("Failed to count knowledge entries") return nil, fmt.Errorf("failed to count knowledge entries: %w", err) } // Get paginated results offset := (page - 1) * pageSize if err := searchQuery.Order("priority DESC, view_count DESC, created_at DESC").Offset(offset).Limit(pageSize).Find(&knowledge).Error; err != nil { logger.WithError(err).WithField("query", query).Error("Failed to search knowledge entries") return nil, fmt.Errorf("failed to search knowledge entries: %w", err) } // Convert to search results with relevance scores results := make([]models.KnowledgeBaseSearchResult, len(knowledge)) for i, entry := range knowledge { results[i] = models.KnowledgeBaseSearchResult{ KnowledgeBase: entry, RelevanceScore: s.calculateRelevanceScore(query, entry), MatchedFields: s.getMatchedFields(query, entry), } } return &models.KnowledgeBaseSearchResponse{ Results: results, Total: total, Query: query, Page: page, PageSize: pageSize, }, nil } // GetCategories retrieves all unique categories in the knowledge base func (s *KnowledgeService) GetCategories() ([]string, error) { var categories []string if err := s.db.Model(&models.KnowledgeBase{}).Where("active = ?", true).Distinct().Pluck("category", &categories).Error; err != nil { logger.WithError(err).Error("Failed to get knowledge categories") return nil, fmt.Errorf("failed to get knowledge categories: %w", err) } return categories, nil } // GetTags retrieves all unique tags in the knowledge base func (s *KnowledgeService) GetTags() ([]string, error) { var tags []string if err := s.db.Model(&models.KnowledgeBase{}).Where("active = ?", true).Find(&[]models.KnowledgeBase{}).Error; err != nil { logger.WithError(err).Error("Failed to get knowledge entries for tags") return nil, fmt.Errorf("failed to get knowledge entries for tags: %w", err) } // Extract and deduplicate tags tagMap := make(map[string]bool) for _, entry := range []models.KnowledgeBase{} { if entry.Tags != "" { entryTags := strings.Split(entry.Tags, ",") for _, tag := range entryTags { tag = strings.TrimSpace(tag) if tag != "" { tagMap[tag] = true } } } } // Convert map to slice tags = make([]string, 0, len(tagMap)) for tag := range tagMap { tags = append(tags, tag) } return tags, nil } // RateKnowledgeEntry rates a knowledge base entry as helpful or not func (s *KnowledgeService) RateKnowledgeEntry(knowledgeID uint, userID uint, helpful bool, comment string) error { var knowledge models.KnowledgeBase if err := s.db.First(&knowledge, knowledgeID).Error; err != nil { if err == gorm.ErrRecordNotFound { return fmt.Errorf("knowledge entry not found") } logger.WithError(err).WithField("knowledge_id", knowledgeID).Error("Failed to get knowledge entry") return fmt.Errorf("failed to get knowledge entry: %w", err) } // Create feedback record feedback := models.KnowledgeBaseFeedback{ KnowledgeBaseID: knowledgeID, UserID: userID, Helpful: helpful, Comment: comment, } if err := s.db.Create(&feedback).Error; err != nil { logger.WithError(err).WithField("knowledge_id", knowledgeID).Error("Failed to create knowledge feedback") return fmt.Errorf("failed to create knowledge feedback: %w", err) } // Update helpful/not helpful counts if helpful { knowledge.Helpful++ } else { knowledge.NotHelpful++ } if err := s.db.Save(&knowledge).Error; err != nil { logger.WithError(err).WithField("knowledge_id", knowledgeID).Error("Failed to rate knowledge entry") return fmt.Errorf("failed to rate knowledge entry: %w", err) } logger.WithFields(map[string]interface{}{ "knowledge_id": knowledgeID, "user_id": userID, "helpful": helpful, }).Info("Knowledge entry rated successfully") return nil } // GetPopularKnowledge retrieves popular knowledge base entries func (s *KnowledgeService) GetPopularKnowledge(limit int) ([]models.KnowledgeBase, error) { var knowledge []models.KnowledgeBase if err := s.db.Where("active = ?", true). Order("view_count DESC, helpful DESC"). Limit(limit). Find(&knowledge).Error; err != nil { logger.WithError(err).Error("Failed to get popular knowledge entries") return nil, fmt.Errorf("failed to get popular knowledge entries: %w", err) } return knowledge, nil } // GetRecentKnowledge retrieves recent knowledge base entries func (s *KnowledgeService) GetRecentKnowledge(limit int) ([]models.KnowledgeBase, error) { var knowledge []models.KnowledgeBase if err := s.db.Where("active = ?", true). Order("created_at DESC"). Limit(limit). Find(&knowledge).Error; err != nil { logger.WithError(err).Error("Failed to get recent knowledge entries") return nil, fmt.Errorf("failed to get recent knowledge entries: %w", err) } return knowledge, nil } // FindBestMatch finds the best matching knowledge base entry for a query func (s *KnowledgeService) FindBestMatch(query string) (*models.KnowledgeBase, error) { // This is a simple implementation that looks for exact matches in tags // In a real implementation, this would use more sophisticated algorithms like TF-IDF or embeddings var knowledge models.KnowledgeBase if err := s.db.Where("active = ? AND tags ILIKE ?", true, "%"+query+"%"). Order("priority DESC, view_count DESC, helpful DESC"). First(&knowledge).Error; err != nil { if err == gorm.ErrRecordNotFound { // Try to find a match in the question if err := s.db.Where("active = ? AND question ILIKE ?", true, "%"+query+"%"). Order("priority DESC, view_count DESC, helpful DESC"). First(&knowledge).Error; err != nil { if err == gorm.ErrRecordNotFound { // Try to find a match in the answer if err := s.db.Where("active = ? AND answer ILIKE ?", true, "%"+query+"%"). Order("priority DESC, view_count DESC, helpful DESC"). First(&knowledge).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, fmt.Errorf("no matching knowledge entry found") } logger.WithError(err).WithField("query", query).Error("Failed to find best match in answer") return nil, fmt.Errorf("failed to find best match in answer: %w", err) } } logger.WithError(err).WithField("query", query).Error("Failed to find best match in question") return nil, fmt.Errorf("failed to find best match in question: %w", err) } } logger.WithError(err).WithField("query", query).Error("Failed to find best match in tags") return nil, fmt.Errorf("failed to find best match in tags: %w", err) } return &knowledge, nil } // GetKnowledgeStats retrieves statistics for the knowledge base func (s *KnowledgeService) GetKnowledgeStats() (*models.KnowledgeBaseStats, error) { var stats models.KnowledgeBaseStats // Get total entries if err := s.db.Model(&models.KnowledgeBase{}).Where("active = ?", true).Count(&stats.TotalEntries).Error; err != nil { logger.WithError(err).Error("Failed to count knowledge entries") return nil, fmt.Errorf("failed to count knowledge entries: %w", err) } // Get total views var totalViews sql.NullInt64 if err := s.db.Model(&models.KnowledgeBase{}).Where("active = ?", true).Select("SUM(view_count)").Scan(&totalViews).Error; err != nil { logger.WithError(err).Error("Failed to calculate total views") return nil, fmt.Errorf("failed to calculate total views: %w", err) } if totalViews.Valid { stats.TotalViews = totalViews.Int64 } // Get average helpful percentage var avgHelpful sql.NullFloat64 if err := s.db.Model(&models.KnowledgeBase{}).Where("active = ? AND helpful + not_helpful > 0", true). Select("AVG(helpful::float / (helpful + not_helpful)::float * 100)").Scan(&avgHelpful).Error; err != nil { logger.WithError(err).Error("Failed to calculate average helpful percentage") return nil, fmt.Errorf("failed to calculate average helpful percentage: %w", err) } if avgHelpful.Valid { stats.AverageHelpful = avgHelpful.Float64 } // Get top categories var categoryStats []models.CategoryStat if err := s.db.Model(&models.KnowledgeBase{}). Where("active = ?", true). Select("category, COUNT(*) as count"). Group("category"). Order("count DESC"). Limit(5). Scan(&categoryStats).Error; err != nil { logger.WithError(err).Error("Failed to get category statistics") return nil, fmt.Errorf("failed to get category statistics: %w", err) } stats.TopCategories = categoryStats // Get top tags var tagStats []models.TagStat if err := s.db.Model(&models.KnowledgeBase{}). Where("active = ? AND tags != ''", true). Select("unnest(string_to_array(tags, ',')) as tag, COUNT(*) as count"). Group("tag"). Order("count DESC"). Limit(10). Scan(&tagStats).Error; err != nil { logger.WithError(err).Error("Failed to get tag statistics") return nil, fmt.Errorf("failed to get tag statistics: %w", err) } stats.TopTags = tagStats return &stats, nil } // calculateRelevanceScore calculates a relevance score for a knowledge base entry against a query func (s *KnowledgeService) calculateRelevanceScore(query string, entry models.KnowledgeBase) float64 { if query == "" { return 0 } query = strings.ToLower(query) score := 0.0 // Check for exact match in question if strings.Contains(strings.ToLower(entry.Question), query) { score += 10.0 } // Check for exact match in answer if strings.Contains(strings.ToLower(entry.Answer), query) { score += 5.0 } // Check for exact match in tags if strings.Contains(strings.ToLower(entry.Tags), query) { score += 7.0 } // Check for word matches queryWords := strings.Fields(query) questionWords := strings.Fields(strings.ToLower(entry.Question)) answerWords := strings.Fields(strings.ToLower(entry.Answer)) tagsWords := strings.Fields(strings.ToLower(entry.Tags)) for _, word := range queryWords { // Check question words for _, qWord := range questionWords { if strings.EqualFold(word, qWord) { score += 2.0 } } // Check answer words for _, aWord := range answerWords { if strings.EqualFold(word, aWord) { score += 1.0 } } // Check tag words for _, tWord := range tagsWords { if strings.EqualFold(word, tWord) { score += 3.0 } } } // Boost score based on priority score += float64(entry.Priority) * 0.5 // Boost score based on helpfulness if entry.Helpful+entry.NotHelpful > 0 { helpfulness := float64(entry.Helpful) / float64(entry.Helpful+entry.NotHelpful) score += helpfulness * 2.0 } return score } // getMatchedFields returns the fields that matched the query func (s *KnowledgeService) getMatchedFields(query string, entry models.KnowledgeBase) []string { if query == "" { return []string{} } query = strings.ToLower(query) matchedFields := []string{} // Check question if strings.Contains(strings.ToLower(entry.Question), query) { matchedFields = append(matchedFields, "question") } // Check answer if strings.Contains(strings.ToLower(entry.Answer), query) { matchedFields = append(matchedFields, "answer") } // Check tags if strings.Contains(strings.ToLower(entry.Tags), query) { matchedFields = append(matchedFields, "tags") } return matchedFields }