committtttt

This commit is contained in:
Dev
2025-09-13 06:48:55 +03:00
commit 2d49f69be6
32 changed files with 6478 additions and 0 deletions

394
backend/internal/ai/ai.go Normal file
View File

@@ -0,0 +1,394 @@
package ai
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"customer-support-system/internal/models"
"customer-support-system/pkg/config"
"customer-support-system/pkg/logger"
)
// OpenAIRequest represents a request to the OpenAI API
type OpenAIRequest struct {
Model string `json:"model"`
Messages []OpenAIMessage `json:"messages"`
MaxTokens int `json:"max_tokens,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
TopP float64 `json:"top_p,omitempty"`
Stream bool `json:"stream,omitempty"`
}
// OpenAIMessage represents a message in the OpenAI API
type OpenAIMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
// OpenAIResponse represents a response from the OpenAI API
type OpenAIResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []OpenAIChoice `json:"choices"`
Usage OpenAIUsage `json:"usage"`
}
// OpenAIChoice represents a choice in the OpenAI API response
type OpenAIChoice struct {
Index int `json:"index"`
Message OpenAIMessage `json:"message"`
FinishReason string `json:"finish_reason"`
}
// OpenAIUsage represents usage statistics in the OpenAI API response
type OpenAIUsage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
// OllamaRequest represents a request to the Ollama API
type OllamaRequest struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
Stream bool `json:"stream,omitempty"`
Options map[string]any `json:"options,omitempty"`
}
// OllamaResponse represents a response from the Ollama API
type OllamaResponse struct {
Model string `json:"model"`
CreatedAt string `json:"created_at"`
Response string `json:"response"`
Done bool `json:"done"`
}
// AIService handles AI operations
type AIService struct {
openAIConfig config.APIConfig
localConfig config.APIConfig
httpClient *http.Client
}
// NewAIService creates a new AI service
func NewAIService() *AIService {
return &AIService{
openAIConfig: config.AppConfig.AI.OpenAI,
localConfig: config.AppConfig.AI.Local,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// QueryOpenAI sends a query to the OpenAI API
func (s *AIService) QueryOpenAI(ctx context.Context, prompt string, conversationHistory []models.Message) (string, error) {
startTime := time.Now()
// Convert conversation history to OpenAI messages
messages := s.convertToOpenAIMessages(prompt, conversationHistory)
// Create request
req := OpenAIRequest{
Model: s.openAIConfig.Model,
Messages: messages,
MaxTokens: s.openAIConfig.MaxTokens,
Temperature: s.openAIConfig.Temperature,
TopP: s.openAIConfig.TopP,
}
// Marshal request to JSON
reqBody, err := json.Marshal(req)
if err != nil {
logger.WithError(err).Error("Failed to marshal OpenAI request")
return "", fmt.Errorf("failed to marshal request: %w", err)
}
// Create HTTP request
httpReq, err := http.NewRequestWithContext(ctx, "POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(reqBody))
if err != nil {
logger.WithError(err).Error("Failed to create OpenAI HTTP request")
return "", fmt.Errorf("failed to create HTTP request: %w", err)
}
// Set headers
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.openAIConfig.APIKey))
// Send request
resp, err := s.httpClient.Do(httpReq)
if err != nil {
logger.WithError(err).Error("Failed to send OpenAI request")
return "", fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
logger.WithError(err).Error("Failed to read OpenAI response")
return "", fmt.Errorf("failed to read response: %w", err)
}
// Check status code
if resp.StatusCode != http.StatusOK {
logger.WithField("status_code", resp.StatusCode).
WithField("response", string(respBody)).
Error("OpenAI API returned non-200 status code")
return "", fmt.Errorf("OpenAI API returned status code %d: %s", resp.StatusCode, string(respBody))
}
// Parse response
var openAIResp OpenAIResponse
if err := json.Unmarshal(respBody, &openAIResp); err != nil {
logger.WithError(err).Error("Failed to parse OpenAI response")
return "", fmt.Errorf("failed to parse response: %w", err)
}
// Extract response text
if len(openAIResp.Choices) == 0 {
logger.Error("OpenAI API returned no choices")
return "", fmt.Errorf("OpenAI API returned no choices")
}
responseText := openAIResp.Choices[0].Message.Content
// Log AI interaction
duration := time.Since(startTime)
logger.LogAIInteraction(
s.openAIConfig.Model,
len(prompt),
len(responseText),
duration,
true,
nil,
)
return responseText, nil
}
// QueryOllama sends a query to the Ollama API
func (s *AIService) QueryOllama(ctx context.Context, prompt string, conversationHistory []models.Message) (string, error) {
startTime := time.Now()
// Create request
req := OllamaRequest{
Model: s.localConfig.Model,
Prompt: s.buildOllamaPrompt(prompt, conversationHistory),
Stream: false,
Options: map[string]any{
"temperature": s.localConfig.Temperature,
"top_p": s.localConfig.TopP,
"num_predict": s.localConfig.MaxTokens,
},
}
// Marshal request to JSON
reqBody, err := json.Marshal(req)
if err != nil {
logger.WithError(err).Error("Failed to marshal Ollama request")
return "", fmt.Errorf("failed to marshal request: %w", err)
}
// Create HTTP request
httpReq, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/api/generate", s.localConfig.Endpoint), bytes.NewBuffer(reqBody))
if err != nil {
logger.WithError(err).Error("Failed to create Ollama HTTP request")
return "", fmt.Errorf("failed to create HTTP request: %w", err)
}
// Set headers
httpReq.Header.Set("Content-Type", "application/json")
// Send request
resp, err := s.httpClient.Do(httpReq)
if err != nil {
logger.WithError(err).Error("Failed to send Ollama request")
return "", fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
logger.WithError(err).Error("Failed to read Ollama response")
return "", fmt.Errorf("failed to read response: %w", err)
}
// Check status code
if resp.StatusCode != http.StatusOK {
logger.WithField("status_code", resp.StatusCode).
WithField("response", string(respBody)).
Error("Ollama API returned non-200 status code")
return "", fmt.Errorf("Ollama API returned status code %d: %s", resp.StatusCode, string(respBody))
}
// Parse response
var ollamaResp OllamaResponse
if err := json.Unmarshal(respBody, &ollamaResp); err != nil {
logger.WithError(err).Error("Failed to parse Ollama response")
return "", fmt.Errorf("failed to parse response: %w", err)
}
// Extract response text
responseText := ollamaResp.Response
// Log AI interaction
duration := time.Since(startTime)
logger.LogAIInteraction(
s.localConfig.Model,
len(prompt),
len(responseText),
duration,
true,
nil,
)
return responseText, nil
}
// Query sends a query to the appropriate AI model based on complexity
func (s *AIService) Query(ctx context.Context, prompt string, conversationHistory []models.Message, complexity int) (string, error) {
// Determine which AI model to use based on complexity
if complexity >= 7 { // High complexity, use OpenAI
return s.QueryOpenAI(ctx, prompt, conversationHistory)
} else { // Low to medium complexity, use local LLM
return s.QueryOllama(ctx, prompt, conversationHistory)
}
}
// AnalyzeComplexity analyzes the complexity of a prompt
func (s *AIService) AnalyzeComplexity(prompt string) int {
// Simple heuristic for complexity analysis
// In a real implementation, this would be more sophisticated
complexity := 0
// Length factor
if len(prompt) > 100 {
complexity += 2
}
if len(prompt) > 200 {
complexity += 2
}
// Question type factor
if strings.Contains(prompt, "?") {
complexity += 1
}
// Technical terms factor
technicalTerms := []string{"API", "database", "server", "code", "programming", "software", "algorithm"}
for _, term := range technicalTerms {
if strings.Contains(strings.ToLower(prompt), strings.ToLower(term)) {
complexity += 1
}
}
// Multiple questions factor
questionCount := strings.Count(prompt, "?")
if questionCount > 1 {
complexity += questionCount - 1
}
// Cap complexity at 10
if complexity > 10 {
complexity = 10
}
return complexity
}
// convertToOpenAIMessages converts conversation history to OpenAI messages
func (s *AIService) convertToOpenAIMessages(prompt string, conversationHistory []models.Message) []OpenAIMessage {
messages := []OpenAIMessage{}
// Add system message
messages = append(messages, OpenAIMessage{
Role: "system",
Content: "You are a helpful customer support assistant. Provide clear, concise, and accurate answers to customer questions.",
})
// Add conversation history
for _, msg := range conversationHistory {
role := "user"
if msg.IsAI {
role = "assistant"
}
messages = append(messages, OpenAIMessage{
Role: role,
Content: msg.Content,
})
}
// Add current prompt
messages = append(messages, OpenAIMessage{
Role: "user",
Content: prompt,
})
return messages
}
// buildOllamaPrompt builds a prompt for Ollama from conversation history
func (s *AIService) buildOllamaPrompt(prompt string, conversationHistory []models.Message) string {
var builder strings.Builder
// Add system instruction
builder.WriteString("You are a helpful customer support assistant. Provide clear, concise, and accurate answers to customer questions.\n\n")
// Add conversation history
for _, msg := range conversationHistory {
if msg.IsAI {
builder.WriteString("Assistant: ")
} else {
builder.WriteString("User: ")
}
builder.WriteString(msg.Content)
builder.WriteString("\n\n")
}
// Add current prompt
builder.WriteString("User: ")
builder.WriteString(prompt)
builder.WriteString("\n\nAssistant: ")
return builder.String()
}
// GetAvailableModels returns the available AI models
func (s *AIService) GetAvailableModels() []models.AIModel {
return []models.AIModel{
{
Name: "OpenAI GPT-4",
Type: "openai",
Model: s.openAIConfig.Model,
MaxTokens: s.openAIConfig.MaxTokens,
Temperature: s.openAIConfig.Temperature,
TopP: s.openAIConfig.TopP,
Active: true,
Priority: 2,
Description: "OpenAI's GPT-4 model for complex queries",
},
{
Name: "Local LLaMA",
Type: "local",
Model: s.localConfig.Model,
Endpoint: s.localConfig.Endpoint,
MaxTokens: s.localConfig.MaxTokens,
Temperature: s.localConfig.Temperature,
TopP: s.localConfig.TopP,
Active: true,
Priority: 1,
Description: "Local LLaMA model for simple queries",
},
}
}

View File

@@ -0,0 +1,362 @@
package auth
import (
"errors"
"fmt"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"customer-support-system/internal/database"
"customer-support-system/internal/models"
"customer-support-system/pkg/config"
"customer-support-system/pkg/logger"
)
var (
ErrInvalidCredentials = errors.New("invalid credentials")
ErrUserNotFound = errors.New("user not found")
ErrUserInactive = errors.New("user is inactive")
ErrAccountLocked = errors.New("account is locked")
ErrTokenExpired = errors.New("token has expired")
ErrInvalidToken = errors.New("invalid token")
)
// AuthService handles authentication operations
type AuthService struct {
db *gorm.DB
}
// NewAuthService creates a new authentication service
func NewAuthService(db *gorm.DB) *AuthService {
return &AuthService{db: db}
}
// Login authenticates a user and returns a JWT token
func (s *AuthService) Login(username, password, clientIP string) (*models.LoginResponse, error) {
// Find user by username
var user models.User
if err := s.db.Where("username = ?", username).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
logger.WithField("username", username).Warn("Login attempt with non-existent username")
return nil, ErrUserNotFound
}
return nil, fmt.Errorf("failed to find user: %w", err)
}
// Check if user is active
if !user.Active {
logger.WithField("user_id", user.ID).Warn("Login attempt by inactive user")
return nil, ErrUserInactive
}
// Check password
if !user.ComparePassword(password) {
logger.WithField("user_id", user.ID).Warn("Login attempt with incorrect password")
return nil, ErrInvalidCredentials
}
// Generate JWT token
token, err := s.GenerateJWTToken(user.ID)
if err != nil {
logger.WithError(err).WithField("user_id", user.ID).Error("Failed to generate JWT token")
return nil, fmt.Errorf("failed to generate token: %w", err)
}
// Log successful login
logger.LogAuthEvent("login", fmt.Sprintf("%d", user.ID), clientIP, true, nil)
return &models.LoginResponse{
Token: token,
User: user.ToSafeUser(),
}, nil
}
// Register creates a new user
func (s *AuthService) Register(req *models.CreateUserRequest) (*models.SafeUser, error) {
// Check if username already exists
var existingUser models.User
if err := s.db.Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
return nil, fmt.Errorf("username already exists")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("failed to check username: %w", err)
}
// Check if email already exists
if err := s.db.Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
return nil, fmt.Errorf("email already exists")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("failed to check email: %w", err)
}
// Create new user
user := models.User{
Username: req.Username,
Email: req.Email,
Password: req.Password,
FirstName: req.FirstName,
LastName: req.LastName,
Role: req.Role,
Active: true,
}
if err := s.db.Create(&user).Error; err != nil {
logger.WithError(err).WithField("username", req.Username).Error("Failed to create user")
return nil, fmt.Errorf("failed to create user: %w", err)
}
// Log user registration
logger.WithField("user_id", user.ID).Info("User registered successfully")
safeUser := user.ToSafeUser()
return &safeUser, nil
}
// GenerateJWTToken generates a JWT token for a user
func (s *AuthService) GenerateJWTToken(userID uint) (string, error) {
// Get JWT configuration
jwtConfig := config.AppConfig.JWT
// Create claims
claims := jwt.MapClaims{
"user_id": userID,
"exp": time.Now().Add(time.Hour * time.Duration(jwtConfig.ExpirationHours)).Unix(),
"iat": time.Now().Unix(),
"iss": jwtConfig.Issuer,
"aud": jwtConfig.Audience,
}
// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Sign token
tokenString, err := token.SignedString(jwtConfig.GetJWTSigningKey())
if err != nil {
return "", fmt.Errorf("failed to sign token: %w", err)
}
return tokenString, nil
}
// ValidateJWTToken validates a JWT token and returns the user ID
func (s *AuthService) ValidateJWTToken(tokenString string) (uint, error) {
// Get JWT configuration
jwtConfig := config.AppConfig.JWT
// Parse token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Validate signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtConfig.GetJWTSigningKey(), nil
})
if err != nil {
return 0, fmt.Errorf("failed to parse token: %w", err)
}
// Validate claims
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
// Check expiration
if exp, ok := claims["exp"].(float64); ok {
if time.Now().Unix() > int64(exp) {
return 0, ErrTokenExpired
}
}
// Get user ID
userID, ok := claims["user_id"].(float64)
if !ok {
return 0, ErrInvalidToken
}
return uint(userID), nil
}
return 0, ErrInvalidToken
}
// GetUserByID returns a user by ID
func (s *AuthService) GetUserByID(userID uint) (*models.User, error) {
var user models.User
if err := s.db.First(&user, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, fmt.Errorf("failed to get user: %w", err)
}
return &user, nil
}
// UpdateUser updates a user
func (s *AuthService) UpdateUser(userID uint, req *models.UpdateUserRequest) (*models.SafeUser, error) {
var user models.User
if err := s.db.First(&user, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, fmt.Errorf("failed to get user: %w", err)
}
// Update fields
if req.FirstName != "" {
user.FirstName = req.FirstName
}
if req.LastName != "" {
user.LastName = req.LastName
}
if req.Email != "" {
user.Email = req.Email
}
if req.Active != nil {
user.Active = *req.Active
}
if req.Role != "" {
user.Role = req.Role
}
if err := s.db.Save(&user).Error; err != nil {
logger.WithError(err).WithField("user_id", userID).Error("Failed to update user")
return nil, fmt.Errorf("failed to update user: %w", err)
}
// Log user update
logger.WithField("user_id", userID).Info("User updated successfully")
safeUser := user.ToSafeUser()
return &safeUser, nil
}
// ChangePassword changes a user's password
func (s *AuthService) ChangePassword(userID uint, req *models.ChangePasswordRequest) error {
var user models.User
if err := s.db.First(&user, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrUserNotFound
}
return fmt.Errorf("failed to get user: %w", err)
}
// Check current password
if !user.ComparePassword(req.CurrentPassword) {
logger.WithField("user_id", userID).Warn("Password change attempt with incorrect current password")
return ErrInvalidCredentials
}
// Update password
user.Password = req.NewPassword
if err := s.db.Save(&user).Error; err != nil {
logger.WithError(err).WithField("user_id", userID).Error("Failed to change password")
return fmt.Errorf("failed to change password: %w", err)
}
// Log password change
logger.WithField("user_id", userID).Info("Password changed successfully")
return nil
}
// HashPassword hashes a password using bcrypt
func HashPassword(password string) (string, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("failed to hash password: %w", err)
}
return string(hashedPassword), nil
}
// CheckPassword checks if a password matches a hashed password
func CheckPassword(password, hashedPassword string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}
// AuthMiddleware returns a gin middleware for authentication
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Get token from Authorization header
tokenString := c.GetHeader("Authorization")
if tokenString == "" {
c.JSON(401, gin.H{"error": "Authorization header is required"})
c.Abort()
return
}
// Remove "Bearer " prefix
if len(tokenString) > 7 && tokenString[:7] == "Bearer " {
tokenString = tokenString[7:]
}
// Validate token
authService := NewAuthService(database.GetDB())
userID, err := authService.ValidateJWTToken(tokenString)
if err != nil {
c.JSON(401, gin.H{"error": "Invalid or expired token"})
c.Abort()
return
}
// Get user
user, err := authService.GetUserByID(userID)
if err != nil {
c.JSON(401, gin.H{"error": "User not found"})
c.Abort()
return
}
// Check if user is active
if !user.Active {
c.JSON(401, gin.H{"error": "User account is inactive"})
c.Abort()
return
}
// Set user ID in context
c.Set("userID", userID)
c.Set("userRole", user.Role)
c.Next()
}
}
// RoleMiddleware returns a gin middleware for role-based authorization
func RoleMiddleware(roles ...string) gin.HandlerFunc {
return func(c *gin.Context) {
// Get user role from context
userRole, exists := c.Get("userRole")
if !exists {
c.JSON(401, gin.H{"error": "User not authenticated"})
c.Abort()
return
}
// Check if user has required role
roleStr, ok := userRole.(string)
if !ok {
c.JSON(500, gin.H{"error": "Invalid user role"})
c.Abort()
return
}
// Check if user role is in allowed roles
allowed := false
for _, role := range roles {
if roleStr == role {
allowed = true
break
}
}
if !allowed {
c.JSON(403, gin.H{"error": "Insufficient permissions"})
c.Abort()
return
}
c.Next()
}
}

View 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
}

View File

@@ -0,0 +1,217 @@
package database
import (
"fmt"
"log"
"os"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"customer-support-system/internal/models"
)
var DB *gorm.DB
// Connect initializes the database connection
func Connect() error {
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=%s",
getEnv("DB_HOST", "localhost"),
getEnv("DB_USER", "postgres"),
getEnv("DB_PASSWORD", "postgres"),
getEnv("DB_NAME", "support"),
getEnv("DB_PORT", "5432"),
getEnv("DB_SSLMODE", "disable"),
getEnv("DB_TIMEZONE", "UTC"),
)
newLogger := logger.New(
log.New(os.Stdout, "\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second,
LogLevel: logger.Info,
Colorful: false,
},
)
var err error
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
log.Println("Database connected successfully")
return nil
}
// AutoMigrate runs the database migrations
func AutoMigrate() error {
err := DB.AutoMigrate(
&models.User{},
&models.Conversation{},
&models.Message{},
&models.KnowledgeBase{},
&models.KnowledgeBaseFeedback{},
&models.AIModel{},
&models.AIInteraction{},
&models.AIFallback{},
)
if err != nil {
return fmt.Errorf("failed to run database migrations: %w", err)
}
log.Println("Database migrations completed successfully")
return nil
}
// Close closes the database connection
func Close() error {
sqlDB, err := DB.DB()
if err != nil {
return fmt.Errorf("failed to get database instance: %w", err)
}
err = sqlDB.Close()
if err != nil {
return fmt.Errorf("failed to close database connection: %w", err)
}
log.Println("Database connection closed successfully")
return nil
}
// GetDB returns the database instance
func GetDB() *gorm.DB {
return DB
}
// getEnv gets an environment variable with a default value
func getEnv(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return defaultValue
}
// SeedDatabase seeds the database with initial data
func SeedDatabase() error {
// Create default admin user if not exists
var userCount int64
if err := DB.Model(&models.User{}).Where("role = ?", "admin").Count(&userCount).Error; err != nil {
return fmt.Errorf("failed to count admin users: %w", err)
}
if userCount == 0 {
adminUser := models.User{
Username: "admin",
Email: "admin@example.com",
Password: "admin123",
FirstName: "Admin",
LastName: "User",
Role: "admin",
Active: true,
}
if err := DB.Create(&adminUser).Error; err != nil {
return fmt.Errorf("failed to create admin user: %w", err)
}
log.Println("Default admin user created")
}
// Create default AI models if not exists
var aiModelCount int64
if err := DB.Model(&models.AIModel{}).Count(&aiModelCount).Error; err != nil {
return fmt.Errorf("failed to count AI models: %w", err)
}
if aiModelCount == 0 {
// Create OpenAI GPT-4 model
openAIModel := models.AIModel{
Name: "OpenAI GPT-4",
Type: "openai",
Model: "gpt-4",
MaxTokens: 4000,
Temperature: 0.7,
TopP: 1.0,
Active: true,
Priority: 2,
Description: "OpenAI's GPT-4 model for complex queries",
}
if err := DB.Create(&openAIModel).Error; err != nil {
return fmt.Errorf("failed to create OpenAI model: %w", err)
}
// Create Local LLaMA model
localModel := models.AIModel{
Name: "Local LLaMA",
Type: "local",
Model: "llama2",
Endpoint: "http://localhost:11434",
MaxTokens: 2000,
Temperature: 0.7,
TopP: 1.0,
Active: true,
Priority: 1,
Description: "Local LLaMA model for simple queries",
}
if err := DB.Create(&localModel).Error; err != nil {
return fmt.Errorf("failed to create local model: %w", err)
}
log.Println("Default AI models created")
}
// Create sample knowledge base entries if not exists
var kbCount int64
if err := DB.Model(&models.KnowledgeBase{}).Count(&kbCount).Error; err != nil {
return fmt.Errorf("failed to count knowledge base entries: %w", err)
}
if kbCount == 0 {
sampleEntries := []models.KnowledgeBase{
{
Question: "How do I reset my password?",
Answer: "To reset your password, click on the 'Forgot Password' link on the login page. Enter your email address and follow the instructions sent to your inbox.",
Category: "Account",
Tags: "password,reset,account",
Priority: 5,
Active: true,
},
{
Question: "What payment methods do you accept?",
Answer: "We accept all major credit cards including Visa, Mastercard, and American Express. We also support payments through PayPal and bank transfers.",
Category: "Billing",
Tags: "payment,billing,methods",
Priority: 4,
Active: true,
},
{
Question: "How can I contact customer support?",
Answer: "You can contact our customer support team through the chat feature on our website, by emailing support@example.com, or by calling our toll-free number at 1-800-123-4567.",
Category: "Support",
Tags: "contact,support,help",
Priority: 3,
Active: true,
},
}
for _, entry := range sampleEntries {
if err := DB.Create(&entry).Error; err != nil {
return fmt.Errorf("failed to create knowledge base entry: %w", err)
}
}
log.Println("Sample knowledge base entries created")
}
return nil
}

View File

@@ -0,0 +1,211 @@
package handlers
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"customer-support-system/internal/ai"
"customer-support-system/internal/models"
"customer-support-system/pkg/logger"
)
// AIHandler handles AI-related HTTP requests
type AIHandler struct {
aiService *ai.AIService
}
// NewAIHandler creates a new AI handler
func NewAIHandler() *AIHandler {
return &AIHandler{
aiService: ai.NewAIService(),
}
}
// QueryAI handles querying the AI service
func (h *AIHandler) QueryAI(c *gin.Context) {
// Check if user is authenticated
_, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
var req struct {
Prompt string `json:"prompt" binding:"required"`
ConversationHistory []models.Message `json:"conversationHistory"`
Complexity int `json:"complexity"`
}
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse query AI request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// If complexity is not provided, analyze it
if req.Complexity == 0 {
req.Complexity = h.aiService.AnalyzeComplexity(req.Prompt)
}
// Query AI
ctx := context.Background()
response, err := h.aiService.Query(ctx, req.Prompt, req.ConversationHistory, req.Complexity)
if err != nil {
logger.WithError(err).Error("Failed to query AI")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to query AI",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "AI query successful",
"data": gin.H{
"response": response,
"complexity": req.Complexity,
},
})
}
// AnalyzeComplexity handles analyzing the complexity of a prompt
func (h *AIHandler) AnalyzeComplexity(c *gin.Context) {
var req struct {
Prompt string `json:"prompt" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse analyze complexity request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// Analyze complexity
complexity := h.aiService.AnalyzeComplexity(req.Prompt)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Complexity analysis successful",
"data": gin.H{
"complexity": complexity,
},
})
}
// GetAvailableModels handles getting available AI models
func (h *AIHandler) GetAvailableModels(c *gin.Context) {
// Get available models
models := h.aiService.GetAvailableModels()
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"models": models,
},
})
}
// QueryOpenAI handles querying the OpenAI API
func (h *AIHandler) QueryOpenAI(c *gin.Context) {
// Check if user is authenticated
_, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
var req struct {
Prompt string `json:"prompt" binding:"required"`
ConversationHistory []models.Message `json:"conversationHistory"`
}
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse query OpenAI request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// Query OpenAI
ctx := context.Background()
response, err := h.aiService.QueryOpenAI(ctx, req.Prompt, req.ConversationHistory)
if err != nil {
logger.WithError(err).Error("Failed to query OpenAI")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to query OpenAI",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "OpenAI query successful",
"data": gin.H{
"response": response,
},
})
}
// QueryOllama handles querying the Ollama API
func (h *AIHandler) QueryOllama(c *gin.Context) {
// Check if user is authenticated
_, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
var req struct {
Prompt string `json:"prompt" binding:"required"`
ConversationHistory []models.Message `json:"conversationHistory"`
}
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse query Ollama request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// Query Ollama
ctx := context.Background()
response, err := h.aiService.QueryOllama(ctx, req.Prompt, req.ConversationHistory)
if err != nil {
logger.WithError(err).Error("Failed to query Ollama")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to query Ollama",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Ollama query successful",
"data": gin.H{
"response": response,
},
})
}

