committtttt
This commit is contained in:
71
.gitignore
vendored
Normal file
71
.gitignore
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
/backend/bin/
|
||||
/backend/support
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Frontend build artifacts
|
||||
frontend/dist/
|
||||
frontend/build/
|
||||
frontend/node_modules/
|
||||
|
||||
# Kubernetes secrets
|
||||
secrets/
|
||||
*.secret
|
||||
*.key
|
||||
*.pem
|
||||
*.crt
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
*.temp
|
||||
.claude
|
288
README.md
Normal file
288
README.md
Normal file
@@ -0,0 +1,288 @@
|
||||
# AI-Powered Support System
|
||||
|
||||
A hybrid AI-powered customer support system that combines OpenAI's GPT-4 for complex queries with local LLM (Ollama) for simple queries. The system includes sentiment analysis, real-time chat, and a knowledge base for efficient customer support.
|
||||
|
||||
## Features
|
||||
|
||||
- **Hybrid AI System**: Combines OpenAI's GPT-4 for complex queries with local LLM (Ollama) for simple queries
|
||||
- **Intelligent Routing**: Automatically routes queries to the appropriate AI model based on complexity
|
||||
- **Sentiment Analysis**: Analyzes customer sentiment and escalates to human agents when needed
|
||||
- **Real-time Chat**: WebSocket-based real-time communication with typing indicators and read receipts
|
||||
- **Knowledge Base**: FAQ system with automatic suggestions based on conversation context
|
||||
- **User Authentication**: JWT-based authentication with role-based access control
|
||||
- **Admin Dashboard**: Analytics and management interface for administrators
|
||||
- **Conversation History**: Search and review past conversations
|
||||
- **Scalable Architecture**: Built with Go, PostgreSQL, and Redis for high performance
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Backend
|
||||
- **Go**: Programming language
|
||||
- **Gin**: Web framework
|
||||
- **PostgreSQL**: Primary database
|
||||
- **Redis**: Caching and session management
|
||||
- **JWT**: Authentication
|
||||
- **GORM**: ORM for database operations
|
||||
- **Logrus**: Structured logging
|
||||
- **Viper**: Configuration management
|
||||
|
||||
### AI Integration
|
||||
- **OpenAI API**: For complex queries
|
||||
- **Ollama**: For local LLM integration
|
||||
- **Sentiment Analysis**: For analyzing customer sentiment
|
||||
|
||||
### Frontend (To be implemented)
|
||||
- **React/Vue.js**: Frontend framework
|
||||
- **WebSocket**: Real-time communication
|
||||
- **Material-UI/Ant Design**: UI components
|
||||
|
||||
### Deployment
|
||||
- **Docker**: Containerization
|
||||
- **Docker Compose**: Local development
|
||||
- **Kubernetes**: Production deployment
|
||||
- **CI/CD**: Automated testing and deployment
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
support/
|
||||
├── backend/ # Backend Go application
|
||||
│ ├── cmd/ # Application entry point
|
||||
│ │ └── main.go # Main application file
|
||||
│ ├── internal/ # Internal application packages
|
||||
│ │ ├── ai/ # AI service
|
||||
│ │ ├── auth/ # Authentication service
|
||||
│ │ ├── conversation/ # Conversation service
|
||||
│ │ ├── database/ # Database connection and migrations
|
||||
│ │ ├── handlers/ # HTTP handlers
|
||||
│ │ ├── knowledge/ # Knowledge base service
|
||||
│ │ ├── models/ # Data models
|
||||
│ │ └── routes/ # Route definitions
|
||||
│ ├── pkg/ # Public library packages
|
||||
│ │ ├── config/ # Configuration management
|
||||
│ │ └── logger/ # Logging utilities
|
||||
│ ├── go.mod # Go module file
|
||||
│ ├── go.sum # Go module checksums
|
||||
│ └── Dockerfile # Docker configuration for backend
|
||||
├── frontend/ # Frontend React/Vue.js application (to be implemented)
|
||||
├── docs/ # Documentation
|
||||
├── deployment/ # Kubernetes deployment configurations (to be implemented)
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
├── .gitignore # Git ignore file
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.21 or higher
|
||||
- Docker and Docker Compose
|
||||
- PostgreSQL (if not using Docker)
|
||||
- Redis (if not using Docker)
|
||||
- OpenAI API key (for GPT-4 integration)
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/support.git
|
||||
cd support
|
||||
```
|
||||
|
||||
2. Set up environment variables:
|
||||
```bash
|
||||
cp backend/.env.example backend/.env
|
||||
```
|
||||
Edit the `.env` file with your configuration, including your OpenAI API key.
|
||||
|
||||
3. Start the services using Docker Compose:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
4. Wait for all services to start up. You can check the logs with:
|
||||
```bash
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
#### Backend Development
|
||||
|
||||
1. Navigate to the backend directory:
|
||||
```bash
|
||||
cd backend
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
go mod download
|
||||
```
|
||||
|
||||
3. Run the application:
|
||||
```bash
|
||||
go run cmd/main.go
|
||||
```
|
||||
|
||||
The backend API will be available at `http://localhost:8080`.
|
||||
|
||||
#### Frontend Development (To be implemented)
|
||||
|
||||
1. Navigate to the frontend directory:
|
||||
```bash
|
||||
cd frontend
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Run the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The frontend will be available at `http://localhost:3000`.
|
||||
|
||||
## API Documentation
|
||||
|
||||
### Authentication Endpoints
|
||||
|
||||
- `POST /api/v1/public/login` - User login
|
||||
- `POST /api/v1/public/register` - User registration
|
||||
|
||||
### User Endpoints
|
||||
|
||||
- `GET /api/v1/user/profile` - Get user profile
|
||||
- `PUT /api/v1/user/profile` - Update user profile
|
||||
- `PUT /api/v1/user/change-password` - Change password
|
||||
|
||||
### Admin Endpoints
|
||||
|
||||
- `GET /api/v1/admin/users` - List all users
|
||||
- `GET /api/v1/admin/users/:id` - Get user by ID
|
||||
- `PUT /api/v1/admin/users/:id` - Update user
|
||||
- `DELETE /api/v1/admin/users/:id` - Delete user
|
||||
|
||||
### Conversation Endpoints
|
||||
|
||||
- `GET /api/v1/conversations` - List conversations
|
||||
- `POST /api/v1/conversations` - Create new conversation
|
||||
- `GET /api/v1/conversations/:id` - Get conversation by ID
|
||||
- `PUT /api/v1/conversations/:id` - Update conversation
|
||||
- `DELETE /api/v1/conversations/:id` - Delete conversation
|
||||
- `GET /api/v1/conversations/:id/stats` - Get conversation statistics
|
||||
|
||||
### Message Endpoints
|
||||
|
||||
- `GET /api/v1/conversations/:id/messages` - Get messages in a conversation
|
||||
- `POST /api/v1/conversations/:id/messages` - Send a message
|
||||
- `PUT /api/v1/conversations/:id/messages/:messageId` - Update a message
|
||||
- `DELETE /api/v1/conversations/:id/messages/:messageId` - Delete a message
|
||||
- `POST /api/v1/conversations/:id/ai` - Send message with AI response
|
||||
|
||||
### Knowledge Base Endpoints
|
||||
|
||||
- `GET /api/v1/knowledge` - Get knowledge base entries
|
||||
- `GET /api/v1/knowledge/search` - Search knowledge base
|
||||
- `GET /api/v1/knowledge/categories` - Get knowledge categories
|
||||
- `GET /api/v1/knowledge/tags` - Get knowledge tags
|
||||
- `GET /api/v1/knowledge/popular` - Get popular knowledge entries
|
||||
- `GET /api/v1/knowledge/recent` - Get recent knowledge entries
|
||||
- `GET /api/v1/knowledge/best-match` - Find best match for a query
|
||||
- `GET /api/v1/knowledge/stats` - Get knowledge base statistics
|
||||
- `GET /api/v1/knowledge/:id` - Get knowledge base entry by ID
|
||||
- `POST /api/v1/knowledge/:id/rate` - Rate knowledge base entry
|
||||
|
||||
#### Admin Knowledge Base Endpoints
|
||||
|
||||
- `POST /api/v1/admin/knowledge` - Create knowledge base entry
|
||||
- `PUT /api/v1/admin/knowledge/:id` - Update knowledge base entry
|
||||
- `DELETE /api/v1/admin/knowledge/:id` - Delete knowledge base entry
|
||||
|
||||
### AI Endpoints
|
||||
|
||||
- `POST /api/v1/ai/query` - Query AI service
|
||||
- `POST /api/v1/ai/analyze-complexity` - Analyze complexity of a prompt
|
||||
- `GET /api/v1/ai/models` - Get available AI models
|
||||
- `POST /api/v1/ai/openai` - Query OpenAI directly
|
||||
- `POST /api/v1/ai/ollama` - Query Ollama directly
|
||||
|
||||
## Testing
|
||||
|
||||
### Backend Testing
|
||||
|
||||
1. Navigate to the backend directory:
|
||||
```bash
|
||||
cd backend
|
||||
```
|
||||
|
||||
2. Run tests:
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
### Frontend Testing (To be implemented)
|
||||
|
||||
1. Navigate to the frontend directory:
|
||||
```bash
|
||||
cd frontend
|
||||
```
|
||||
|
||||
2. Run tests:
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
1. Build the Docker image:
|
||||
```bash
|
||||
docker build -t support-backend ./backend
|
||||
```
|
||||
|
||||
2. Run the container:
|
||||
```bash
|
||||
docker run -p 8080:8080 support-backend
|
||||
```
|
||||
|
||||
### Docker Compose Deployment
|
||||
|
||||
1. Start all services:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
2. Stop all services:
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Kubernetes Deployment (To be implemented)
|
||||
|
||||
1. Apply the Kubernetes configurations:
|
||||
```bash
|
||||
kubectl apply -f deployment/
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Contact
|
||||
|
||||
Your Name - [@yourusername](https://twitter.com/yourusername) - email@example.com
|
||||
|
||||
Project Link: [https://github.com/yourusername/support](https://github.com/yourusername/support)
|
41
backend/.env.example
Normal file
41
backend/.env.example
Normal file
@@ -0,0 +1,41 @@
|
||||
# Server Configuration
|
||||
SERVER_HOST=0.0.0.0
|
||||
SERVER_PORT=8080
|
||||
SERVER_READ_TIMEOUT=30s
|
||||
SERVER_WRITE_TIMEOUT=30s
|
||||
|
||||
# Database Configuration
|
||||
DB_HOST=localhost
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=postgres
|
||||
DB_NAME=support
|
||||
DB_PORT=5432
|
||||
DB_SSLMODE=disable
|
||||
DB_TIMEZONE=UTC
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# Authentication Configuration
|
||||
AUTH_JWT_SECRET=your-secret-key-change-in-production
|
||||
AUTH_JWT_EXPIRATION_HOURS=24
|
||||
|
||||
# AI Configuration
|
||||
AI_OPENAI_API_KEY=your-openai-api-key
|
||||
AI_OPENAI_MODEL=gpt-4
|
||||
AI_OPENAI_MAX_TOKENS=4000
|
||||
AI_OPENAI_TEMPERATURE=0.7
|
||||
AI_OPENAI_TOP_P=1.0
|
||||
|
||||
AI_LOCAL_ENDPOINT=http://localhost:11434
|
||||
AI_LOCAL_MODEL=llama2
|
||||
AI_LOCAL_MAX_TOKENS=2000
|
||||
AI_LOCAL_TEMPERATURE=0.7
|
||||
AI_LOCAL_TOP_P=1.0
|
||||
|
||||
# Logging Configuration
|
||||
LOG_LEVEL=info
|
||||
LOG_FORMAT=json
|
39
backend/Dockerfile
Normal file
39
backend/Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
||||
# Build stage
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install git and certificates
|
||||
RUN apk add --no-cache git ca-certificates tzdata
|
||||
|
||||
# Copy go mod files
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# Download dependencies
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main cmd/main.go
|
||||
|
||||
# Final stage
|
||||
FROM alpine:latest
|
||||
|
||||
# Install ca-certificates for HTTPS
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /root/
|
||||
|
||||
# Copy binary from builder
|
||||
COPY --from=builder /app/main .
|
||||
COPY --from=builder /app/.env.example .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Command to run the application
|
||||
CMD ["./main"]
|
125
backend/Makefile
Normal file
125
backend/Makefile
Normal file
@@ -0,0 +1,125 @@
|
||||
.PHONY: build run test clean deps docker-build docker-run docker-compose-up docker-compose-down
|
||||
|
||||
# Variables
|
||||
BINARY_NAME=support
|
||||
BINARY_UNIX=$(BINARY_NAME)_unix
|
||||
VERSION?=1.0.0
|
||||
BUILD_TIME?=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
||||
COMMIT?=$(shell git rev-parse --short HEAD)
|
||||
|
||||
# Go build flags
|
||||
LDFLAGS=-ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -X main.Commit=$(COMMIT)"
|
||||
|
||||
# Default target
|
||||
all: deps build
|
||||
|
||||
# Install dependencies
|
||||
deps:
|
||||
go mod download
|
||||
go mod tidy
|
||||
|
||||
# Build the application
|
||||
build:
|
||||
go build $(LDFLAGS) -o $(BINARY_NAME) cmd/main.go
|
||||
|
||||
# Build for Linux
|
||||
build-linux:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BINARY_UNIX) cmd/main.go
|
||||
|
||||
# Run the application
|
||||
run:
|
||||
go run cmd/main.go
|
||||
|
||||
# Run tests
|
||||
test:
|
||||
go test -v ./...
|
||||
|
||||
# Run tests with coverage
|
||||
test-coverage:
|
||||
go test -v -cover ./...
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
go clean
|
||||
rm -f $(BINARY_NAME)
|
||||
rm -f $(BINARY_UNIX)
|
||||
|
||||
# Build Docker image
|
||||
docker-build:
|
||||
docker build -t $(BINARY_NAME):$(VERSION) .
|
||||
|
||||
# Run Docker container
|
||||
docker-run:
|
||||
docker run -p 8080:8080 $(BINARY_NAME):$(VERSION)
|
||||
|
||||
# Start Docker Compose
|
||||
docker-compose-up:
|
||||
docker-compose up -d
|
||||
|
||||
# Stop Docker Compose
|
||||
docker-compose-down:
|
||||
docker-compose down
|
||||
|
||||
# View Docker Compose logs
|
||||
docker-compose-logs:
|
||||
docker-compose logs -f
|
||||
|
||||
# Run database migrations
|
||||
migrate-up:
|
||||
go run cmd/migrate.go up
|
||||
|
||||
# Rollback database migrations
|
||||
migrate-down:
|
||||
go run cmd/migrate.go down
|
||||
|
||||
# Seed database
|
||||
seed-db:
|
||||
go run cmd/seed.go
|
||||
|
||||
# Format code
|
||||
fmt:
|
||||
go fmt ./...
|
||||
|
||||
# Lint code
|
||||
lint:
|
||||
golangci-lint run
|
||||
|
||||
# Security check
|
||||
security:
|
||||
gosec ./...
|
||||
|
||||
# Generate API documentation
|
||||
docs:
|
||||
swag init -g cmd/main.go
|
||||
|
||||
# Install development tools
|
||||
install-tools:
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
|
||||
# Help target
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " all - Install dependencies and build the application"
|
||||
@echo " deps - Install dependencies"
|
||||
@echo " build - Build the application"
|
||||
@echo " build-linux - Build the application for Linux"
|
||||
@echo " run - Run the application"
|
||||
@echo " test - Run tests"
|
||||
@echo " test-coverage - Run tests with coverage"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo " docker-build - Build Docker image"
|
||||
@echo " docker-run - Run Docker container"
|
||||
@echo " docker-compose-up - Start Docker Compose"
|
||||
@echo " docker-compose-down - Stop Docker Compose"
|
||||
@echo " docker-compose-logs - View Docker Compose logs"
|
||||
@echo " migrate-up - Run database migrations"
|
||||
@echo " migrate-down - Rollback database migrations"
|
||||
@echo " seed-db - Seed database"
|
||||
@echo " fmt - Format code"
|
||||
@echo " lint - Lint code"
|
||||
@echo " security - Security check"
|
||||
@echo " docs - Generate API documentation"
|
||||
@echo " install-tools - Install development tools"
|
||||
@echo " help - Show this help message"
|
82
backend/cmd/main/main.go
Normal file
82
backend/cmd/main/main.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"customer-support-system/internal/database"
|
||||
"customer-support-system/internal/routes"
|
||||
"customer-support-system/pkg/config"
|
||||
"customer-support-system/pkg/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load configuration
|
||||
if err := config.LoadConfig(); err != nil {
|
||||
log.Fatalf("Failed to load configuration: %v", err)
|
||||
}
|
||||
|
||||
// Initialize logger
|
||||
logger.InitLogger()
|
||||
|
||||
// Connect to database
|
||||
if err := database.Connect(); err != nil {
|
||||
logger.WithError(err).Fatal("Failed to connect to database")
|
||||
}
|
||||
|
||||
// Run migrations
|
||||
if err := database.AutoMigrate(); err != nil {
|
||||
logger.WithError(err).Fatal("Failed to run database migrations")
|
||||
}
|
||||
|
||||
// Seed database with initial data
|
||||
if err := database.SeedDatabase(); err != nil {
|
||||
logger.WithError(err).Fatal("Failed to seed database")
|
||||
}
|
||||
|
||||
// Set up routes
|
||||
router := routes.SetupRoutes()
|
||||
|
||||
// Create HTTP server
|
||||
server := &http.Server{
|
||||
Addr: config.AppConfig.Server.GetServerAddress(),
|
||||
Handler: router,
|
||||
ReadTimeout: config.AppConfig.Server.ReadTimeout,
|
||||
WriteTimeout: config.AppConfig.Server.WriteTimeout,
|
||||
}
|
||||
|
||||
// Start server in a goroutine
|
||||
go func() {
|
||||
logger.WithField("address", server.Addr).Info("Starting server")
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.WithError(err).Fatal("Failed to start server")
|
||||
}
|
||||
}()
|
||||
|
||||
// Set up graceful shutdown
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
logger.Info("Shutting down server...")
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Shutdown server
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
logger.WithError(err).Fatal("Server forced to shutdown")
|
||||
}
|
||||
|
||||
// Close database connection
|
||||
if err := database.Close(); err != nil {
|
||||
logger.WithError(err).Error("Failed to close database connection")
|
||||
}
|
||||
|
||||
logger.Info("Server exited")
|
||||
}
|
54
backend/cmd/migrate/migrate.go
Normal file
54
backend/cmd/migrate/migrate.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"customer-support-system/internal/database"
|
||||
"customer-support-system/pkg/config"
|
||||
"customer-support-system/pkg/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load configuration
|
||||
if err := config.LoadConfig(); err != nil {
|
||||
log.Fatalf("Failed to load configuration: %v", err)
|
||||
}
|
||||
|
||||
// Initialize logger
|
||||
logger.InitLogger()
|
||||
|
||||
// Connect to database
|
||||
if err := database.Connect(); err != nil {
|
||||
logger.WithError(err).Fatal("Failed to connect to database")
|
||||
}
|
||||
|
||||
// Parse command line arguments
|
||||
action := flag.String("action", "up", "Migration action (up or down)")
|
||||
flag.Parse()
|
||||
|
||||
// Run migrations based on action
|
||||
switch *action {
|
||||
case "up":
|
||||
if err := database.AutoMigrate(); err != nil {
|
||||
logger.WithError(err).Fatal("Failed to run database migrations")
|
||||
}
|
||||
fmt.Println("Database migrations completed successfully")
|
||||
case "down":
|
||||
// Note: GORM doesn't support automatic rollback of migrations
|
||||
// In a real application, you would need to implement a more sophisticated migration system
|
||||
fmt.Println("Automatic rollback is not supported with GORM")
|
||||
fmt.Println("Please manually revert the database schema changes")
|
||||
default:
|
||||
fmt.Printf("Unknown action: %s\n", *action)
|
||||
fmt.Println("Usage: go run cmd/migrate.go -action=[up|down]")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Close database connection
|
||||
if err := database.Close(); err != nil {
|
||||
logger.WithError(err).Error("Failed to close database connection")
|
||||
}
|
||||
}
|
36
backend/cmd/seed/seed.go
Normal file
36
backend/cmd/seed/seed.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"customer-support-system/internal/database"
|
||||
"customer-support-system/pkg/config"
|
||||
"customer-support-system/pkg/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load configuration
|
||||
if err := config.LoadConfig(); err != nil {
|
||||
log.Fatalf("Failed to load configuration: %v", err)
|
||||
}
|
||||
|
||||
// Initialize logger
|
||||
logger.InitLogger()
|
||||
|
||||
// Connect to database
|
||||
if err := database.Connect(); err != nil {
|
||||
logger.WithError(err).Fatal("Failed to connect to database")
|
||||
}
|
||||
|
||||
// Seed database with initial data
|
||||
if err := database.SeedDatabase(); err != nil {
|
||||
logger.WithError(err).Fatal("Failed to seed database")
|
||||
}
|
||||
|
||||
logger.Info("Database seeded successfully")
|
||||
|
||||
// Close database connection
|
||||
if err := database.Close(); err != nil {
|
||||
logger.WithError(err).Error("Failed to close database connection")
|
||||
}
|
||||
}
|
60
backend/go.mod
Normal file
60
backend/go.mod
Normal file
@@ -0,0 +1,60 @@
|
||||
module customer-support-system
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/viper v1.16.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/crypto v0.14.0
|
||||
gorm.io/driver/postgres v1.5.2
|
||||
gorm.io/gorm v1.25.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.4.3 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
)
|
566
backend/go.sum
Normal file
566
backend/go.sum
Normal file
@@ -0,0 +1,566 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
|
||||
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
|
||||
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
|
||||
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
|
||||
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
|
||||
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
|
||||
gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
|
||||
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
|
||||
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
394
backend/internal/ai/ai.go
Normal file
394
backend/internal/ai/ai.go
Normal 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",
|
||||
},
|
||||
}
|
||||
}
|
362
backend/internal/auth/auth.go
Normal file
362
backend/internal/auth/auth.go
Normal 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()
|
||||
}
|
||||
}
|
436
backend/internal/conversation/conversation.go
Normal file
436
backend/internal/conversation/conversation.go
Normal file
@@ -0,0 +1,436 @@
|
||||
package conversation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"customer-support-system/internal/ai"
|
||||
"customer-support-system/internal/database"
|
||||
"customer-support-system/internal/models"
|
||||
"customer-support-system/pkg/logger"
|
||||
)
|
||||
|
||||
// ConversationService handles conversation operations
|
||||
type ConversationService struct {
|
||||
db *gorm.DB
|
||||
aiService *ai.AIService
|
||||
}
|
||||
|
||||
// NewConversationService creates a new conversation service
|
||||
func NewConversationService() *ConversationService {
|
||||
return &ConversationService{
|
||||
db: database.GetDB(),
|
||||
aiService: ai.NewAIService(),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateConversation creates a new conversation
|
||||
func (s *ConversationService) CreateConversation(userID uint, req *models.CreateConversationRequest) (*models.Conversation, error) {
|
||||
conversation := models.Conversation{
|
||||
Title: req.Title,
|
||||
UserID: userID,
|
||||
Department: req.Department,
|
||||
Priority: req.Priority,
|
||||
Tags: req.Tags,
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
if err := s.db.Create(&conversation).Error; err != nil {
|
||||
logger.WithError(err).WithField("user_id", userID).Error("Failed to create conversation")
|
||||
return nil, fmt.Errorf("failed to create conversation: %w", err)
|
||||
}
|
||||
|
||||
logger.WithField("conversation_id", conversation.ID).Info("Conversation created successfully")
|
||||
|
||||
return &conversation, nil
|
||||
}
|
||||
|
||||
// GetConversation retrieves a conversation by ID
|
||||
func (s *ConversationService) GetConversation(conversationID uint, userID uint) (*models.Conversation, error) {
|
||||
var conversation models.Conversation
|
||||
if err := s.db.Where("id = ? AND user_id = ?", conversationID, userID).First(&conversation).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("conversation not found")
|
||||
}
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get conversation")
|
||||
return nil, fmt.Errorf("failed to get conversation: %w", err)
|
||||
}
|
||||
|
||||
return &conversation, nil
|
||||
}
|
||||
|
||||
// ListConversations retrieves a list of conversations for a user
|
||||
func (s *ConversationService) ListConversations(userID uint, page, pageSize int, status string) (*models.ConversationListResponse, error) {
|
||||
var conversations []models.Conversation
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&models.Conversation{}).Where("user_id = ?", userID)
|
||||
|
||||
if status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
// Get total count
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
logger.WithError(err).WithField("user_id", userID).Error("Failed to count conversations")
|
||||
return nil, fmt.Errorf("failed to count conversations: %w", err)
|
||||
}
|
||||
|
||||
// Get paginated results
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("last_message_at DESC").Offset(offset).Limit(pageSize).Find(&conversations).Error; err != nil {
|
||||
logger.WithError(err).WithField("user_id", userID).Error("Failed to list conversations")
|
||||
return nil, fmt.Errorf("failed to list conversations: %w", err)
|
||||
}
|
||||
|
||||
return &models.ConversationListResponse{
|
||||
Conversations: conversations,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateConversation updates a conversation
|
||||
func (s *ConversationService) UpdateConversation(conversationID uint, userID uint, req *models.UpdateConversationRequest) (*models.Conversation, error) {
|
||||
var conversation models.Conversation
|
||||
if err := s.db.Where("id = ? AND user_id = ?", conversationID, userID).First(&conversation).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("conversation not found")
|
||||
}
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get conversation")
|
||||
return nil, fmt.Errorf("failed to get conversation: %w", err)
|
||||
}
|
||||
|
||||
// Update fields
|
||||
if req.Title != "" {
|
||||
conversation.Title = req.Title
|
||||
}
|
||||
if req.Status != "" {
|
||||
conversation.Status = req.Status
|
||||
}
|
||||
if req.Department != "" {
|
||||
conversation.Department = req.Department
|
||||
}
|
||||
if req.Priority != "" {
|
||||
conversation.Priority = req.Priority
|
||||
}
|
||||
if req.Tags != "" {
|
||||
conversation.Tags = req.Tags
|
||||
}
|
||||
if req.AgentID != nil {
|
||||
conversation.AgentID = req.AgentID
|
||||
}
|
||||
|
||||
if err := s.db.Save(&conversation).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to update conversation")
|
||||
return nil, fmt.Errorf("failed to update conversation: %w", err)
|
||||
}
|
||||
|
||||
logger.WithField("conversation_id", conversationID).Info("Conversation updated successfully")
|
||||
|
||||
return &conversation, nil
|
||||
}
|
||||
|
||||
// DeleteConversation deletes a conversation
|
||||
func (s *ConversationService) DeleteConversation(conversationID uint, userID uint) error {
|
||||
var conversation models.Conversation
|
||||
if err := s.db.Where("id = ? AND user_id = ?", conversationID, userID).First(&conversation).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return fmt.Errorf("conversation not found")
|
||||
}
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get conversation")
|
||||
return fmt.Errorf("failed to get conversation: %w", err)
|
||||
}
|
||||
|
||||
if err := s.db.Delete(&conversation).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to delete conversation")
|
||||
return fmt.Errorf("failed to delete conversation: %w", err)
|
||||
}
|
||||
|
||||
logger.WithField("conversation_id", conversationID).Info("Conversation deleted successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateMessage creates a new message in a conversation
|
||||
func (s *ConversationService) CreateMessage(conversationID uint, userID uint, req *models.CreateMessageRequest) (*models.Message, error) {
|
||||
// Check if conversation exists and belongs to user
|
||||
var conversation models.Conversation
|
||||
if err := s.db.Where("id = ? AND user_id = ?", conversationID, userID).First(&conversation).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("conversation not found")
|
||||
}
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get conversation")
|
||||
return nil, fmt.Errorf("failed to get conversation: %w", err)
|
||||
}
|
||||
|
||||
// Create message
|
||||
message := models.Message{
|
||||
ConversationID: conversationID,
|
||||
UserID: userID,
|
||||
Content: req.Content,
|
||||
Type: req.Type,
|
||||
Status: "sent",
|
||||
IsAI: false,
|
||||
}
|
||||
|
||||
if err := s.db.Create(&message).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to create message")
|
||||
return nil, fmt.Errorf("failed to create message: %w", err)
|
||||
}
|
||||
|
||||
logger.WithField("message_id", message.ID).Info("Message created successfully")
|
||||
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
// GetMessages retrieves messages in a conversation
|
||||
func (s *ConversationService) GetMessages(conversationID uint, userID uint, page, pageSize int) (*models.MessageListResponse, error) {
|
||||
// Check if conversation exists and belongs to user
|
||||
var conversation models.Conversation
|
||||
if err := s.db.Where("id = ? AND user_id = ?", conversationID, userID).First(&conversation).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("conversation not found")
|
||||
}
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get conversation")
|
||||
return nil, fmt.Errorf("failed to get conversation: %w", err)
|
||||
}
|
||||
|
||||
var messages []models.Message
|
||||
var total int64
|
||||
|
||||
// Get total count
|
||||
if err := s.db.Model(&models.Message{}).Where("conversation_id = ?", conversationID).Count(&total).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to count messages")
|
||||
return nil, fmt.Errorf("failed to count messages: %w", err)
|
||||
}
|
||||
|
||||
// Get paginated results
|
||||
offset := (page - 1) * pageSize
|
||||
if err := s.db.Where("conversation_id = ?", conversationID).Order("created_at ASC").Offset(offset).Limit(pageSize).Find(&messages).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get messages")
|
||||
return nil, fmt.Errorf("failed to get messages: %w", err)
|
||||
}
|
||||
|
||||
return &models.MessageListResponse{
|
||||
Messages: messages,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateMessage updates a message
|
||||
func (s *ConversationService) UpdateMessage(messageID uint, userID uint, req *models.UpdateMessageRequest) (*models.Message, error) {
|
||||
var message models.Message
|
||||
if err := s.db.Where("id = ? AND user_id = ?", messageID, userID).First(&message).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("message not found")
|
||||
}
|
||||
logger.WithError(err).WithField("message_id", messageID).Error("Failed to get message")
|
||||
return nil, fmt.Errorf("failed to get message: %w", err)
|
||||
}
|
||||
|
||||
// Update fields
|
||||
if req.Content != "" {
|
||||
message.Content = req.Content
|
||||
}
|
||||
if req.Status != "" {
|
||||
message.Status = req.Status
|
||||
}
|
||||
|
||||
if err := s.db.Save(&message).Error; err != nil {
|
||||
logger.WithError(err).WithField("message_id", messageID).Error("Failed to update message")
|
||||
return nil, fmt.Errorf("failed to update message: %w", err)
|
||||
}
|
||||
|
||||
logger.WithField("message_id", messageID).Info("Message updated successfully")
|
||||
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
// DeleteMessage deletes a message
|
||||
func (s *ConversationService) DeleteMessage(messageID uint, userID uint) error {
|
||||
var message models.Message
|
||||
if err := s.db.Where("id = ? AND user_id = ?", messageID, userID).First(&message).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return fmt.Errorf("message not found")
|
||||
}
|
||||
logger.WithError(err).WithField("message_id", messageID).Error("Failed to get message")
|
||||
return fmt.Errorf("failed to get message: %w", err)
|
||||
}
|
||||
|
||||
if err := s.db.Delete(&message).Error; err != nil {
|
||||
logger.WithError(err).WithField("message_id", messageID).Error("Failed to delete message")
|
||||
return fmt.Errorf("failed to delete message: %w", err)
|
||||
}
|
||||
|
||||
logger.WithField("message_id", messageID).Info("Message deleted successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendMessageWithAI sends a message and gets an AI response
|
||||
func (s *ConversationService) SendMessageWithAI(conversationID uint, userID uint, content string) (*models.Message, *models.Message, error) {
|
||||
// Check if conversation exists and belongs to user
|
||||
var conversation models.Conversation
|
||||
if err := s.db.Where("id = ? AND user_id = ?", conversationID, userID).First(&conversation).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil, fmt.Errorf("conversation not found")
|
||||
}
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get conversation")
|
||||
return nil, nil, fmt.Errorf("failed to get conversation: %w", err)
|
||||
}
|
||||
|
||||
// Create user message
|
||||
userMessage := models.Message{
|
||||
ConversationID: conversationID,
|
||||
UserID: userID,
|
||||
Content: content,
|
||||
Type: "text",
|
||||
Status: "sent",
|
||||
IsAI: false,
|
||||
}
|
||||
|
||||
if err := s.db.Create(&userMessage).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to create user message")
|
||||
return nil, nil, fmt.Errorf("failed to create user message: %w", err)
|
||||
}
|
||||
|
||||
// Get conversation history
|
||||
var messages []models.Message
|
||||
if err := s.db.Where("conversation_id = ?", conversationID).Order("created_at ASC").Find(&messages).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get conversation history")
|
||||
return nil, nil, fmt.Errorf("failed to get conversation history: %w", err)
|
||||
}
|
||||
|
||||
// Analyze complexity of the message
|
||||
complexity := s.aiService.AnalyzeComplexity(content)
|
||||
|
||||
// Get AI response
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
aiResponse, err := s.aiService.Query(ctx, content, messages, complexity)
|
||||
if err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get AI response")
|
||||
return &userMessage, nil, fmt.Errorf("failed to get AI response: %w", err)
|
||||
}
|
||||
|
||||
// Create AI message
|
||||
aiMessage := models.Message{
|
||||
ConversationID: conversationID,
|
||||
UserID: userID, // AI responses are associated with the user who asked the question
|
||||
Content: aiResponse,
|
||||
Type: "text",
|
||||
Status: "sent",
|
||||
IsAI: true,
|
||||
AIModel: "gpt-4", // This should be determined based on which AI model was used
|
||||
}
|
||||
|
||||
if err := s.db.Create(&aiMessage).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to create AI message")
|
||||
return &userMessage, nil, fmt.Errorf("failed to create AI message: %w", err)
|
||||
}
|
||||
|
||||
logger.WithFields(map[string]interface{}{
|
||||
"conversation_id": conversationID,
|
||||
"user_message_id": userMessage.ID,
|
||||
"ai_message_id": aiMessage.ID,
|
||||
}).Info("AI response created successfully")
|
||||
|
||||
return &userMessage, &aiMessage, nil
|
||||
}
|
||||
|
||||
// GetConversationStats retrieves statistics for a conversation
|
||||
func (s *ConversationService) GetConversationStats(conversationID uint, userID uint) (*models.ConversationStats, error) {
|
||||
// Check if conversation exists and belongs to user
|
||||
var conversation models.Conversation
|
||||
if err := s.db.Where("id = ? AND user_id = ?", conversationID, userID).First(&conversation).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("conversation not found")
|
||||
}
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get conversation")
|
||||
return nil, fmt.Errorf("failed to get conversation: %w", err)
|
||||
}
|
||||
|
||||
var stats models.ConversationStats
|
||||
|
||||
// Get total messages count
|
||||
if err := s.db.Model(&models.Message{}).Where("conversation_id = ?", conversationID).Count(&stats.TotalMessages).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to count messages")
|
||||
return nil, fmt.Errorf("failed to count messages: %w", err)
|
||||
}
|
||||
|
||||
// Get average sentiment
|
||||
var avgSentiment sql.NullFloat64
|
||||
if err := s.db.Model(&models.Message{}).Where("conversation_id = ?", conversationID).Select("AVG(sentiment)").Scan(&avgSentiment).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to calculate average sentiment")
|
||||
return nil, fmt.Errorf("failed to calculate average sentiment: %w", err)
|
||||
}
|
||||
if avgSentiment.Valid {
|
||||
stats.AverageSentiment = avgSentiment.Float64
|
||||
}
|
||||
|
||||
// Get first and last message timestamps
|
||||
var firstMessage, lastMessage models.Message
|
||||
if err := s.db.Where("conversation_id = ?", conversationID).Order("created_at ASC").First(&firstMessage).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get first message")
|
||||
return nil, fmt.Errorf("failed to get first message: %w", err)
|
||||
}
|
||||
stats.FirstMessageAt = firstMessage.CreatedAt
|
||||
|
||||
if err := s.db.Where("conversation_id = ?", conversationID).Order("created_at DESC").First(&lastMessage).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get last message")
|
||||
return nil, fmt.Errorf("failed to get last message: %w", err)
|
||||
}
|
||||
stats.LastMessageAt = lastMessage.CreatedAt
|
||||
|
||||
// Calculate average response time (time between user messages and AI responses)
|
||||
// This is a simplified calculation and could be improved
|
||||
if stats.TotalMessages > 1 {
|
||||
var responseTimes []int64
|
||||
var prevMessage models.Message
|
||||
isUserMessage := false
|
||||
|
||||
var allMessages []models.Message
|
||||
if err := s.db.Where("conversation_id = ?", conversationID).Order("created_at ASC").Find(&allMessages).Error; err != nil {
|
||||
logger.WithError(err).WithField("conversation_id", conversationID).Error("Failed to get messages for response time calculation")
|
||||
return nil, fmt.Errorf("failed to get messages for response time calculation: %w", err)
|
||||
}
|
||||
|
||||
for _, msg := range allMessages {
|
||||
if !msg.IsAI {
|
||||
if isUserMessage {
|
||||
// Consecutive user messages, skip
|
||||
prevMessage = msg
|
||||
continue
|
||||
}
|
||||
isUserMessage = true
|
||||
prevMessage = msg
|
||||
} else {
|
||||
if isUserMessage {
|
||||
// AI response to user message, calculate response time
|
||||
responseTime := msg.CreatedAt.Sub(prevMessage.CreatedAt).Seconds()
|
||||
responseTimes = append(responseTimes, int64(responseTime))
|
||||
}
|
||||
isUserMessage = false
|
||||
}
|
||||
}
|
||||
|
||||
if len(responseTimes) > 0 {
|
||||
var totalResponseTime int64 = 0
|
||||
for _, rt := range responseTimes {
|
||||
totalResponseTime += rt
|
||||
}
|
||||
stats.ResponseTime = totalResponseTime / int64(len(responseTimes))
|
||||
}
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
217
backend/internal/database/database.go
Normal file
217
backend/internal/database/database.go
Normal 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
|
||||
}
|
211
backend/internal/handlers/ai.go
Normal file
211
backend/internal/handlers/ai.go
Normal 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,
|
||||
},
|
||||
})
|
||||
}
|
558
backend/internal/handlers/conversation.go
Normal file
558
backend/internal/handlers/conversation.go
Normal 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,
|
||||
},
|
||||
})
|
||||
}
|
475
backend/internal/handlers/knowledge.go
Normal file
475
backend/internal/handlers/knowledge.go
Normal 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,
|
||||
},
|
||||
})
|
||||
}
|
453
backend/internal/handlers/user.go
Normal file
453
backend/internal/handlers/user.go
Normal 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",
|
||||
})
|
||||
}
|
553
backend/internal/knowledge/knowledge.go
Normal file
553
backend/internal/knowledge/knowledge.go
Normal 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
|
||||
}
|
127
backend/internal/models/ai.go
Normal file
127
backend/internal/models/ai.go
Normal 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"
|
||||
}
|
63
backend/internal/models/conversation.go
Normal file
63
backend/internal/models/conversation.go
Normal 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
|
||||
}
|
85
backend/internal/models/knowledge.go
Normal file
85
backend/internal/models/knowledge.go
Normal 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
|
||||
}
|
143
backend/internal/models/models_test.go
Normal file
143
backend/internal/models/models_test.go
Normal 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)
|
||||
}
|
261
backend/internal/models/requests.go
Normal file
261
backend/internal/models/requests.go
Normal 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,
|
||||
})
|
||||
}
|
69
backend/internal/models/user.go
Normal file
69
backend/internal/models/user.go
Normal 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"`
|
||||
}
|
142
backend/internal/routes/routes.go
Normal file
142
backend/internal/routes/routes.go
Normal 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()
|
||||
}
|
||||
}
|
BIN
backend/main
Executable file
BIN
backend/main
Executable file
Binary file not shown.
BIN
backend/migrate
Executable file
BIN
backend/migrate
Executable file
Binary file not shown.
205
backend/pkg/config/config.go
Normal file
205
backend/pkg/config/config.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Database DatabaseConfig
|
||||
Auth AuthConfig
|
||||
AI AIConfig
|
||||
JWT JWTConfig
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string
|
||||
Port string
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Password string
|
||||
DBName string
|
||||
SSLMode string
|
||||
TimeZone string
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
JWTSecret string
|
||||
JWTExpirationHours int
|
||||
BCryptCost int
|
||||
MaxLoginAttempts int
|
||||
AccountLockDuration time.Duration
|
||||
}
|
||||
|
||||
type AIConfig struct {
|
||||
OpenAI APIConfig
|
||||
Local APIConfig
|
||||
}
|
||||
|
||||
type APIConfig struct {
|
||||
APIKey string
|
||||
Endpoint string
|
||||
Model string
|
||||
MaxTokens int
|
||||
Temperature float64
|
||||
TopP float64
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
Secret string
|
||||
ExpirationHours int
|
||||
Issuer string
|
||||
Audience string
|
||||
}
|
||||
|
||||
var AppConfig Config
|
||||
|
||||
// LoadConfig loads configuration from environment variables and config file
|
||||
func LoadConfig() error {
|
||||
// Set default values
|
||||
viper.SetDefault("server.host", "0.0.0.0")
|
||||
viper.SetDefault("server.port", "8080")
|
||||
viper.SetDefault("server.readTimeout", "15s")
|
||||
viper.SetDefault("server.writeTimeout", "15s")
|
||||
|
||||
viper.SetDefault("database.host", "localhost")
|
||||
viper.SetDefault("database.port", "5432")
|
||||
viper.SetDefault("database.user", "postgres")
|
||||
viper.SetDefault("database.password", "postgres")
|
||||
viper.SetDefault("database.dbname", "support")
|
||||
viper.SetDefault("database.sslmode", "disable")
|
||||
viper.SetDefault("database.timezone", "UTC")
|
||||
|
||||
viper.SetDefault("auth.jwtSecret", "your-secret-key")
|
||||
viper.SetDefault("auth.jwtExpirationHours", 24)
|
||||
viper.SetDefault("auth.bcryptCost", 12)
|
||||
viper.SetDefault("auth.maxLoginAttempts", 5)
|
||||
viper.SetDefault("auth.accountLockDuration", "30m")
|
||||
|
||||
viper.SetDefault("ai.openai.maxTokens", 4000)
|
||||
viper.SetDefault("ai.openai.temperature", 0.7)
|
||||
viper.SetDefault("ai.openai.topP", 1.0)
|
||||
viper.SetDefault("ai.openai.timeout", "30s")
|
||||
|
||||
viper.SetDefault("ai.local.maxTokens", 2000)
|
||||
viper.SetDefault("ai.local.temperature", 0.7)
|
||||
viper.SetDefault("ai.local.topP", 1.0)
|
||||
viper.SetDefault("ai.local.timeout", "60s")
|
||||
viper.SetDefault("ai.local.endpoint", "http://localhost:11434")
|
||||
viper.SetDefault("ai.local.model", "llama2")
|
||||
|
||||
viper.SetDefault("jwt.secret", "your-jwt-secret")
|
||||
viper.SetDefault("jwt.expirationHours", 24)
|
||||
viper.SetDefault("jwt.issuer", "support-system")
|
||||
viper.SetDefault("jwt.audience", "support-users")
|
||||
|
||||
// Configure viper to read from environment variables
|
||||
viper.AutomaticEnv()
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
|
||||
// Read config file if it exists
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath("./")
|
||||
viper.AddConfigPath("./config")
|
||||
viper.AddConfigPath("../config")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
return fmt.Errorf("error reading config file: %w", err)
|
||||
}
|
||||
// Config file not found; ignore error
|
||||
}
|
||||
|
||||
// Unmarshal config
|
||||
if err := viper.Unmarshal(&AppConfig); err != nil {
|
||||
return fmt.Errorf("unable to decode config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDSN returns the database connection string
|
||||
func (c *DatabaseConfig) GetDSN() string {
|
||||
return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=%s",
|
||||
c.Host, c.User, c.Password, c.DBName, c.Port, c.SSLMode, c.TimeZone)
|
||||
}
|
||||
|
||||
// GetServerAddress returns the server address
|
||||
func (c *ServerConfig) GetServerAddress() string {
|
||||
return fmt.Sprintf("%s:%s", c.Host, c.Port)
|
||||
}
|
||||
|
||||
// GetJWTSigningKey returns the JWT signing key
|
||||
func (c *JWTConfig) GetJWTSigningKey() []byte {
|
||||
return []byte(c.Secret)
|
||||
}
|
||||
|
||||
// ParseJWTToken parses a JWT token and returns the claims
|
||||
func (c *JWTConfig) ParseJWTToken(tokenString string) (*jwt.MapClaims, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return c.GetJWTSigningKey(), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// GetEnvAsInt gets an environment variable as an integer with a default value
|
||||
func GetEnvAsInt(key string, defaultValue int) int {
|
||||
valueStr := GetEnv(key, "")
|
||||
if value, err := strconv.Atoi(valueStr); err == nil {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GetEnvAsBool gets an environment variable as a boolean with a default value
|
||||
func GetEnvAsBool(key string, defaultValue bool) bool {
|
||||
valueStr := GetEnv(key, "")
|
||||
if value, err := strconv.ParseBool(valueStr); err == nil {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GetEnvAsDuration gets an environment variable as a duration with a default value
|
||||
func GetEnvAsDuration(key string, defaultValue time.Duration) time.Duration {
|
||||
valueStr := GetEnv(key, "")
|
||||
if value, err := time.ParseDuration(valueStr); err == nil {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
260
backend/pkg/logger/logger.go
Normal file
260
backend/pkg/logger/logger.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var Logger *logrus.Logger
|
||||
|
||||
// InitLogger initializes the logger
|
||||
func InitLogger() {
|
||||
Logger = logrus.New()
|
||||
|
||||
// Set log level based on environment
|
||||
logLevel := os.Getenv("LOG_LEVEL")
|
||||
if logLevel == "" {
|
||||
logLevel = "info"
|
||||
}
|
||||
|
||||
level, err := logrus.ParseLevel(logLevel)
|
||||
if err != nil {
|
||||
level = logrus.InfoLevel
|
||||
}
|
||||
|
||||
Logger.SetLevel(level)
|
||||
|
||||
// Set formatter
|
||||
Logger.SetFormatter(&logrus.JSONFormatter{
|
||||
TimestampFormat: time.RFC3339,
|
||||
})
|
||||
|
||||
// Set output to stdout by default
|
||||
Logger.SetOutput(os.Stdout)
|
||||
}
|
||||
|
||||
// GetLogger returns the logger instance
|
||||
func GetLogger() *logrus.Logger {
|
||||
if Logger == nil {
|
||||
InitLogger()
|
||||
}
|
||||
return Logger
|
||||
}
|
||||
|
||||
// GinLogger returns a gin middleware for logging
|
||||
func GinLogger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Start timer
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
raw := c.Request.URL.RawQuery
|
||||
|
||||
// Process request
|
||||
c.Next()
|
||||
|
||||
// Stop timer
|
||||
end := time.Now()
|
||||
latency := end.Sub(start)
|
||||
|
||||
// Get client IP
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
// Get method
|
||||
method := c.Request.Method
|
||||
|
||||
// Get status code
|
||||
statusCode := c.Writer.Status()
|
||||
|
||||
// Get error message if any
|
||||
var errorMessage string
|
||||
if len(c.Errors) > 0 {
|
||||
errorMessage = c.Errors.String()
|
||||
}
|
||||
|
||||
// Get body size
|
||||
bodySize := c.Writer.Size()
|
||||
|
||||
// Get request ID if available
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = c.GetString("requestID")
|
||||
}
|
||||
|
||||
// Get user ID if available
|
||||
userID := c.GetString("userID")
|
||||
|
||||
// Log the request
|
||||
entry := GetLogger().WithFields(logrus.Fields{
|
||||
"request_id": requestID,
|
||||
"user_id": userID,
|
||||
"client_ip": clientIP,
|
||||
"method": method,
|
||||
"path": path,
|
||||
"query": raw,
|
||||
"status_code": statusCode,
|
||||
"body_size": bodySize,
|
||||
"latency": latency,
|
||||
"latency_human": latency.String(),
|
||||
"error_message": errorMessage,
|
||||
})
|
||||
|
||||
if statusCode >= 500 {
|
||||
entry.Error()
|
||||
} else if statusCode >= 400 {
|
||||
entry.Warn()
|
||||
} else {
|
||||
entry.Info()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithRequestID adds a request ID to the logger
|
||||
func WithRequestID(requestID string) *logrus.Entry {
|
||||
return GetLogger().WithField("request_id", requestID)
|
||||
}
|
||||
|
||||
// WithUserID adds a user ID to the logger
|
||||
func WithUserID(userID string) *logrus.Entry {
|
||||
return GetLogger().WithField("user_id", userID)
|
||||
}
|
||||
|
||||
// WithError adds an error to the logger
|
||||
func WithError(err error) *logrus.Entry {
|
||||
return GetLogger().WithError(err)
|
||||
}
|
||||
|
||||
// WithField adds a custom field to the logger
|
||||
func WithField(key string, value interface{}) *logrus.Entry {
|
||||
return GetLogger().WithField(key, value)
|
||||
}
|
||||
|
||||
// WithFields adds multiple custom fields to the logger
|
||||
func WithFields(fields logrus.Fields) *logrus.Entry {
|
||||
return GetLogger().WithFields(fields)
|
||||
}
|
||||
|
||||
// Info logs an info message
|
||||
func Info(args ...interface{}) {
|
||||
GetLogger().Info(args...)
|
||||
}
|
||||
|
||||
// Infof logs a formatted info message
|
||||
func Infof(format string, args ...interface{}) {
|
||||
GetLogger().Infof(format, args...)
|
||||
}
|
||||
|
||||
// Warn logs a warning message
|
||||
func Warn(args ...interface{}) {
|
||||
GetLogger().Warn(args...)
|
||||
}
|
||||
|
||||
// Warnf logs a formatted warning message
|
||||
func Warnf(format string, args ...interface{}) {
|
||||
GetLogger().Warnf(format, args...)
|
||||
}
|
||||
|
||||
// Error logs an error message
|
||||
func Error(args ...interface{}) {
|
||||
GetLogger().Error(args...)
|
||||
}
|
||||
|
||||
// Errorf logs a formatted error message
|
||||
func Errorf(format string, args ...interface{}) {
|
||||
GetLogger().Errorf(format, args...)
|
||||
}
|
||||
|
||||
// Fatal logs a fatal message and exits
|
||||
func Fatal(args ...interface{}) {
|
||||
GetLogger().Fatal(args...)
|
||||
}
|
||||
|
||||
// Fatalf logs a formatted fatal message and exits
|
||||
func Fatalf(format string, args ...interface{}) {
|
||||
GetLogger().Fatalf(format, args...)
|
||||
}
|
||||
|
||||
// Debug logs a debug message
|
||||
func Debug(args ...interface{}) {
|
||||
GetLogger().Debug(args...)
|
||||
}
|
||||
|
||||
// Debugf logs a formatted debug message
|
||||
func Debugf(format string, args ...interface{}) {
|
||||
GetLogger().Debugf(format, args...)
|
||||
}
|
||||
|
||||
// Panic logs a panic message and panics
|
||||
func Panic(args ...interface{}) {
|
||||
GetLogger().Panic(args...)
|
||||
}
|
||||
|
||||
// Panicf logs a formatted panic message and panics
|
||||
func Panicf(format string, args ...interface{}) {
|
||||
GetLogger().Panicf(format, args...)
|
||||
}
|
||||
|
||||
// LogDatabaseOperation logs a database operation
|
||||
func LogDatabaseOperation(operation string, table string, duration time.Duration, err error) {
|
||||
entry := GetLogger().WithFields(logrus.Fields{
|
||||
"operation": operation,
|
||||
"table": table,
|
||||
"duration": duration,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
entry.WithError(err).Error("Database operation failed")
|
||||
} else {
|
||||
entry.Info("Database operation completed")
|
||||
}
|
||||
}
|
||||
|
||||
// LogAIInteraction logs an AI interaction
|
||||
func LogAIInteraction(model string, promptLength int, responseLength int, duration time.Duration, success bool, err error) {
|
||||
entry := GetLogger().WithFields(logrus.Fields{
|
||||
"ai_model": model,
|
||||
"prompt_length": promptLength,
|
||||
"response_length": responseLength,
|
||||
"duration": duration,
|
||||
"success": success,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
entry.WithError(err).Error("AI interaction failed")
|
||||
} else {
|
||||
entry.Info("AI interaction completed")
|
||||
}
|
||||
}
|
||||
|
||||
// LogWebSocketEvent logs a WebSocket event
|
||||
func LogWebSocketEvent(event string, clientID string, roomID string, err error) {
|
||||
entry := GetLogger().WithFields(logrus.Fields{
|
||||
"event": event,
|
||||
"client_id": clientID,
|
||||
"room_id": roomID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
entry.WithError(err).Error("WebSocket event failed")
|
||||
} else {
|
||||
entry.Info("WebSocket event completed")
|
||||
}
|
||||
}
|
||||
|
||||
// LogAuthEvent logs an authentication event
|
||||
func LogAuthEvent(event string, userID string, clientIP string, success bool, err error) {
|
||||
entry := GetLogger().WithFields(logrus.Fields{
|
||||
"event": event,
|
||||
"user_id": userID,
|
||||
"client_ip": clientIP,
|
||||
"success": success,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
entry.WithError(err).Error("Authentication event failed")
|
||||
} else {
|
||||
entry.Info("Authentication event completed")
|
||||
}
|
||||
}
|
BIN
backend/seed
Executable file
BIN
backend/seed
Executable file
Binary file not shown.
102
docker-compose.yml
Normal file
102
docker-compose.yml
Normal file
@@ -0,0 +1,102 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# PostgreSQL database
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: support-postgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: support
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- support-network
|
||||
|
||||
# Redis for caching and session management
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: support-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- support-network
|
||||
|
||||
# Backend API
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: support-backend
|
||||
environment:
|
||||
DB_HOST: postgres
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
DB_NAME: support
|
||||
DB_PORT: 5432
|
||||
DB_SSLMODE: disable
|
||||
DB_TIMEZONE: UTC
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
SERVER_HOST: 0.0.0.0
|
||||
SERVER_PORT: 8080
|
||||
AUTH_JWT_SECRET: your-secret-key-change-in-production
|
||||
AUTH_JWT_EXPIRATION_HOURS: 24
|
||||
AI_OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
AI_OPENAI_MODEL: gpt-4
|
||||
AI_OPENAI_MAX_TOKENS: 4000
|
||||
AI_OPENAI_TEMPERATURE: 0.7
|
||||
AI_OPENAI_TOP_P: 1.0
|
||||
AI_LOCAL_ENDPOINT: http://ollama:11434
|
||||
AI_LOCAL_MODEL: llama2
|
||||
AI_LOCAL_MAX_TOKENS: 2000
|
||||
AI_LOCAL_TEMPERATURE: 0.7
|
||||
AI_LOCAL_TOP_P: 1.0
|
||||
LOG_LEVEL: info
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
networks:
|
||||
- support-network
|
||||
|
||||
# Ollama for local LLM
|
||||
ollama:
|
||||
image: ollama/ollama:latest
|
||||
container_name: support-ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama_data:/root/.ollama
|
||||
networks:
|
||||
- support-network
|
||||
|
||||
# Frontend (to be implemented)
|
||||
# frontend:
|
||||
# build:
|
||||
# context: ./frontend
|
||||
# dockerfile: Dockerfile
|
||||
# container_name: support-frontend
|
||||
# ports:
|
||||
# - "3000:3000"
|
||||
# depends_on:
|
||||
# - backend
|
||||
# networks:
|
||||
# - support-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
ollama_data:
|
||||
|
||||
networks:
|
||||
support-network:
|
||||
driver: bridge
|
Reference in New Issue
Block a user