View File

@@ -0,0 +1,558 @@
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"customer-support-system/internal/conversation"
"customer-support-system/internal/models"
"customer-support-system/pkg/logger"
)
// ConversationHandler handles conversation-related HTTP requests
type ConversationHandler struct {
conversationService *conversation.ConversationService
}
// NewConversationHandler creates a new conversation handler
func NewConversationHandler() *ConversationHandler {
return &ConversationHandler{
conversationService: conversation.NewConversationService(),
}
}
// CreateConversation handles creating a new conversation
func (h *ConversationHandler) CreateConversation(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
var req models.CreateConversationRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse create conversation request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// Create conversation
conv, err := h.conversationService.CreateConversation(userID.(uint), &req)
if err != nil {
logger.WithError(err).Error("Failed to create conversation")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to create conversation",
})
return
}
c.JSON(http.StatusCreated, gin.H{
"success": true,
"message": "Conversation created successfully",
"data": gin.H{
"conversation": conv,
},
})
}
// GetConversation handles getting a conversation by ID
func (h *ConversationHandler) GetConversation(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
// Get conversation ID from URL params
conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
logger.WithError(err).Error("Failed to parse conversation ID")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid conversation ID",
})
return
}
// Get conversation
conv, err := h.conversationService.GetConversation(uint(conversationID), userID.(uint))
if err != nil {
logger.WithError(err).Error("Failed to get conversation")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get conversation",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"conversation": conv,
},
})
}
// ListConversations handles listing conversations for a user
func (h *ConversationHandler) ListConversations(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
// Parse query parameters
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
status := c.DefaultQuery("status", "")
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
// List conversations
response, err := h.conversationService.ListConversations(userID.(uint), page, pageSize, status)
if err != nil {
logger.WithError(err).Error("Failed to list conversations")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to list conversations",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"conversations": response.Conversations,
"total": response.Total,
"page": response.Page,
"pageSize": response.PageSize,
},
})
}
// UpdateConversation handles updating a conversation
func (h *ConversationHandler) UpdateConversation(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
// Get conversation ID from URL params
conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
logger.WithError(err).Error("Failed to parse conversation ID")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid conversation ID",
})
return
}
var req models.UpdateConversationRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse update conversation request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// Update conversation
conv, err := h.conversationService.UpdateConversation(uint(conversationID), userID.(uint), &req)
if err != nil {
logger.WithError(err).Error("Failed to update conversation")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to update conversation",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Conversation updated successfully",
"data": gin.H{
"conversation": conv,
},
})
}
// DeleteConversation handles deleting a conversation
func (h *ConversationHandler) DeleteConversation(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
// Get conversation ID from URL params
conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
logger.WithError(err).Error("Failed to parse conversation ID")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid conversation ID",
})
return
}
// Delete conversation
err = h.conversationService.DeleteConversation(uint(conversationID), userID.(uint))
if err != nil {
logger.WithError(err).Error("Failed to delete conversation")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to delete conversation",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Conversation deleted successfully",
})
}
// CreateMessage handles creating a new message in a conversation
func (h *ConversationHandler) CreateMessage(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
// Get conversation ID from URL params
conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
logger.WithError(err).Error("Failed to parse conversation ID")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid conversation ID",
})
return
}
var req models.CreateMessageRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse create message request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// Create message
message, err := h.conversationService.CreateMessage(uint(conversationID), userID.(uint), &req)
if err != nil {
logger.WithError(err).Error("Failed to create message")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to create message",
})
return
}
c.JSON(http.StatusCreated, gin.H{
"success": true,
"message": "Message created successfully",
"data": gin.H{
"message": message,
},
})
}
// GetMessages handles getting messages in a conversation
func (h *ConversationHandler) GetMessages(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
// Get conversation ID from URL params
conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
logger.WithError(err).Error("Failed to parse conversation ID")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid conversation ID",
})
return
}
// Parse query parameters
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
// Get messages
response, err := h.conversationService.GetMessages(uint(conversationID), userID.(uint), page, pageSize)
if err != nil {
logger.WithError(err).Error("Failed to get messages")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get messages",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"messages": response.Messages,
"total": response.Total,
"page": response.Page,
"pageSize": response.PageSize,
},
})
}
// UpdateMessage handles updating a message
func (h *ConversationHandler) UpdateMessage(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
// Get message ID from URL params
messageID, err := strconv.ParseUint(c.Param("messageId"), 10, 32)
if err != nil {
logger.WithError(err).Error("Failed to parse message ID")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid message ID",
})
return
}
var req models.UpdateMessageRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse update message request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// Update message
message, err := h.conversationService.UpdateMessage(uint(messageID), userID.(uint), &req)
if err != nil {
logger.WithError(err).Error("Failed to update message")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to update message",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Message updated successfully",
"data": gin.H{
"message": message,
},
})
}
// DeleteMessage handles deleting a message
func (h *ConversationHandler) DeleteMessage(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
// Get message ID from URL params
messageID, err := strconv.ParseUint(c.Param("messageId"), 10, 32)
if err != nil {
logger.WithError(err).Error("Failed to parse message ID")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid message ID",
})
return
}
// Delete message
err = h.conversationService.DeleteMessage(uint(messageID), userID.(uint))
if err != nil {
logger.WithError(err).Error("Failed to delete message")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to delete message",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Message deleted successfully",
})
}
// SendMessageWithAI handles sending a message and getting an AI response
func (h *ConversationHandler) SendMessageWithAI(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
// Get conversation ID from URL params
conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
logger.WithError(err).Error("Failed to parse conversation ID")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid conversation ID",
})
return
}
var req struct {
Content string `json:"content" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse send message request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// Send message and get AI response
userMessage, aiMessage, err := h.conversationService.SendMessageWithAI(uint(conversationID), userID.(uint), req.Content)
if err != nil {
logger.WithError(err).Error("Failed to send message with AI")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to send message with AI",
})
return
}
c.JSON(http.StatusCreated, gin.H{
"success": true,
"message": "Message sent and AI response received",
"data": gin.H{
"userMessage": userMessage,
"aiMessage": aiMessage,
},
})
}
// GetConversationStats handles getting statistics for a conversation
func (h *ConversationHandler) GetConversationStats(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
// Get conversation ID from URL params
conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
logger.WithError(err).Error("Failed to parse conversation ID")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid conversation ID",
})
return
}
// Get conversation stats
stats, err := h.conversationService.GetConversationStats(uint(conversationID), userID.(uint))
if err != nil {
logger.WithError(err).Error("Failed to get conversation stats")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get conversation stats",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"stats": stats,
},
})
}

View File

@@ -0,0 +1,475 @@
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"customer-support-system/internal/knowledge"
"customer-support-system/internal/models"
"customer-support-system/pkg/logger"
)
// KnowledgeHandler handles knowledge base-related HTTP requests
type KnowledgeHandler struct {
knowledgeService *knowledge.KnowledgeService
}
// NewKnowledgeHandler creates a new knowledge handler
func NewKnowledgeHandler() *KnowledgeHandler {
return &KnowledgeHandler{
knowledgeService: knowledge.NewKnowledgeService(),
}
}
// CreateKnowledgeEntry handles creating a new knowledge base entry
func (h *KnowledgeHandler) CreateKnowledgeEntry(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
var req models.CreateKnowledgeBaseRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse create knowledge entry request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// Create knowledge entry
entry, err := h.knowledgeService.CreateKnowledgeEntry(userID.(uint), &req)
if err != nil {
logger.WithError(err).Error("Failed to create knowledge entry")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to create knowledge entry",
})
return
}
c.JSON(http.StatusCreated, gin.H{
"success": true,
"message": "Knowledge entry created successfully",
"data": gin.H{
"entry": entry,
},
})
}
// GetKnowledgeEntry handles getting a knowledge base entry by ID
func (h *KnowledgeHandler) GetKnowledgeEntry(c *gin.Context) {
// Get knowledge entry ID from URL params
entryID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
logger.WithError(err).Error("Failed to parse knowledge entry ID")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid knowledge entry ID",
})
return
}
// Get knowledge entry
entry, err := h.knowledgeService.GetKnowledgeEntry(uint(entryID))
if err != nil {
logger.WithError(err).Error("Failed to get knowledge entry")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get knowledge entry",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"entry": entry,
},
})
}
// ListKnowledgeEntries handles listing knowledge base entries
func (h *KnowledgeHandler) ListKnowledgeEntries(c *gin.Context) {
// Parse query parameters
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
category := c.DefaultQuery("category", "")
activeStr := c.DefaultQuery("active", "true")
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
// Parse active parameter
var active *bool
if activeStr != "" {
activeVal := activeStr == "true"
active = &activeVal
}
// List knowledge entries
response, err := h.knowledgeService.ListKnowledgeEntries(page, pageSize, category, active)
if err != nil {
logger.WithError(err).Error("Failed to list knowledge entries")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to list knowledge entries",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"entries": response.Entries,
"total": response.Total,
"page": response.Page,
"pageSize": response.PageSize,
},
})
}
// UpdateKnowledgeEntry handles updating a knowledge base entry
func (h *KnowledgeHandler) UpdateKnowledgeEntry(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
// Get knowledge entry ID from URL params
entryID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
logger.WithError(err).Error("Failed to parse knowledge entry ID")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid knowledge entry ID",
})
return
}
var req models.UpdateKnowledgeBaseRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse update knowledge entry request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// Update knowledge entry
entry, err := h.knowledgeService.UpdateKnowledgeEntry(uint(entryID), userID.(uint), &req)
if err != nil {
logger.WithError(err).Error("Failed to update knowledge entry")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to update knowledge entry",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Knowledge entry updated successfully",
"data": gin.H{
"entry": entry,
},
})
}
// DeleteKnowledgeEntry handles deleting a knowledge base entry
func (h *KnowledgeHandler) DeleteKnowledgeEntry(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
// Get knowledge entry ID from URL params
entryID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
logger.WithError(err).Error("Failed to parse knowledge entry ID")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid knowledge entry ID",
})
return
}
// Delete knowledge entry
err = h.knowledgeService.DeleteKnowledgeEntry(uint(entryID), userID.(uint))
if err != nil {
logger.WithError(err).Error("Failed to delete knowledge entry")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to delete knowledge entry",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Knowledge entry deleted successfully",
})
}
// SearchKnowledge handles searching knowledge base entries
func (h *KnowledgeHandler) SearchKnowledge(c *gin.Context) {
// Parse query parameters
query := c.DefaultQuery("query", "")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
category := c.DefaultQuery("category", "")
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
// Search knowledge entries
response, err := h.knowledgeService.SearchKnowledge(query, page, pageSize, category)
if err != nil {
logger.WithError(err).Error("Failed to search knowledge entries")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to search knowledge entries",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"results": response.Results,
"total": response.Total,
"query": response.Query,
"page": response.Page,
"pageSize": response.PageSize,
},
})
}
// GetCategories handles getting all unique categories in the knowledge base
func (h *KnowledgeHandler) GetCategories(c *gin.Context) {
// Get categories
categories, err := h.knowledgeService.GetCategories()
if err != nil {
logger.WithError(err).Error("Failed to get knowledge categories")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get knowledge categories",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"categories": categories,
},
})
}
// GetTags handles getting all unique tags in the knowledge base
func (h *KnowledgeHandler) GetTags(c *gin.Context) {
// Get tags
tags, err := h.knowledgeService.GetTags()
if err != nil {
logger.WithError(err).Error("Failed to get knowledge tags")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get knowledge tags",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"tags": tags,
},
})
}
// RateKnowledgeEntry handles rating a knowledge base entry as helpful or not
func (h *KnowledgeHandler) RateKnowledgeEntry(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
// Get knowledge entry ID from URL params
entryID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
logger.WithError(err).Error("Failed to parse knowledge entry ID")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid knowledge entry ID",
})
return
}
var req models.CreateKnowledgeBaseFeedbackRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse rate knowledge entry request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// Rate knowledge entry
err = h.knowledgeService.RateKnowledgeEntry(uint(entryID), userID.(uint), req.Helpful, req.Comment)
if err != nil {
logger.WithError(err).Error("Failed to rate knowledge entry")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to rate knowledge entry",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Knowledge entry rated successfully",
})
}
// GetPopularKnowledge handles getting popular knowledge base entries
func (h *KnowledgeHandler) GetPopularKnowledge(c *gin.Context) {
// Parse query parameters
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
if limit < 1 || limit > 100 {
limit = 10
}
// Get popular knowledge entries
entries, err := h.knowledgeService.GetPopularKnowledge(limit)
if err != nil {
logger.WithError(err).Error("Failed to get popular knowledge entries")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get popular knowledge entries",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"entries": entries,
},
})
}
// GetRecentKnowledge handles getting recent knowledge base entries
func (h *KnowledgeHandler) GetRecentKnowledge(c *gin.Context) {
// Parse query parameters
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
if limit < 1 || limit > 100 {
limit = 10
}
// Get recent knowledge entries
entries, err := h.knowledgeService.GetRecentKnowledge(limit)
if err != nil {
logger.WithError(err).Error("Failed to get recent knowledge entries")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get recent knowledge entries",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"entries": entries,
},
})
}
// FindBestMatch handles finding the best matching knowledge base entry for a query
func (h *KnowledgeHandler) FindBestMatch(c *gin.Context) {
// Parse query parameters
query := c.DefaultQuery("query", "")
if query == "" {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Query parameter is required",
})
return
}
// Find best match
entry, err := h.knowledgeService.FindBestMatch(query)
if err != nil {
logger.WithError(err).Error("Failed to find best match")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to find best match",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"entry": entry,
},
})
}
// GetKnowledgeStats handles getting statistics for the knowledge base
func (h *KnowledgeHandler) GetKnowledgeStats(c *gin.Context) {
// Get knowledge stats
stats, err := h.knowledgeService.GetKnowledgeStats()
if err != nil {
logger.WithError(err).Error("Failed to get knowledge stats")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get knowledge stats",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"stats": stats,
},
})
}

View File

@@ -0,0 +1,453 @@
package handlers
import (
"fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"customer-support-system/internal/auth"
"customer-support-system/internal/database"
"customer-support-system/internal/models"
"customer-support-system/pkg/logger"
)
// UserHandler handles user-related HTTP requests
type UserHandler struct {
authService *auth.AuthService
}
// NewUserHandler creates a new user handler
func NewUserHandler() *UserHandler {
return &UserHandler{
authService: auth.NewAuthService(database.GetDB()),
}
}
// Register handles user registration
func (h *UserHandler) Register(c *gin.Context) {
var req models.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse register request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// Validate request
if err := validateRegisterRequest(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": err.Error(),
})
return
}
// Create user
user, err := h.authService.Register(&req)
if err != nil {
logger.WithError(err).Error("Failed to register user")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to register user",
})
return
}
c.JSON(http.StatusCreated, gin.H{
"success": true,
"message": "User registered successfully",
"data": gin.H{
"user": user,
},
})
}
// Login handles user login
func (h *UserHandler) Login(c *gin.Context) {
var req models.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse login request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// Validate request
if err := validateLoginRequest(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": err.Error(),
})
return
}
// Get client IP
clientIP := c.ClientIP()
// Authenticate user
response, err := h.authService.Login(req.Username, req.Password, clientIP)
if err != nil {
logger.WithError(err).Error("Failed to login user")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Invalid username or password",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "User logged in successfully",
"data": gin.H{
"user": response.User,
"token": response.Token,
},
})
}
// GetProfile handles getting user profile
func (h *UserHandler) GetProfile(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
// Get user profile
user, err := h.authService.GetUserByID(userID.(uint))
if err != nil {
logger.WithError(err).Error("Failed to get user profile")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get user profile",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"user": user.ToSafeUser(),
},
})
}
// UpdateProfile handles updating user profile
func (h *UserHandler) UpdateProfile(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
var req models.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse update profile request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// Update user profile
user, err := h.authService.UpdateUser(userID.(uint), &req)
if err != nil {
logger.WithError(err).Error("Failed to update user profile")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to update user profile",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "User profile updated successfully",
"data": gin.H{
"user": user,
},
})
}
// ChangePassword handles changing user password
func (h *UserHandler) ChangePassword(c *gin.Context) {
// Get user ID from context
userID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
var req models.ChangePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse change password request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// Validate request
if err := validateChangePasswordRequest(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": err.Error(),
})
return
}
// Change password
err := h.authService.ChangePassword(userID.(uint), &req)
if err != nil {
logger.WithError(err).Error("Failed to change password")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to change password",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Password changed successfully",
})
}
// validateRegisterRequest validates the register request
func validateRegisterRequest(req *models.CreateUserRequest) error {
if req.Username == "" {
return fmt.Errorf("username is required")
}
if req.Password == "" {
return fmt.Errorf("password is required")
}
if req.Email == "" {
return fmt.Errorf("email is required")
}
return nil
}
// validateLoginRequest validates the login request
func validateLoginRequest(req *models.LoginRequest) error {
if req.Username == "" {
return fmt.Errorf("username is required")
}
if req.Password == "" {
return fmt.Errorf("password is required")
}
return nil
}
// validateChangePasswordRequest validates the change password request
func validateChangePasswordRequest(req *models.ChangePasswordRequest) error {
if req.CurrentPassword == "" {
return fmt.Errorf("current password is required")
}
if req.NewPassword == "" {
return fmt.Errorf("new password is required")
}
return nil
}
// AdminGetUsers handles getting all users (admin only)
func (h *UserHandler) AdminGetUsers(c *gin.Context) {
// Parse query parameters
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
// Get users
var users []models.User
var total int64
db := database.GetDB()
// Get total count
if err := db.Model(&models.User{}).Count(&total).Error; err != nil {
logger.WithError(err).Error("Failed to count users")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to count users",
})
return
}
// Get paginated results
offset := (page - 1) * pageSize
if err := db.Offset(offset).Limit(pageSize).Find(&users).Error; err != nil {
logger.WithError(err).Error("Failed to get users")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get users",
})
return
}
// Convert to safe users
safeUsers := make([]models.SafeUser, len(users))
for i, user := range users {
safeUsers[i] = user.ToSafeUser()
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"users": safeUsers,
"total": total,
"page": page,
"pageSize": pageSize,
},
})
}
// AdminGetUser handles getting a user by ID (admin only)
func (h *UserHandler) AdminGetUser(c *gin.Context) {
// Get target user ID from URL params
targetUserID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
logger.WithError(err).Error("Failed to parse user ID")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid user ID",
})
return
}
// Get user
user, err := h.authService.GetUserByID(uint(targetUserID))
if err != nil {
logger.WithError(err).Error("Failed to get user")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get user",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"user": user.ToSafeUser(),
},
})
}
// AdminUpdateUser handles updating a user (admin only)
func (h *UserHandler) AdminUpdateUser(c *gin.Context) {
// Get target user ID from URL params
targetUserID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
logger.WithError(err).Error("Failed to parse user ID")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid user ID",
})
return
}
var req models.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.WithError(err).Error("Failed to parse update user request")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
})
return
}
// Update user
updatedUser, err := h.authService.UpdateUser(uint(targetUserID), &req)
if err != nil {
logger.WithError(err).Error("Failed to update user")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to update user",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "User updated successfully",
"data": gin.H{
"user": updatedUser,
},
})
}
// AdminDeleteUser handles deleting a user (admin only)
func (h *UserHandler) AdminDeleteUser(c *gin.Context) {
// Get target user ID from URL params
targetUserID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
logger.WithError(err).Error("Failed to parse user ID")
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid user ID",
})
return
}
// Get current user ID from context
currentUserID, exists := c.Get("userID")
if !exists {
logger.Error("Failed to get current user ID from context")
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Unauthorized",
})
return
}
// Prevent self-deletion
if currentUserID.(uint) == uint(targetUserID) {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Cannot delete your own account",
})
return
}
// Delete user
db := database.GetDB()
if err := db.Delete(&models.User{}, uint(targetUserID)).Error; err != nil {
logger.WithError(err).Error("Failed to delete user")
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to delete user",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "User deleted successfully",
})
}

View File

@@ -0,0 +1,553 @@
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
}

View File

@@ -0,0 +1,127 @@
package models
import (
"time"
"gorm.io/gorm"
)
// AIModel represents an AI model configuration
type AIModel struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"not null"`
Type string `json:"type" gorm:"not null"` // 'openai', 'local', 'custom'
Endpoint string `json:"endpoint"`
APIKey string `json:"apiKey"` // Encrypted in database
Model string `json:"model"` // e.g., 'gpt-4', 'llama2', etc.
MaxTokens int `json:"maxTokens" gorm:"default:1000"`
Temperature float64 `json:"temperature" gorm:"default:0.7"`
TopP float64 `json:"topP" gorm:"default:1.0"`
Active bool `json:"active" gorm:"default:true"`
Priority int `json:"priority" gorm:"default:1"` // Higher number = higher priority
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
// AIInteraction represents an interaction with an AI model
type AIInteraction struct {
ID uint `json:"id" gorm:"primaryKey"`
ConversationID uint `json:"conversationId"`
MessageID uint `json:"messageId"`
AIModelID uint `json:"aiModelId"`
Prompt string `json:"prompt" gorm:"not null"`
Response string `json:"response" gorm:"not null"`
TokensUsed int `json:"tokensUsed"`
ResponseTime int64 `json:"responseTime"` // in milliseconds
Cost float64 `json:"cost"`
Success bool `json:"success"`
ErrorMessage string `json:"errorMessage"`
CreatedAt time.Time `json:"createdAt"`
// Relationships
Conversation Conversation `json:"conversation" gorm:"foreignKey:ConversationID"`
Message Message `json:"message" gorm:"foreignKey:MessageID"`
AIModel AIModel `json:"aiModel" gorm:"foreignKey:AIModelID"`
}
// AIFallback represents a fallback event when an AI model fails
type AIFallback struct {
ID uint `json:"id" gorm:"primaryKey"`
ConversationID uint `json:"conversationId"`
MessageID uint `json:"messageId"`
FromAIModelID uint `json:"fromAiModelId"`
ToAIModelID uint `json:"toAiModelId"`
Reason string `json:"reason" gorm:"not null"`
CreatedAt time.Time `json:"createdAt"`
// Relationships
Conversation Conversation `json:"conversation" gorm:"foreignKey:ConversationID"`
Message Message `json:"message" gorm:"foreignKey:MessageID"`
FromAIModel AIModel `json:"fromAiModel" gorm:"foreignKey:FromAIModelID"`
ToAIModel AIModel `json:"toAiModel" gorm:"foreignKey:ToAIModelID"`
}
// BeforeCreate is a GORM hook that validates the AI model before creation
func (a *AIModel) BeforeCreate(tx *gorm.DB) error {
if a.Priority < 1 {
a.Priority = 1
}
if a.MaxTokens < 1 {
a.MaxTokens = 1000
}
if a.Temperature < 0 {
a.Temperature = 0
} else if a.Temperature > 2 {
a.Temperature = 2
}
if a.TopP < 0 {
a.TopP = 0
} else if a.TopP > 1 {
a.TopP = 1
}
return nil
}
// BeforeUpdate is a GORM hook that validates the AI model before update
func (a *AIModel) BeforeUpdate(tx *gorm.DB) error {
if a.Priority < 1 {
a.Priority = 1
}
if a.MaxTokens < 1 {
a.MaxTokens = 1000
}
if a.Temperature < 0 {
a.Temperature = 0
} else if a.Temperature > 2 {
a.Temperature = 2
}
if a.TopP < 0 {
a.TopP = 0
} else if a.TopP > 1 {
a.TopP = 1
}
return nil
}
// IsActive checks if the AI model is active
func (a *AIModel) IsActive() bool {
return a.Active
}
// IsOpenAI checks if the AI model is an OpenAI model
func (a *AIModel) IsOpenAI() bool {
return a.Type == "openai"
}
// IsLocal checks if the AI model is a local model
func (a *AIModel) IsLocal() bool {
return a.Type == "local"
}
// IsCustom checks if the AI model is a custom model
func (a *AIModel) IsCustom() bool {
return a.Type == "custom"
}

View File

@@ -0,0 +1,63 @@
package models
import (
"time"
"gorm.io/gorm"
)
// Conversation represents a chat conversation between users and/or agents
type Conversation struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title"`
UserID uint `json:"userId"`
AgentID *uint `json:"agentId,omitempty"`
Status string `json:"status" gorm:"default:'active'"` // 'active', 'closed', 'escalated'
Department string `json:"department"`
Priority string `json:"priority" gorm:"default:'medium'"` // 'low', 'medium', 'high', 'urgent'
Tags string `json:"tags"` // Comma-separated tags
LastMessageAt time.Time `json:"lastMessageAt"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
// Relationships
User User `json:"user" gorm:"foreignKey:UserID"`
Agent *User `json:"agent,omitempty" gorm:"foreignKey:AgentID"`
Messages []Message `json:"messages" gorm:"foreignKey:ConversationID"`
}
// Message represents a single message in a conversation
type Message struct {
ID uint `json:"id" gorm:"primaryKey"`
ConversationID uint `json:"conversationId"`
UserID uint `json:"userId"`
Content string `json:"content" gorm:"not null"`
Type string `json:"type" gorm:"default:'text'"` // 'text', 'image', 'file', 'system'
Status string `json:"status" gorm:"default:'sent'"` // 'sent', 'delivered', 'read'
Sentiment float64 `json:"sentiment"` // Sentiment score from -1 (negative) to 1 (positive)
IsAI bool `json:"isAI" gorm:"default:false"`
AIModel string `json:"aiModel,omitempty"` // Which AI model generated this message
ReadAt *time.Time `json:"readAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
// Relationships
Conversation Conversation `json:"conversation" gorm:"foreignKey:ConversationID"`
User User `json:"user" gorm:"foreignKey:UserID"`
}
// BeforeCreate is a GORM hook that sets the LastMessageAt field when creating a conversation
func (c *Conversation) BeforeCreate(tx *gorm.DB) error {
c.LastMessageAt = time.Now()
return nil
}
// BeforeCreate is a GORM hook that updates the conversation's LastMessageAt when creating a message
func (m *Message) BeforeCreate(tx *gorm.DB) error {
// Update the conversation's LastMessageAt
tx.Model(&Conversation{}).Where("id = ?", m.ConversationID).Update("last_message_at", time.Now())
return nil
}

View File

@@ -0,0 +1,85 @@
package models
import (
"time"
"gorm.io/gorm"
)
// KnowledgeBase represents a knowledge base entry (FAQ)
type KnowledgeBase struct {
ID uint `json:"id" gorm:"primaryKey"`
Question string `json:"question" gorm:"not null"`
Answer string `json:"answer" gorm:"not null"`
Category string `json:"category" gorm:"not null"`
Tags string `json:"tags"` // Comma-separated tags
Priority int `json:"priority" gorm:"default:1"` // Higher number = higher priority
ViewCount int `json:"viewCount" gorm:"default:0"`
Helpful int `json:"helpful" gorm:"default:0"` // Number of times marked as helpful
NotHelpful int `json:"notHelpful" gorm:"default:0"` // Number of times marked as not helpful
Active bool `json:"active" gorm:"default:true"`
CreatedBy uint `json:"createdBy"`
UpdatedBy uint `json:"updatedBy"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
// Relationships
Creator User `json:"creator" gorm:"foreignKey:CreatedBy"`
Updater User `json:"updater" gorm:"foreignKey:UpdatedBy"`
}
// KnowledgeBaseFeedback represents user feedback on a knowledge base entry
type KnowledgeBaseFeedback struct {
ID uint `json:"id" gorm:"primaryKey"`
KnowledgeBaseID uint `json:"knowledgeBaseId" gorm:"not null"`
UserID uint `json:"userId" gorm:"not null"`
Helpful bool `json:"helpful"` // true if helpful, false if not helpful
Comment string `json:"comment"`
CreatedAt time.Time `json:"createdAt"`
// Relationships
KnowledgeBase KnowledgeBase `json:"knowledgeBase" gorm:"foreignKey:KnowledgeBaseID"`
User User `json:"user" gorm:"foreignKey:UserID"`
}
// BeforeCreate is a GORM hook that validates the knowledge base entry before creation
func (k *KnowledgeBase) BeforeCreate(tx *gorm.DB) error {
if k.Priority < 1 {
k.Priority = 1
}
return nil
}
// BeforeUpdate is a GORM hook that validates the knowledge base entry before update
func (k *KnowledgeBase) BeforeUpdate(tx *gorm.DB) error {
if k.Priority < 1 {
k.Priority = 1
}
return nil
}
// IncrementViewCount increments the view count of a knowledge base entry
func (k *KnowledgeBase) IncrementViewCount(tx *gorm.DB) error {
return tx.Model(k).UpdateColumn("view_count", gorm.Expr("view_count + ?", 1)).Error
}
// MarkHelpful marks a knowledge base entry as helpful
func (k *KnowledgeBase) MarkHelpful(tx *gorm.DB) error {
return tx.Model(k).UpdateColumn("helpful", gorm.Expr("helpful + ?", 1)).Error
}
// MarkNotHelpful marks a knowledge base entry as not helpful
func (k *KnowledgeBase) MarkNotHelpful(tx *gorm.DB) error {
return tx.Model(k).UpdateColumn("not_helpful", gorm.Expr("not_helpful + ?", 1)).Error
}
// GetHelpfulnessPercentage returns the helpfulness percentage
func (k *KnowledgeBase) GetHelpfulnessPercentage() float64 {
total := k.Helpful + k.NotHelpful
if total == 0 {
return 0
}
return float64(k.Helpful) / float64(total) * 100
}

View File

@@ -0,0 +1,143 @@
package models
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUserModel(t *testing.T) {
// Test user model fields
user := User{
ID: 1,
Username: "testuser",
Email: "test@example.com",
Password: "hashedpassword",
FirstName: "Test",
LastName: "User",
Role: "user",
Active: true,
}
// Verify user fields
assert.Equal(t, uint(1), user.ID)
assert.Equal(t, "testuser", user.Username)
assert.Equal(t, "test@example.com", user.Email)
assert.Equal(t, "hashedpassword", user.Password)
assert.Equal(t, "Test", user.FirstName)
assert.Equal(t, "User", user.LastName)
assert.Equal(t, "user", user.Role)
assert.True(t, user.Active)
}
func TestConversationModel(t *testing.T) {
// Test conversation model fields
conversation := Conversation{
ID: 1,
UserID: 1,
Title: "Test Conversation",
Status: "active",
Department: "support",
Priority: "medium",
Tags: "test,sample",
}
// Verify conversation fields
assert.Equal(t, uint(1), conversation.ID)
assert.Equal(t, uint(1), conversation.UserID)
assert.Equal(t, "Test Conversation", conversation.Title)
assert.Equal(t, "active", conversation.Status)
assert.Equal(t, "support", conversation.Department)
assert.Equal(t, "medium", conversation.Priority)
assert.Equal(t, "test,sample", conversation.Tags)
}
func TestMessageModel(t *testing.T) {
// Test message model fields
message := Message{
ID: 1,
ConversationID: 1,
UserID: 1,
Content: "Test message content",
Type: "text",
Status: "sent",
Sentiment: 0.5,
IsAI: false,
AIModel: "",
}
// Verify message fields
assert.Equal(t, uint(1), message.ID)
assert.Equal(t, uint(1), message.ConversationID)
assert.Equal(t, uint(1), message.UserID)
assert.Equal(t, "Test message content", message.Content)
assert.Equal(t, "text", message.Type)
assert.Equal(t, "sent", message.Status)
assert.Equal(t, 0.5, message.Sentiment)
assert.False(t, message.IsAI)
assert.Equal(t, "", message.AIModel)
}
func TestKnowledgeModel(t *testing.T) {
// Test knowledge model fields
knowledge := KnowledgeBase{
ID: 1,
Question: "Test Question",
Answer: "Test knowledge content",
Category: "general",
Tags: "test,sample",
Priority: 1,
ViewCount: 10,
Helpful: 5,
NotHelpful: 2,
Active: true,
CreatedBy: 1,
UpdatedBy: 1,
}
// Verify knowledge fields
assert.Equal(t, uint(1), knowledge.ID)
assert.Equal(t, "Test Question", knowledge.Question)
assert.Equal(t, "Test knowledge content", knowledge.Answer)
assert.Equal(t, "general", knowledge.Category)
assert.Equal(t, "test,sample", knowledge.Tags)
assert.Equal(t, 1, knowledge.Priority)
assert.Equal(t, 10, knowledge.ViewCount)
assert.Equal(t, 5, knowledge.Helpful)
assert.Equal(t, 2, knowledge.NotHelpful)
assert.True(t, knowledge.Active)
assert.Equal(t, uint(1), knowledge.CreatedBy)
assert.Equal(t, uint(1), knowledge.UpdatedBy)
}
func TestAIModel(t *testing.T) {
// Test AI model fields
aiModel := AIModel{
ID: 1,
Name: "Test AI Model",
Type: "openai",
Endpoint: "https://api.openai.com/v1/chat/completions",
APIKey: "test-api-key",
Model: "gpt-4",
MaxTokens: 4000,
Temperature: 0.7,
TopP: 1.0,
Active: true,
Priority: 1,
Description: "Test AI model description",
}
// Verify AI model fields
assert.Equal(t, uint(1), aiModel.ID)
assert.Equal(t, "Test AI Model", aiModel.Name)
assert.Equal(t, "openai", aiModel.Type)
assert.Equal(t, "https://api.openai.com/v1/chat/completions", aiModel.Endpoint)
assert.Equal(t, "test-api-key", aiModel.APIKey)
assert.Equal(t, "gpt-4", aiModel.Model)
assert.Equal(t, 4000, aiModel.MaxTokens)
assert.Equal(t, 0.7, aiModel.Temperature)
assert.Equal(t, 1.0, aiModel.TopP)
assert.True(t, aiModel.Active)
assert.Equal(t, 1, aiModel.Priority)
assert.Equal(t, "Test AI model description", aiModel.Description)
}

View File

@@ -0,0 +1,261 @@
package models
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// User Request/Response types
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Role string `json:"role"`
}
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type LoginResponse struct {
Token string `json:"token"`
User SafeUser `json:"user"`
}
type UpdateUserRequest struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"email" binding:"omitempty,email"`
Active *bool `json:"active"`
Role string `json:"role"`
}
type ChangePasswordRequest struct {
CurrentPassword string `json:"currentPassword" binding:"required"`
NewPassword string `json:"newPassword" binding:"required,min=6"`
}
// Conversation Request/Response types
type CreateConversationRequest struct {
Title string `json:"title" binding:"required,min=1,max=100"`
Department string `json:"department" binding:"required"`
Priority string `json:"priority" binding:"oneof=low medium high urgent"`
Tags string `json:"tags"`
}
type UpdateConversationRequest struct {
Title string `json:"title"`
Status string `json:"status" binding:"oneof=active closed escalated"`
Department string `json:"department"`
Priority string `json:"priority" binding:"oneof=low medium high urgent"`
Tags string `json:"tags"`
AgentID *uint `json:"agentId"`
}
type CreateMessageRequest struct {
ConversationID uint `json:"conversationId" binding:"required"`
Content string `json:"content" binding:"required,min=1,max=5000"`
Type string `json:"type" binding:"oneof=text image file system"`
}
type UpdateMessageRequest struct {
Content string `json:"content" binding:"omitempty,min=1,max=5000"`
Status string `json:"status" binding:"oneof=sent delivered read"`
}
type ConversationListResponse struct {
Conversations []Conversation `json:"conversations"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
type MessageListResponse struct {
Messages []Message `json:"messages"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
type ConversationStats struct {
TotalMessages int64 `json:"totalMessages"`
AverageSentiment float64 `json:"averageSentiment"`
ResponseTime int64 `json:"responseTime"` // in seconds
FirstMessageAt time.Time `json:"firstMessageAt"`
LastMessageAt time.Time `json:"lastMessageAt"`
}
// Knowledge Base Request/Response types
type CreateKnowledgeBaseRequest struct {
Question string `json:"question" binding:"required,min=1,max=500"`
Answer string `json:"answer" binding:"required,min=1,max=5000"`
Category string `json:"category" binding:"required,min=1,max=100"`
Tags string `json:"tags"`
Priority int `json:"priority" binding:"min=1,max=10"`
}
type UpdateKnowledgeBaseRequest struct {
Question string `json:"question" binding:"omitempty,min=1,max=500"`
Answer string `json:"answer" binding:"omitempty,min=1,max=5000"`
Category string `json:"category" binding:"omitempty,min=1,max=100"`
Tags string `json:"tags"`
Priority int `json:"priority" binding:"omitempty,min=1,max=10"`
Active *bool `json:"active"`
}
type CreateKnowledgeBaseFeedbackRequest struct {
KnowledgeBaseID uint `json:"knowledgeBaseId" binding:"required"`
Helpful bool `json:"helpful"`
Comment string `json:"comment" binding:"max=500"`
}
type KnowledgeBaseSearchRequest struct {
Query string `json:"query" binding:"required,min=1,max=100"`
Category string `json:"category"`
Tags string `json:"tags"`
Page int `json:"page" binding:"min=1"`
PageSize int `json:"pageSize" binding:"min=1,max=100"`
}
type KnowledgeBaseListResponse struct {
Entries []KnowledgeBase `json:"entries"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
type KnowledgeBaseSearchResponse struct {
Results []KnowledgeBaseSearchResult `json:"results"`
Total int64 `json:"total"`
Query string `json:"query"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
type KnowledgeBaseSearchResult struct {
KnowledgeBase
RelevanceScore float64 `json:"relevanceScore"`
MatchedFields []string `json:"matchedFields"`
}
type KnowledgeBaseStats struct {
TotalEntries int64 `json:"totalEntries"`
TotalViews int64 `json:"totalViews"`
AverageHelpful float64 `json:"averageHelpful"` // Average helpful rating
TopCategories []CategoryStat `json:"topCategories"`
TopTags []TagStat `json:"topTags"`
}
type CategoryStat struct {
Category string `json:"category"`
Count int64 `json:"count"`
}
type TagStat struct {
Tag string `json:"tag"`
Count int64 `json:"count"`
}
// AI Model Request/Response types
type CreateAIModelRequest struct {
Name string `json:"name" binding:"required,min=1,max=100"`
Type string `json:"type" binding:"required,oneof=openai local custom"`
Endpoint string `json:"endpoint"`
APIKey string `json:"apiKey"`
Model string `json:"model" binding:"required,min=1,max=100"`
MaxTokens int `json:"maxTokens" binding:"min=1,max=100000"`
Temperature float64 `json:"temperature" binding:"min=0,max=2"`
TopP float64 `json:"topP" binding:"min=0,max=1"`
Priority int `json:"priority" binding:"min=1,max=10"`
Description string `json:"description" binding:"max=500"`
}
type UpdateAIModelRequest struct {
Name string `json:"name" binding:"omitempty,min=1,max=100"`
Type string `json:"type" binding:"omitempty,oneof=openai local custom"`
Endpoint string `json:"endpoint"`
APIKey string `json:"apiKey"`
Model string `json:"model" binding:"omitempty,min=1,max=100"`
MaxTokens int `json:"maxTokens" binding:"omitempty,min=1,max=100000"`
Temperature float64 `json:"temperature" binding:"omitempty,min=0,max=2"`
TopP float64 `json:"topP" binding:"omitempty,min=0,max=1"`
Active *bool `json:"active"`
Priority int `json:"priority" binding:"omitempty,min=1,max=10"`
Description string `json:"description" binding:"omitempty,max=500"`
}
type AIModelListResponse struct {
Models []AIModel `json:"models"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
type AIInteractionListResponse struct {
Interactions []AIInteraction `json:"interactions"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
type AIFallbackListResponse struct {
Fallbacks []AIFallback `json:"fallbacks"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
type AIStats struct {
TotalInteractions int64 `json:"totalInteractions"`
TotalFallbacks int64 `json:"totalFallbacks"`
AverageResponseTime float64 `json:"averageResponseTime"` // in milliseconds
TotalTokensUsed int64 `json:"totalTokensUsed"`
TotalCost float64 `json:"totalCost"`
ModelStats []AIModelStats `json:"modelStats"`
SuccessRate float64 `json:"successRate"`
}
type AIModelStats struct {
AIModel AIModel `json:"aiModel"`
InteractionsCount int64 `json:"interactionsCount"`
FallbacksCount int64 `json:"fallbacksCount"`
AverageResponseTime float64 `json:"averageResponseTime"`
SuccessRate float64 `json:"successRate"`
TokensUsed int64 `json:"tokensUsed"`
Cost float64 `json:"cost"`
}
// Common Response types
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Status int `json:"status"`
}
type SuccessResponse struct {
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Status int `json:"status"`
}
// Response helpers
func NewErrorResponse(c *gin.Context, message string, statusCode int) {
c.JSON(statusCode, ErrorResponse{
Error: http.StatusText(statusCode),
Message: message,
Status: statusCode,
})
}
func NewSuccessResponse(c *gin.Context, message string, data interface{}, statusCode int) {
c.JSON(statusCode, SuccessResponse{
Message: message,
Data: data,
Status: statusCode,
})
}

View File

@@ -0,0 +1,69 @@
package models
import (
"time"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// User represents a user in the system
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Username string `json:"username" gorm:"uniqueIndex;not null"`
Email string `json:"email" gorm:"uniqueIndex;not null"`
Password string `json:"-" gorm:"not null"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Role string `json:"role" gorm:"default:'user'"` // 'user', 'agent', 'admin'
Active bool `json:"active" gorm:"default:true"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
// BeforeSave is a GORM hook that hashes the password before saving
func (u *User) BeforeSave(tx *gorm.DB) error {
if u.Password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashedPassword)
}
return nil
}
// ComparePassword compares a plaintext password with the user's hashed password
func (u *User) ComparePassword(password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
return err == nil
}
// ToSafeUser returns a user object without sensitive information
func (u *User) ToSafeUser() SafeUser {
return SafeUser{
ID: u.ID,
Username: u.Username,
Email: u.Email,
FirstName: u.FirstName,
LastName: u.LastName,
Role: u.Role,
Active: u.Active,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
// SafeUser represents user information that can be safely exposed to clients
type SafeUser struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Role string `json:"role"`
Active bool `json:"active"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

View File

@@ -0,0 +1,142 @@
package routes
import (
"github.com/gin-gonic/gin"
"customer-support-system/internal/auth"
"customer-support-system/internal/handlers"
)
// SetupRoutes configures all the routes for the application
func SetupRoutes() *gin.Engine {
// Create a new Gin engine
r := gin.Default()
// Add CORS middleware
r.Use(CORSMiddleware())
// Create handlers
userHandler := handlers.NewUserHandler()
conversationHandler := handlers.NewConversationHandler()
knowledgeHandler := handlers.NewKnowledgeHandler()
aiHandler := handlers.NewAIHandler()
// Health check endpoint
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{
"status": "ok",
})
})
// API version 1 group
v1 := r.Group("/api/v1")
{
// Public routes (no authentication required)
public := v1.Group("/public")
{
// User authentication routes
public.POST("/register", userHandler.Register)
public.POST("/login", userHandler.Login)
}
// Protected routes (authentication required)
protected := v1.Group("")
protected.Use(auth.AuthMiddleware())
{
// User routes
user := protected.Group("/user")
{
user.GET("/profile", userHandler.GetProfile)
user.PUT("/profile", userHandler.UpdateProfile)
user.PUT("/change-password", userHandler.ChangePassword)
}
// Conversation routes
conversations := protected.Group("/conversations")
{
conversations.GET("", conversationHandler.ListConversations)
conversations.POST("", conversationHandler.CreateConversation)
conversations.GET("/:id", conversationHandler.GetConversation)
conversations.PUT("/:id", conversationHandler.UpdateConversation)
conversations.DELETE("/:id", conversationHandler.DeleteConversation)
conversations.GET("/:id/stats", conversationHandler.GetConversationStats)
// Message routes
conversations.POST("/:id/messages", conversationHandler.CreateMessage)
conversations.GET("/:id/messages", conversationHandler.GetMessages)
conversations.PUT("/:id/messages/:messageId", conversationHandler.UpdateMessage)
conversations.DELETE("/:id/messages/:messageId", conversationHandler.DeleteMessage)
// AI interaction routes
conversations.POST("/:id/ai", conversationHandler.SendMessageWithAI)
}
// Knowledge base routes
knowledge := protected.Group("/knowledge")
{
knowledge.GET("", knowledgeHandler.ListKnowledgeEntries)
knowledge.GET("/search", knowledgeHandler.SearchKnowledge)
knowledge.GET("/categories", knowledgeHandler.GetCategories)
knowledge.GET("/tags", knowledgeHandler.GetTags)
knowledge.GET("/popular", knowledgeHandler.GetPopularKnowledge)
knowledge.GET("/recent", knowledgeHandler.GetRecentKnowledge)
knowledge.GET("/best-match", knowledgeHandler.FindBestMatch)
knowledge.GET("/stats", knowledgeHandler.GetKnowledgeStats)
knowledge.GET("/:id", knowledgeHandler.GetKnowledgeEntry)
knowledge.POST("/:id/rate", knowledgeHandler.RateKnowledgeEntry)
}
// AI routes
ai := protected.Group("/ai")
{
ai.POST("/query", aiHandler.QueryAI)
ai.POST("/analyze-complexity", aiHandler.AnalyzeComplexity)
ai.GET("/models", aiHandler.GetAvailableModels)
ai.POST("/openai", aiHandler.QueryOpenAI)
ai.POST("/ollama", aiHandler.QueryOllama)
}
// Admin routes (admin role required)
admin := protected.Group("/admin")
admin.Use(auth.RoleMiddleware("admin"))
{
// User management
admin.GET("/users", userHandler.AdminGetUsers)
admin.GET("/users/:id", userHandler.AdminGetUser)
admin.PUT("/users/:id", userHandler.AdminUpdateUser)
admin.DELETE("/users/:id", userHandler.AdminDeleteUser)
// Knowledge base management
admin.POST("/knowledge", knowledgeHandler.CreateKnowledgeEntry)
admin.PUT("/knowledge/:id", knowledgeHandler.UpdateKnowledgeEntry)
admin.DELETE("/knowledge/:id", knowledgeHandler.DeleteKnowledgeEntry)
}
// Agent routes (agent or admin role required)
agent := protected.Group("/agent")
agent.Use(auth.RoleMiddleware("agent", "admin"))
{
// Additional agent-only endpoints can be added here
}
}
}
return r
}
// CORSMiddleware adds CORS headers to the response
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}