feat: update ai-chat an chat_history

This commit is contained in:
hanif salafi 2025-09-19 14:40:54 +07:00
parent a88bd957e3
commit f1b49d0c63
24 changed files with 6154 additions and 262 deletions

View File

@ -30,4 +30,4 @@ deploy:
services:
- docker:dind
script:
- curl --user $JENKINS_USER:$JENKINS_PWD http://38.47.180.165:8080/job/autodeploy-medols-be/build?token=autodeploymedols
- curl --user $JENKINS_USER:$JENKINS_PWD http://38.47.180.165:8080/job/autodeploy-narasiahli-be/build?token=autodeploynarasiahli

View File

@ -0,0 +1,206 @@
# Chat History Implementation
This document describes the implementation of the chat history functionality based on the old web API code from the `plan/old` folder.
## Overview
The implementation includes:
1. **New Chat History Module**: A complete module for managing chat sessions and messages
2. **Updated AI Chat Module**: Enhanced with proper routing
3. **Database Entities**: New entities for chat sessions and messages
4. **API Endpoints**: RESTful endpoints for all chat history operations
## New Files Created
### Database Entities
- `app/database/entity/chat_sessions.entity.go` - Chat sessions entity
- `app/database/entity/chat_messages_new.entity.go` - Chat messages entity (renamed to avoid conflicts)
### Chat History Module
- `app/module/chat_history/chat_history.module.go` - Module definition
- `app/module/chat_history/chat_history.router.go` - Router configuration
- `app/module/chat_history/request/chat_history.request.go` - Request DTOs
- `app/module/chat_history/response/chat_history.response.go` - Response DTOs
- `app/module/chat_history/mapper/chat_history.mapper.go` - Entity to response mappers
- `app/module/chat_history/repository/chat_history.repository.go` - Data access layer
- `app/module/chat_history/service/chat_history.service.go` - Business logic layer
- `app/module/chat_history/controller/chat_history.controller.go` - API controller
### AI Chat Module Updates
- `app/module/ai_chat/ai_chat.router.go` - Router configuration for AI chat
### Database Migration
- `migrations/001_create_chat_history_tables.sql` - SQL migration script
- `migrations/001_create_chat_history_tables.go` - Go migration script
- `scripts/migrate.go` - Migration runner script
## API Endpoints
### Chat History Endpoints
#### Sessions
- `GET /chat-history/sessions` - Get user's chat sessions
- `GET /chat-history/sessions/{sessionId}` - Get specific session with messages
- `POST /chat-history/sessions` - Create new session
- `PUT /chat-history/sessions/{sessionId}` - Update session
- `DELETE /chat-history/sessions/{sessionId}` - Delete session
#### Messages
- `GET /chat-history/sessions/{sessionId}/messages` - Get session messages
- `POST /chat-history/sessions/{sessionId}/messages` - Create message
- `PUT /chat-history/messages/{messageId}` - Update message
- `DELETE /chat-history/messages/{messageId}` - Delete message
#### Combined Operations
- `POST /chat-history/save` - Save complete chat history (sessions + messages)
### AI Chat Endpoints (Updated)
- `GET /ai-chat/sessions` - Get user's AI chat sessions
- `GET /ai-chat/sessions/{id}` - Get specific AI chat session
- `POST /ai-chat/sessions` - Create AI chat session
- `PUT /ai-chat/sessions/{id}` - Update AI chat session
- `DELETE /ai-chat/sessions/{id}` - Delete AI chat session
- `GET /ai-chat/sessions/{sessionId}/messages` - Get AI chat messages
- `POST /ai-chat/sessions/{sessionId}/messages` - Send AI chat message
- `PUT /ai-chat/sessions/{sessionId}/messages/{messageId}` - Update AI chat message
- `DELETE /ai-chat/sessions/{sessionId}/messages/{messageId}` - Delete AI chat message
- `GET /ai-chat/logs` - Get user's AI chat logs
- `GET /ai-chat/logs/{id}` - Get specific AI chat log
## Database Schema
### chat_sessions Table
```sql
CREATE TABLE chat_sessions (
id SERIAL PRIMARY KEY,
session_id VARCHAR(255) NOT NULL UNIQUE,
user_id INTEGER NOT NULL,
agent_id VARCHAR(255) NOT NULL,
title VARCHAR(255) NOT NULL DEFAULT 'Chat Session',
message_count INTEGER DEFAULT 0,
status VARCHAR(50) DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
### chat_messages_new Table
```sql
CREATE TABLE chat_messages_new (
id SERIAL PRIMARY KEY,
session_id VARCHAR(255) NOT NULL,
message_type VARCHAR(50) NOT NULL CHECK (message_type IN ('user', 'assistant')),
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
## Key Features Implemented
### 1. Session Management
- Create, read, update, delete chat sessions
- Session-based architecture with unique session IDs
- User ownership validation
- Message count tracking
### 2. Message Management
- Create, read, update, delete messages within sessions
- Support for user and assistant message types
- Automatic message count updates
- Cascade deletion when sessions are deleted
### 3. Combined Operations
- Save complete chat history in one operation
- Update existing sessions or create new ones
- Bulk message operations
### 4. Security
- User authentication required for all operations
- User ownership validation for all resources
- Proper error handling and validation
## Migration Instructions
### 1. Run Database Migration
```bash
# Option 1: Run the Go migration script
go run scripts/migrate.go
# Option 2: Run SQL migration manually
psql -d your_database -f migrations/001_create_chat_history_tables.sql
```
### 2. Update Application
The application has been updated to include:
- New modules in `main.go`
- Updated router configuration
- New API endpoints
### 3. Test the Implementation
```bash
# Start the application
go run main.go
# Test endpoints
curl -X GET "http://localhost:8080/chat-history/sessions" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X POST "http://localhost:8080/chat-history/save" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"user_id": 1,
"agent_id": "agent-123",
"session_id": "session-456",
"title": "Test Chat",
"messages": [
{"type": "user", "content": "Hello"},
{"type": "assistant", "content": "Hi there!"}
]
}'
```
## Differences from Old Code
### Architecture Changes
- **Language**: Converted from TypeScript/JavaScript to Go
- **Framework**: Converted from Next.js API routes to Fiber (Go web framework)
- **Database**: Converted from SQLite to PostgreSQL (via GORM)
- **Structure**: Implemented clean architecture with repository, service, and controller layers
### API Changes
- **Authentication**: Added Bearer token authentication
- **Validation**: Added comprehensive request validation
- **Error Handling**: Standardized error responses
- **Pagination**: Added pagination support for list endpoints
### Database Changes
- **Schema**: Adapted SQLite schema to PostgreSQL
- **Relationships**: Added proper foreign key relationships
- **Indexes**: Added performance indexes
- **Constraints**: Added data validation constraints
## Future Enhancements
1. **Real-time Features**: Add WebSocket support for real-time chat
2. **File Attachments**: Support for file uploads in messages
3. **Message Search**: Full-text search across messages
4. **Analytics**: Chat analytics and reporting
5. **Export**: Export chat history to various formats
6. **Archiving**: Automatic archiving of old sessions
## Troubleshooting
### Common Issues
1. **Migration Errors**: Ensure database connection is properly configured
2. **Authentication Errors**: Verify Bearer token is valid and user exists
3. **Validation Errors**: Check request payload matches expected schema
4. **Permission Errors**: Ensure user owns the resource being accessed
### Debug Mode
Enable debug logging by setting the log level to debug in your configuration.
## Support
For issues or questions regarding this implementation, please refer to the application logs or contact the development team.

View File

@ -0,0 +1,14 @@
package entity
import (
"time"
)
type ChatMessagesNew struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
SessionID string `json:"session_id" gorm:"type:varchar;not null;index"`
MessageType string `json:"message_type" gorm:"type:varchar;not null;check:message_type IN ('user', 'assistant')"`
Content string `json:"content" gorm:"type:text;not null"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
Session *ChatSessions `json:"session" gorm:"foreignKey:SessionID;references:SessionID"`
}

View File

@ -0,0 +1,20 @@
package entity
import (
"narasi-ahli-be/app/database/entity/users"
"time"
)
type ChatSessions struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
SessionID string `json:"session_id" gorm:"type:varchar;not null;unique;index"`
UserID uint `json:"user_id" gorm:"type:int4;not null;index"`
AgentID string `json:"agent_id" gorm:"type:varchar;not null"`
Title string `json:"title" gorm:"type:varchar;not null"`
MessageCount int `json:"message_count" gorm:"type:int4;default:0"`
Status string `json:"status" gorm:"type:varchar;default:'active'"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
User *users.Users `json:"user" gorm:"foreignKey:UserID;references:ID"`
Messages []ChatMessages `json:"messages" gorm:"foreignKey:SessionID;references:SessionID"`
}

View File

@ -4,24 +4,62 @@ import (
"narasi-ahli-be/app/module/ai_chat/controller"
"narasi-ahli-be/app/module/ai_chat/repository"
"narasi-ahli-be/app/module/ai_chat/service"
usersRepository "narasi-ahli-be/app/module/users/repository"
"github.com/rs/zerolog"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
)
var Module = fx.Options(
fx.Provide(
repository.NewAIChatRepository,
service.NewAIChatService,
controller.NewAIChatController,
),
fx.Invoke(func(
aiChatController controller.AIChatController,
usersRepo usersRepository.UsersRepository,
log zerolog.Logger,
) {
log.Info().Msg("AI Chat module initialized successfully")
}),
// struct of AIChatRouter
type AIChatRouter struct {
App fiber.Router
Controller controller.AIChatController
}
// register bulky of AI Chat module
var NewAIChatModule = fx.Options(
// register repository of AI Chat module
fx.Provide(repository.NewAIChatRepository),
// register service of AI Chat module
fx.Provide(service.NewAIChatService),
// register controller of AI Chat module
fx.Provide(controller.NewAIChatController),
// register router of AI Chat module
fx.Provide(NewAIChatRouter),
)
// init AIChatRouter
func NewAIChatRouter(fiber *fiber.App, controller controller.AIChatController) *AIChatRouter {
return &AIChatRouter{
App: fiber,
Controller: controller,
}
}
// register routes of AI Chat module
func (_i *AIChatRouter) RegisterAIChatRoutes() {
// define controllers
aiChatController := _i.Controller
// define routes
_i.App.Route("/ai-chat", func(router fiber.Router) {
// Sessions routes
router.Get("/sessions", aiChatController.GetUserSessions)
router.Get("/sessions/:id", aiChatController.GetSession)
router.Post("/sessions", aiChatController.CreateSession)
router.Put("/sessions/:id", aiChatController.UpdateSession)
router.Delete("/sessions/:id", aiChatController.DeleteSession)
// Messages routes
router.Get("/sessions/:sessionId/messages", aiChatController.GetSessionMessages)
router.Post("/sessions/:sessionId/messages", aiChatController.SendMessage)
router.Put("/sessions/:sessionId/messages/:messageId", aiChatController.UpdateMessage)
router.Delete("/sessions/:sessionId/messages/:messageId", aiChatController.DeleteMessage)
// Logs routes
router.Get("/logs", aiChatController.GetUserLogs)
router.Get("/logs/:id", aiChatController.GetLog)
})
}

View File

@ -0,0 +1,64 @@
package chat_history
import (
"narasi-ahli-be/app/module/chat_history/controller"
"narasi-ahli-be/app/module/chat_history/repository"
"narasi-ahli-be/app/module/chat_history/service"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
)
// struct of ChatHistoryRouter
type ChatHistoryRouter struct {
App fiber.Router
Controller controller.ChatHistoryController
}
// register bulky of Chat History module
var NewChatHistoryModule = fx.Options(
// register repository of Chat History module
fx.Provide(repository.NewChatHistoryRepository),
// register service of Chat History module
fx.Provide(service.NewChatHistoryService),
// register controller of Chat History module
fx.Provide(controller.NewChatHistoryController),
// register router of Chat History module
fx.Provide(NewChatHistoryRouter),
)
// init ChatHistoryRouter
func NewChatHistoryRouter(fiber *fiber.App, controller controller.ChatHistoryController) *ChatHistoryRouter {
return &ChatHistoryRouter{
App: fiber,
Controller: controller,
}
}
// register routes of Chat History module
func (_i *ChatHistoryRouter) RegisterChatHistoryRoutes() {
// define controllers
chatHistoryController := _i.Controller
// define routes
_i.App.Route("/chat-history", func(router fiber.Router) {
// Sessions routes
router.Get("/sessions", chatHistoryController.GetUserSessions)
router.Get("/sessions/:sessionId", chatHistoryController.GetSession)
router.Post("/sessions", chatHistoryController.CreateSession)
router.Put("/sessions/:sessionId", chatHistoryController.UpdateSession)
router.Delete("/sessions/:sessionId", chatHistoryController.DeleteSession)
// Messages routes
router.Get("/sessions/:sessionId/messages", chatHistoryController.GetSessionMessages)
router.Post("/sessions/:sessionId/messages", chatHistoryController.CreateMessage)
router.Put("/messages/:messageId", chatHistoryController.UpdateMessage)
router.Delete("/messages/:messageId", chatHistoryController.DeleteMessage)
// Combined operations
router.Post("/save", chatHistoryController.SaveChatHistory)
})
}

View File

@ -0,0 +1,424 @@
package controller
import (
"narasi-ahli-be/app/module/chat_history/request"
"narasi-ahli-be/app/module/chat_history/service"
"narasi-ahli-be/utils/paginator"
utilRes "narasi-ahli-be/utils/response"
utilVal "narasi-ahli-be/utils/validator"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
)
type chatHistoryController struct {
chatHistoryService service.ChatHistoryService
Log zerolog.Logger
}
type ChatHistoryController interface {
// Chat History Sessions
GetUserSessions(c *fiber.Ctx) error
GetSession(c *fiber.Ctx) error
CreateSession(c *fiber.Ctx) error
UpdateSession(c *fiber.Ctx) error
DeleteSession(c *fiber.Ctx) error
// Chat History Messages
GetSessionMessages(c *fiber.Ctx) error
CreateMessage(c *fiber.Ctx) error
UpdateMessage(c *fiber.Ctx) error
DeleteMessage(c *fiber.Ctx) error
// Combined operations
SaveChatHistory(c *fiber.Ctx) error
}
func NewChatHistoryController(chatHistoryService service.ChatHistoryService, log zerolog.Logger) ChatHistoryController {
return &chatHistoryController{
chatHistoryService: chatHistoryService,
Log: log,
}
}
// Get User Sessions
// @Summary Get user chat history sessions
// @Description API for getting all chat history sessions for authenticated user
// @Tags Chat History
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param req query request.ChatHistorySessionsQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /chat-history/sessions [get]
func (_i *chatHistoryController) GetUserSessions(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
reqContext := request.ChatHistorySessionsQueryRequestContext{
AgentID: c.Query("agent_id"),
SessionID: c.Query("session_id"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
sessionsData, paging, err := _i.chatHistoryService.GetUserSessions(authHeader, req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Chat history sessions successfully retrieved"},
Data: sessionsData,
Meta: paging,
})
}
// Get Session
// @Summary Get one chat history session with messages
// @Description API for getting one chat history session with all its messages
// @Tags Chat History
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param sessionId path string true "Session ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /chat-history/sessions/{sessionId} [get]
func (_i *chatHistoryController) GetSession(c *fiber.Ctx) error {
sessionID := c.Params("sessionId")
if sessionID == "" {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Session ID is required"},
})
}
authHeader := c.Get("Authorization")
sessionData, err := _i.chatHistoryService.GetSession(authHeader, sessionID)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Chat history session successfully retrieved"},
Data: sessionData,
})
}
// Create Session
// @Summary Create chat history session
// @Description API for create chat history session
// @Tags Chat History
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.ChatHistorySessionsCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /chat-history/sessions [post]
func (_i *chatHistoryController) CreateSession(c *fiber.Ctx) error {
req := new(request.ChatHistorySessionsCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authHeader := c.Get("Authorization")
dataResult, err := _i.chatHistoryService.CreateSession(authHeader, *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Chat history session successfully created"},
Data: dataResult,
})
}
// Update Session
// @Summary Update chat history session
// @Description API for update chat history session
// @Tags Chat History
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param sessionId path string true "Session ID"
// @Param payload body request.ChatHistorySessionsUpdateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /chat-history/sessions/{sessionId} [put]
func (_i *chatHistoryController) UpdateSession(c *fiber.Ctx) error {
sessionID := c.Params("sessionId")
if sessionID == "" {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Session ID is required"},
})
}
req := new(request.ChatHistorySessionsUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authHeader := c.Get("Authorization")
err := _i.chatHistoryService.UpdateSession(authHeader, sessionID, *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Chat history session successfully updated"},
})
}
// Delete Session
// @Summary Delete chat history session
// @Description API for delete chat history session
// @Tags Chat History
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param sessionId path string true "Session ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /chat-history/sessions/{sessionId} [delete]
func (_i *chatHistoryController) DeleteSession(c *fiber.Ctx) error {
sessionID := c.Params("sessionId")
if sessionID == "" {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Session ID is required"},
})
}
authHeader := c.Get("Authorization")
err := _i.chatHistoryService.DeleteSession(authHeader, sessionID)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Chat history session successfully deleted"},
})
}
// Get Session Messages
// @Summary Get chat history session messages
// @Description API for getting all messages in a chat history session
// @Tags Chat History
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param sessionId path string true "Session ID"
// @Param req query request.ChatHistoryMessagesQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /chat-history/sessions/{sessionId}/messages [get]
func (_i *chatHistoryController) GetSessionMessages(c *fiber.Ctx) error {
sessionID := c.Params("sessionId")
if sessionID == "" {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Session ID is required"},
})
}
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
req := request.ChatHistoryMessagesQueryRequest{
SessionID: sessionID,
Pagination: paginate,
}
messagesData, paging, err := _i.chatHistoryService.GetSessionMessages(authHeader, sessionID, req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Chat history messages successfully retrieved"},
Data: messagesData,
Meta: paging,
})
}
// Create Message
// @Summary Create chat history message
// @Description API for creating a message in a chat history session
// @Tags Chat History
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param sessionId path string true "Session ID"
// @Param payload body request.ChatHistoryMessagesCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /chat-history/sessions/{sessionId}/messages [post]
func (_i *chatHistoryController) CreateMessage(c *fiber.Ctx) error {
sessionID := c.Params("sessionId")
if sessionID == "" {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Session ID is required"},
})
}
req := new(request.ChatHistoryMessagesCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
// Set session ID from URL parameter
req.SessionID = sessionID
authHeader := c.Get("Authorization")
dataResult, err := _i.chatHistoryService.CreateMessage(authHeader, *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Chat history message successfully created"},
Data: dataResult,
})
}
// Update Message
// @Summary Update chat history message
// @Description API for update chat history message
// @Tags Chat History
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param messageId path int true "Message ID"
// @Param payload body request.ChatHistoryMessagesUpdateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /chat-history/messages/{messageId} [put]
func (_i *chatHistoryController) UpdateMessage(c *fiber.Ctx) error {
messageId, err := strconv.ParseUint(c.Params("messageId"), 10, 0)
if err != nil {
return err
}
req := new(request.ChatHistoryMessagesUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authHeader := c.Get("Authorization")
if err := _i.chatHistoryService.UpdateMessage(authHeader, uint(messageId), *req); err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Chat history message successfully updated"},
})
}
// Delete Message
// @Summary Delete chat history message
// @Description API for delete chat history message
// @Tags Chat History
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param messageId path int true "Message ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /chat-history/messages/{messageId} [delete]
func (_i *chatHistoryController) DeleteMessage(c *fiber.Ctx) error {
messageId, err := strconv.ParseUint(c.Params("messageId"), 10, 0)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
if err := _i.chatHistoryService.DeleteMessage(authHeader, uint(messageId)); err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Chat history message successfully deleted"},
})
}
// Save Chat History
// @Summary Save chat history (sessions and messages)
// @Description API for saving complete chat history including sessions and messages
// @Tags Chat History
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.ChatHistorySessionsCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /chat-history/save [post]
func (_i *chatHistoryController) SaveChatHistory(c *fiber.Ctx) error {
req := new(request.ChatHistorySessionsCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authHeader := c.Get("Authorization")
dataResult, err := _i.chatHistoryService.SaveChatHistory(authHeader, *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Chat history saved successfully"},
Data: dataResult,
})
}

View File

@ -0,0 +1,77 @@
package mapper
import (
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/app/module/chat_history/response"
)
// Chat History Sessions Mapper
func ChatHistorySessionsResponseMapper(entity *entity.ChatSessions) *response.ChatHistorySessionsResponse {
if entity == nil {
return nil
}
return &response.ChatHistorySessionsResponse{
ID: entity.ID,
SessionID: entity.SessionID,
UserID: entity.UserID,
AgentID: entity.AgentID,
Title: entity.Title,
MessageCount: entity.MessageCount,
Status: entity.Status,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
}
}
// Chat History Messages Mapper
func ChatHistoryMessagesResponseMapper(entity *entity.ChatMessagesNew) *response.ChatHistoryMessagesResponse {
if entity == nil {
return nil
}
return &response.ChatHistoryMessagesResponse{
ID: entity.ID,
SessionID: entity.SessionID,
MessageType: entity.MessageType,
Content: entity.Content,
CreatedAt: entity.CreatedAt,
}
}
// Chat History Session with Messages Mapper
func ChatHistorySessionWithMessagesResponseMapper(session *entity.ChatSessions, messages []*entity.ChatMessagesNew) *response.ChatHistorySessionWithMessagesResponse {
if session == nil {
return nil
}
sessionResponse := ChatHistorySessionsResponseMapper(session)
var messagesResponse []response.ChatHistoryMessagesResponse
for _, message := range messages {
if message != nil {
messagesResponse = append(messagesResponse, *ChatHistoryMessagesResponseMapper(message))
}
}
return &response.ChatHistorySessionWithMessagesResponse{
Session: *sessionResponse,
Messages: messagesResponse,
}
}
// Chat History List Mapper
func ChatHistoryListResponseMapper(sessions []*entity.ChatSessions) *response.ChatHistoryListResponse {
var sessionsResponse []response.ChatHistorySessionsResponse
for _, session := range sessions {
if session != nil {
sessionsResponse = append(sessionsResponse, *ChatHistorySessionsResponseMapper(session))
}
}
return &response.ChatHistoryListResponse{
Sessions: sessionsResponse,
Total: len(sessionsResponse),
}
}

View File

@ -0,0 +1,159 @@
package repository
import (
"narasi-ahli-be/app/database"
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/app/module/chat_history/request"
"narasi-ahli-be/utils/paginator"
)
type chatHistoryRepository struct {
DB *database.Database
}
type ChatHistoryRepository interface {
// Chat Sessions
GetUserSessions(userId uint, req request.ChatHistorySessionsQueryRequest) (sessions []*entity.ChatSessions, paging paginator.Pagination, err error)
FindSessionByID(id uint) (session *entity.ChatSessions, err error)
FindSessionBySessionID(sessionID string) (session *entity.ChatSessions, err error)
FindSessionByUserAndSessionID(userID uint, sessionID string) (session *entity.ChatSessions, err error)
CreateSession(session *entity.ChatSessions) (result *entity.ChatSessions, err error)
UpdateSession(sessionID string, session *entity.ChatSessions) (err error)
DeleteSession(sessionID string) (err error)
IncrementMessageCount(sessionID string) (err error)
// Chat Messages
GetSessionMessages(sessionID string, req request.ChatHistoryMessagesQueryRequest) (messages []*entity.ChatMessagesNew, paging paginator.Pagination, err error)
CreateMessage(message *entity.ChatMessagesNew) (result *entity.ChatMessagesNew, err error)
UpdateMessage(messageID uint, message *entity.ChatMessagesNew) (err error)
DeleteMessage(messageID uint) (err error)
DeleteMessagesBySessionID(sessionID string) (err error)
GetLastMessage(sessionID string) (message *entity.ChatMessagesNew, err error)
}
func NewChatHistoryRepository(db *database.Database) ChatHistoryRepository {
return &chatHistoryRepository{
DB: db,
}
}
// Chat Sessions methods
func (_i *chatHistoryRepository) GetUserSessions(userId uint, req request.ChatHistorySessionsQueryRequest) (sessions []*entity.ChatSessions, paging paginator.Pagination, err error) {
query := _i.DB.DB.Where("user_id = ?", userId)
// Apply filters
if req.AgentID != nil {
query = query.Where("agent_id = ?", *req.AgentID)
}
if req.SessionID != nil {
query = query.Where("session_id = ?", *req.SessionID)
}
// Include user relationship
query = query.Preload("User")
// Order by updated_at desc (most recent first)
query = query.Order("updated_at DESC")
// Apply pagination
var count int64
query.Count(&count)
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&sessions).Error
paging = *req.Pagination
return
}
func (_i *chatHistoryRepository) FindSessionByID(id uint) (session *entity.ChatSessions, err error) {
err = _i.DB.DB.Where("id = ?", id).Preload("User").First(&session).Error
return
}
func (_i *chatHistoryRepository) FindSessionBySessionID(sessionID string) (session *entity.ChatSessions, err error) {
err = _i.DB.DB.Where("session_id = ?", sessionID).Preload("User").First(&session).Error
return
}
func (_i *chatHistoryRepository) FindSessionByUserAndSessionID(userID uint, sessionID string) (session *entity.ChatSessions, err error) {
err = _i.DB.DB.Where("user_id = ? AND session_id = ?", userID, sessionID).Preload("User").First(&session).Error
return
}
func (_i *chatHistoryRepository) CreateSession(session *entity.ChatSessions) (result *entity.ChatSessions, err error) {
err = _i.DB.DB.Create(session).Error
if err != nil {
return nil, err
}
// Reload with relationships
err = _i.DB.DB.Preload("User").First(&result, session.ID).Error
return
}
func (_i *chatHistoryRepository) UpdateSession(sessionID string, session *entity.ChatSessions) (err error) {
err = _i.DB.DB.Where("session_id = ?", sessionID).Updates(session).Error
return
}
func (_i *chatHistoryRepository) DeleteSession(sessionID string) (err error) {
err = _i.DB.DB.Where("session_id = ?", sessionID).Delete(&entity.ChatSessions{}).Error
return
}
func (_i *chatHistoryRepository) IncrementMessageCount(sessionID string) (err error) {
err = _i.DB.DB.Exec("UPDATE chat_sessions SET message_count = message_count + 1 WHERE session_id = ?", sessionID).Error
return
}
// Chat Messages methods
func (_i *chatHistoryRepository) GetSessionMessages(sessionID string, req request.ChatHistoryMessagesQueryRequest) (messages []*entity.ChatMessagesNew, paging paginator.Pagination, err error) {
query := _i.DB.DB.Where("session_id = ?", sessionID)
// Order by created_at asc (oldest first for chat)
query = query.Order("created_at ASC")
// Apply pagination
var count int64
query.Count(&count)
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&messages).Error
paging = *req.Pagination
return
}
func (_i *chatHistoryRepository) CreateMessage(message *entity.ChatMessagesNew) (result *entity.ChatMessagesNew, err error) {
err = _i.DB.DB.Create(message).Error
if err != nil {
return nil, err
}
// Reload
err = _i.DB.DB.First(&result, message.ID).Error
return
}
func (_i *chatHistoryRepository) UpdateMessage(messageID uint, message *entity.ChatMessagesNew) (err error) {
err = _i.DB.DB.Model(&entity.ChatMessagesNew{}).Where("id = ?", messageID).Updates(message).Error
return
}
func (_i *chatHistoryRepository) DeleteMessage(messageID uint) (err error) {
err = _i.DB.DB.Where("id = ?", messageID).Delete(&entity.ChatMessagesNew{}).Error
return
}
func (_i *chatHistoryRepository) DeleteMessagesBySessionID(sessionID string) (err error) {
err = _i.DB.DB.Where("session_id = ?", sessionID).Delete(&entity.ChatMessagesNew{}).Error
return
}
func (_i *chatHistoryRepository) GetLastMessage(sessionID string) (message *entity.ChatMessagesNew, err error) {
err = _i.DB.DB.Where("session_id = ?", sessionID).Order("created_at DESC").First(&message).Error
return
}

View File

@ -0,0 +1,131 @@
package request
import (
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/utils/paginator"
)
// Chat History Sessions Request DTOs
type ChatHistorySessionsQueryRequest struct {
UserID *uint `json:"user_id"`
AgentID *string `json:"agent_id"`
SessionID *string `json:"session_id"`
Pagination *paginator.Pagination `json:"pagination"`
}
type ChatHistorySessionsCreateRequest struct {
UserID uint `json:"user_id" validate:"required"`
AgentID string `json:"agent_id" validate:"required"`
SessionID string `json:"session_id" validate:"required"`
Title *string `json:"title"`
Messages []ChatMessageRequest `json:"messages"`
}
type ChatMessageRequest struct {
Type string `json:"type" validate:"required,oneof=user assistant"`
Content string `json:"content" validate:"required"`
}
func (req ChatHistorySessionsCreateRequest) ToEntity() *entity.ChatSessions {
title := "Chat Session"
if req.Title != nil {
title = *req.Title
}
return &entity.ChatSessions{
SessionID: req.SessionID,
UserID: req.UserID,
AgentID: req.AgentID,
Title: title,
MessageCount: len(req.Messages),
Status: "active",
}
}
type ChatHistorySessionsUpdateRequest struct {
Title *string `json:"title"`
Status *string `json:"status" validate:"omitempty,oneof=active archived deleted"`
}
func (req ChatHistorySessionsUpdateRequest) ToEntity() *entity.ChatSessions {
entity := &entity.ChatSessions{}
if req.Title != nil {
entity.Title = *req.Title
}
if req.Status != nil {
entity.Status = *req.Status
}
return entity
}
// Chat History Messages Request DTOs
type ChatHistoryMessagesQueryRequest struct {
SessionID string `json:"session_id" validate:"required"`
Pagination *paginator.Pagination `json:"pagination"`
}
type ChatHistoryMessagesCreateRequest struct {
SessionID string `json:"session_id" validate:"required"`
MessageType string `json:"message_type" validate:"required,oneof=user assistant"`
Content string `json:"content" validate:"required"`
}
func (req ChatHistoryMessagesCreateRequest) ToEntity() *entity.ChatMessagesNew {
return &entity.ChatMessagesNew{
SessionID: req.SessionID,
MessageType: req.MessageType,
Content: req.Content,
}
}
type ChatHistoryMessagesUpdateRequest struct {
Content string `json:"content" validate:"required"`
}
func (req ChatHistoryMessagesUpdateRequest) ToEntity() *entity.ChatMessagesNew {
return &entity.ChatMessagesNew{
Content: req.Content,
}
}
// Context Request DTOs for query parameters
type ChatHistorySessionsQueryRequestContext struct {
UserID string `json:"user_id"`
AgentID string `json:"agent_id"`
SessionID string `json:"session_id"`
}
func (req ChatHistorySessionsQueryRequestContext) ToParamRequest() ChatHistorySessionsQueryRequest {
var request ChatHistorySessionsQueryRequest
if userIDStr := req.UserID; userIDStr != "" {
// Parse user ID from string to uint
// This will be handled in the controller
}
if agentID := req.AgentID; agentID != "" {
request.AgentID = &agentID
}
if sessionID := req.SessionID; sessionID != "" {
request.SessionID = &sessionID
}
return request
}
type ChatHistoryMessagesQueryRequestContext struct {
SessionID string `json:"session_id"`
}
func (req ChatHistoryMessagesQueryRequestContext) ToParamRequest() ChatHistoryMessagesQueryRequest {
var request ChatHistoryMessagesQueryRequest
if sessionID := req.SessionID; sessionID != "" {
request.SessionID = sessionID
}
return request
}

View File

@ -0,0 +1,39 @@
package response
import (
"time"
)
// Chat History Sessions Response DTOs
type ChatHistorySessionsResponse struct {
ID uint `json:"id"`
SessionID string `json:"session_id"`
UserID uint `json:"user_id"`
AgentID string `json:"agent_id"`
Title string `json:"title"`
MessageCount int `json:"message_count"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Chat History Messages Response DTOs
type ChatHistoryMessagesResponse struct {
ID uint `json:"id"`
SessionID string `json:"session_id"`
MessageType string `json:"message_type"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
}
// Combined Response for Session with Messages
type ChatHistorySessionWithMessagesResponse struct {
Session ChatHistorySessionsResponse `json:"session"`
Messages []ChatHistoryMessagesResponse `json:"messages"`
}
// Chat History List Response
type ChatHistoryListResponse struct {
Sessions []ChatHistorySessionsResponse `json:"sessions"`
Total int `json:"total"`
}

View File

@ -0,0 +1,317 @@
package service
import (
"errors"
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/app/module/chat_history/mapper"
"narasi-ahli-be/app/module/chat_history/repository"
"narasi-ahli-be/app/module/chat_history/request"
"narasi-ahli-be/app/module/chat_history/response"
usersRepository "narasi-ahli-be/app/module/users/repository"
"narasi-ahli-be/utils/paginator"
utilSvc "narasi-ahli-be/utils/service"
"github.com/rs/zerolog"
)
type chatHistoryService struct {
Repo repository.ChatHistoryRepository
UsersRepo usersRepository.UsersRepository
Log zerolog.Logger
}
type ChatHistoryService interface {
// Sessions
GetUserSessions(authToken string, req request.ChatHistorySessionsQueryRequest) (sessions []*response.ChatHistorySessionsResponse, paging paginator.Pagination, err error)
GetSession(authToken string, sessionID string) (session *response.ChatHistorySessionWithMessagesResponse, err error)
CreateSession(authToken string, req request.ChatHistorySessionsCreateRequest) (session *response.ChatHistorySessionsResponse, err error)
UpdateSession(authToken string, sessionID string, req request.ChatHistorySessionsUpdateRequest) (err error)
DeleteSession(authToken string, sessionID string) error
// Messages
GetSessionMessages(authToken string, sessionID string, req request.ChatHistoryMessagesQueryRequest) (messages []*response.ChatHistoryMessagesResponse, paging paginator.Pagination, err error)
CreateMessage(authToken string, req request.ChatHistoryMessagesCreateRequest) (message *response.ChatHistoryMessagesResponse, err error)
UpdateMessage(authToken string, messageID uint, req request.ChatHistoryMessagesUpdateRequest) (err error)
DeleteMessage(authToken string, messageID uint) error
// Combined operations
SaveChatHistory(authToken string, req request.ChatHistorySessionsCreateRequest) (session *response.ChatHistorySessionsResponse, err error)
}
func NewChatHistoryService(repo repository.ChatHistoryRepository, usersRepo usersRepository.UsersRepository, log zerolog.Logger) ChatHistoryService {
return &chatHistoryService{
Repo: repo,
UsersRepo: usersRepo,
Log: log,
}
}
// Sessions methods
func (_i *chatHistoryService) GetUserSessions(authToken string, req request.ChatHistorySessionsQueryRequest) (sessions []*response.ChatHistorySessionsResponse, paging paginator.Pagination, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
// Set user ID from auth token
req.UserID = &userInfo.ID
results, paging, err := _i.Repo.GetUserSessions(userInfo.ID, req)
if err != nil {
return
}
for _, result := range results {
sessions = append(sessions, mapper.ChatHistorySessionsResponseMapper(result))
}
return
}
func (_i *chatHistoryService) GetSession(authToken string, sessionID string) (session *response.ChatHistorySessionWithMessagesResponse, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
// Verify session belongs to user
sessionEntity, err := _i.Repo.FindSessionByUserAndSessionID(userInfo.ID, sessionID)
if err != nil {
return nil, err
}
// Get messages for this session
messagesReq := request.ChatHistoryMessagesQueryRequest{
SessionID: sessionID,
Pagination: &paginator.Pagination{
Limit: 1000, // Get all messages for now
},
}
messages, _, err := _i.Repo.GetSessionMessages(sessionID, messagesReq)
if err != nil {
return nil, err
}
return mapper.ChatHistorySessionWithMessagesResponseMapper(sessionEntity, messages), nil
}
func (_i *chatHistoryService) CreateSession(authToken string, req request.ChatHistorySessionsCreateRequest) (session *response.ChatHistorySessionsResponse, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
// Set user ID from auth token
req.UserID = userInfo.ID
entity := req.ToEntity()
result, err := _i.Repo.CreateSession(entity)
if err != nil {
return nil, err
}
return mapper.ChatHistorySessionsResponseMapper(result), nil
}
func (_i *chatHistoryService) UpdateSession(authToken string, sessionID string, req request.ChatHistorySessionsUpdateRequest) (err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
_i.Log.Info().Interface("data", req).Msg("Updating chat history session")
// Check if session exists and belongs to user
existing, err := _i.Repo.FindSessionByUserAndSessionID(userInfo.ID, sessionID)
if err != nil {
return err
}
if existing == nil {
return errors.New("chat history session not found")
}
entity := req.ToEntity()
return _i.Repo.UpdateSession(sessionID, entity)
}
func (_i *chatHistoryService) DeleteSession(authToken string, sessionID string) error {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
_i.Log.Info().Uint("userId", userInfo.ID).Str("sessionId", sessionID).Msg("Deleting chat history session")
// Check if session exists and belongs to user
existing, err := _i.Repo.FindSessionByUserAndSessionID(userInfo.ID, sessionID)
if err != nil {
return err
}
if existing == nil {
return errors.New("chat history session not found")
}
return _i.Repo.DeleteSession(sessionID)
}
// Messages methods
func (_i *chatHistoryService) GetSessionMessages(authToken string, sessionID string, req request.ChatHistoryMessagesQueryRequest) (messages []*response.ChatHistoryMessagesResponse, paging paginator.Pagination, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
// Verify session belongs to user
_, err = _i.Repo.FindSessionByUserAndSessionID(userInfo.ID, sessionID)
if err != nil {
return nil, paginator.Pagination{}, err
}
results, paging, err := _i.Repo.GetSessionMessages(sessionID, req)
if err != nil {
return
}
for _, result := range results {
messages = append(messages, mapper.ChatHistoryMessagesResponseMapper(result))
}
return
}
func (_i *chatHistoryService) CreateMessage(authToken string, req request.ChatHistoryMessagesCreateRequest) (message *response.ChatHistoryMessagesResponse, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
_i.Log.Info().Interface("data", req).Msg("Creating chat history message")
// Verify session belongs to user
_, err = _i.Repo.FindSessionByUserAndSessionID(userInfo.ID, req.SessionID)
if err != nil {
return nil, err
}
entity := req.ToEntity()
result, err := _i.Repo.CreateMessage(entity)
if err != nil {
return nil, err
}
// Increment message count in session
err = _i.Repo.IncrementMessageCount(req.SessionID)
if err != nil {
_i.Log.Error().Err(err).Msg("Failed to increment message count")
}
return mapper.ChatHistoryMessagesResponseMapper(result), nil
}
func (_i *chatHistoryService) UpdateMessage(authToken string, messageID uint, req request.ChatHistoryMessagesUpdateRequest) (err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
// Get message to verify it belongs to user's session
messages, _, err := _i.Repo.GetSessionMessages("", request.ChatHistoryMessagesQueryRequest{})
if err != nil {
return err
}
// Find the specific message
var targetMessage *entity.ChatMessagesNew
for _, msg := range messages {
if msg.ID == messageID {
targetMessage = msg
break
}
}
if targetMessage == nil {
return errors.New("message not found")
}
// Verify session belongs to user
_, err = _i.Repo.FindSessionByUserAndSessionID(userInfo.ID, targetMessage.SessionID)
if err != nil {
return err
}
entity := req.ToEntity()
return _i.Repo.UpdateMessage(messageID, entity)
}
func (_i *chatHistoryService) DeleteMessage(authToken string, messageID uint) error {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
// Get message to verify it belongs to user's session
messages, _, err := _i.Repo.GetSessionMessages("", request.ChatHistoryMessagesQueryRequest{})
if err != nil {
return err
}
// Find the specific message
var targetMessage *entity.ChatMessagesNew
for _, msg := range messages {
if msg.ID == messageID {
targetMessage = msg
break
}
}
if targetMessage == nil {
return errors.New("message not found")
}
// Verify session belongs to user
_, err = _i.Repo.FindSessionByUserAndSessionID(userInfo.ID, targetMessage.SessionID)
if err != nil {
return err
}
return _i.Repo.DeleteMessage(messageID)
}
// Combined operations
func (_i *chatHistoryService) SaveChatHistory(authToken string, req request.ChatHistorySessionsCreateRequest) (session *response.ChatHistorySessionsResponse, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
_i.Log.Info().Interface("data", req).Msg("Saving chat history")
// Set user ID from auth token
req.UserID = userInfo.ID
// Check if session already exists
existingSession, err := _i.Repo.FindSessionBySessionID(req.SessionID)
if err == nil && existingSession != nil {
// Update existing session
updateReq := request.ChatHistorySessionsUpdateRequest{
Title: req.Title,
}
err = _i.UpdateSession(authToken, req.SessionID, updateReq)
if err != nil {
return nil, err
}
// Delete existing messages
err = _i.Repo.DeleteMessagesBySessionID(req.SessionID)
if err != nil {
_i.Log.Error().Err(err).Msg("Failed to delete existing messages")
}
} else {
// Create new session
entity := req.ToEntity()
_, err = _i.Repo.CreateSession(entity)
if err != nil {
return nil, err
}
}
// Save messages if provided
if len(req.Messages) > 0 {
for _, msgReq := range req.Messages {
messageEntity := &entity.ChatMessagesNew{
SessionID: req.SessionID,
MessageType: msgReq.Type,
Content: msgReq.Content,
}
_, err = _i.Repo.CreateMessage(messageEntity)
if err != nil {
_i.Log.Error().Err(err).Msg("Failed to create message")
}
}
// Update message count
err = _i.Repo.UpdateSession(req.SessionID, &entity.ChatSessions{
MessageCount: len(req.Messages),
})
if err != nil {
_i.Log.Error().Err(err).Msg("Failed to update message count")
}
}
// Return the session
result, err := _i.Repo.FindSessionBySessionID(req.SessionID)
if err != nil {
return nil, err
}
return mapper.ChatHistorySessionsResponseMapper(result), nil
}

View File

@ -3,12 +3,14 @@ package router
import (
"narasi-ahli-be/app/module/activity_logs"
"narasi-ahli-be/app/module/advertisement"
"narasi-ahli-be/app/module/ai_chat"
"narasi-ahli-be/app/module/article_approvals"
"narasi-ahli-be/app/module/article_categories"
"narasi-ahli-be/app/module/article_category_details"
"narasi-ahli-be/app/module/article_comments"
"narasi-ahli-be/app/module/article_files"
"narasi-ahli-be/app/module/articles"
"narasi-ahli-be/app/module/chat_history"
"narasi-ahli-be/app/module/cities"
"narasi-ahli-be/app/module/custom_static_pages"
"narasi-ahli-be/app/module/districts"
@ -39,12 +41,14 @@ type Router struct {
ActivityLogsRouter *activity_logs.ActivityLogsRouter
AdvertisementRouter *advertisement.AdvertisementRouter
AIChatRouter *ai_chat.AIChatRouter
ArticleCategoriesRouter *article_categories.ArticleCategoriesRouter
ArticleCategoryDetailsRouter *article_category_details.ArticleCategoryDetailsRouter
ArticleFilesRouter *article_files.ArticleFilesRouter
ArticleCommentsRouter *article_comments.ArticleCommentsRouter
ArticleApprovalsRouter *article_approvals.ArticleApprovalsRouter
ArticlesRouter *articles.ArticlesRouter
ChatHistoryRouter *chat_history.ChatHistoryRouter
CitiesRouter *cities.CitiesRouter
CustomStaticPagesRouter *custom_static_pages.CustomStaticPagesRouter
DistrictsRouter *districts.DistrictsRouter
@ -70,12 +74,14 @@ func NewRouter(
activityLogsRouter *activity_logs.ActivityLogsRouter,
advertisementRouter *advertisement.AdvertisementRouter,
aiChatRouter *ai_chat.AIChatRouter,
articleCategoriesRouter *article_categories.ArticleCategoriesRouter,
articleCategoryDetailsRouter *article_category_details.ArticleCategoryDetailsRouter,
articleFilesRouter *article_files.ArticleFilesRouter,
articleCommentsRouter *article_comments.ArticleCommentsRouter,
articleApprovalsRouter *article_approvals.ArticleApprovalsRouter,
articlesRouter *articles.ArticlesRouter,
chatHistoryRouter *chat_history.ChatHistoryRouter,
citiesRouter *cities.CitiesRouter,
customStaticPagesRouter *custom_static_pages.CustomStaticPagesRouter,
districtsRouter *districts.DistrictsRouter,
@ -99,12 +105,14 @@ func NewRouter(
Cfg: cfg,
ActivityLogsRouter: activityLogsRouter,
AdvertisementRouter: advertisementRouter,
AIChatRouter: aiChatRouter,
ArticleCategoriesRouter: articleCategoriesRouter,
ArticleCategoryDetailsRouter: articleCategoryDetailsRouter,
ArticleFilesRouter: articleFilesRouter,
ArticleCommentsRouter: articleCommentsRouter,
ArticleApprovalsRouter: articleApprovalsRouter,
ArticlesRouter: articlesRouter,
ChatHistoryRouter: chatHistoryRouter,
CitiesRouter: citiesRouter,
CustomStaticPagesRouter: customStaticPagesRouter,
DistrictsRouter: districtsRouter,
@ -138,12 +146,14 @@ func (r *Router) Register() {
// Register routes of modules
r.ActivityLogsRouter.RegisterActivityLogsRoutes()
r.AdvertisementRouter.RegisterAdvertisementRoutes()
r.AIChatRouter.RegisterAIChatRoutes()
r.ArticleCategoriesRouter.RegisterArticleCategoriesRoutes()
r.ArticleCategoryDetailsRouter.RegisterArticleCategoryDetailsRoutes()
r.ArticleFilesRouter.RegisterArticleFilesRoutes()
r.ArticleApprovalsRouter.RegisterArticleApprovalsRoutes()
r.ArticlesRouter.RegisterArticlesRoutes()
r.ArticleCommentsRouter.RegisterArticleCommentsRoutes()
r.ChatHistoryRouter.RegisterChatHistoryRoutes()
r.CitiesRouter.RegisterCitiesRoutes()
r.CustomStaticPagesRouter.RegisterCustomStaticPagesRoutes()
r.DistrictsRouter.RegisterDistrictsRoutes()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -5,12 +5,14 @@ import (
"narasi-ahli-be/app/middleware"
"narasi-ahli-be/app/module/activity_logs"
"narasi-ahli-be/app/module/advertisement"
"narasi-ahli-be/app/module/ai_chat"
"narasi-ahli-be/app/module/article_approvals"
"narasi-ahli-be/app/module/article_categories"
"narasi-ahli-be/app/module/article_category_details"
"narasi-ahli-be/app/module/article_comments"
"narasi-ahli-be/app/module/article_files"
"narasi-ahli-be/app/module/articles"
"narasi-ahli-be/app/module/chat_history"
"narasi-ahli-be/app/module/cities"
"narasi-ahli-be/app/module/custom_static_pages"
"narasi-ahli-be/app/module/districts"
@ -65,12 +67,14 @@ func main() {
// provide modules
activity_logs.NewActivityLogsModule,
advertisement.NewAdvertisementModule,
ai_chat.NewAIChatModule,
article_categories.NewArticleCategoriesModule,
article_category_details.NewArticleCategoryDetailsModule,
article_files.NewArticleFilesModule,
article_approvals.NewArticleApprovalsModule,
articles.NewArticlesModule,
article_comments.NewArticleCommentsModule,
chat_history.NewChatHistoryModule,
cities.NewCitiesModule,
custom_static_pages.NewCustomStaticPagesModule,
districts.NewDistrictsModule,

View File

@ -0,0 +1,28 @@
package migrations
import (
"narasi-ahli-be/app/database"
"narasi-ahli-be/app/database/entity"
)
// CreateChatHistoryTables creates the chat history tables
func CreateChatHistoryTables(db *database.Database) error {
// Auto-migrate the new entities
err := db.DB.AutoMigrate(
&entity.ChatSessions{},
&entity.ChatMessagesNew{},
)
if err != nil {
return err
}
// Create indexes manually if needed
db.DB.Exec("CREATE INDEX IF NOT EXISTS idx_chat_sessions_user_id ON chat_sessions (user_id)")
db.DB.Exec("CREATE INDEX IF NOT EXISTS idx_chat_sessions_agent_id ON chat_sessions (agent_id)")
db.DB.Exec("CREATE INDEX IF NOT EXISTS idx_chat_sessions_session_id ON chat_sessions (session_id)")
db.DB.Exec("CREATE INDEX IF NOT EXISTS idx_chat_sessions_created_at ON chat_sessions (created_at)")
db.DB.Exec("CREATE INDEX IF NOT EXISTS idx_chat_messages_new_session_id ON chat_messages_new (session_id)")
db.DB.Exec("CREATE INDEX IF NOT EXISTS idx_chat_messages_new_created_at ON chat_messages_new (created_at)")
return nil
}

View File

@ -0,0 +1,53 @@
-- Migration: Create Chat History Tables
-- Description: Create tables for chat history functionality based on the old web API structure
-- Date: 2024-01-01
-- Create chat_sessions table
CREATE TABLE IF NOT EXISTS chat_sessions (
id SERIAL PRIMARY KEY,
session_id VARCHAR(255) NOT NULL UNIQUE,
user_id INTEGER NOT NULL,
agent_id VARCHAR(255) NOT NULL,
title VARCHAR(255) NOT NULL DEFAULT 'Chat Session',
message_count INTEGER DEFAULT 0,
status VARCHAR(50) DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create chat_messages_new table (to avoid conflict with existing chat_messages)
CREATE TABLE IF NOT EXISTS chat_messages_new (
id SERIAL PRIMARY KEY,
session_id VARCHAR(255) NOT NULL,
message_type VARCHAR(50) NOT NULL CHECK (message_type IN ('user', 'assistant')),
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_chat_sessions_user_id ON chat_sessions (user_id);
CREATE INDEX IF NOT EXISTS idx_chat_sessions_agent_id ON chat_sessions (agent_id);
CREATE INDEX IF NOT EXISTS idx_chat_sessions_session_id ON chat_sessions (session_id);
CREATE INDEX IF NOT EXISTS idx_chat_sessions_created_at ON chat_sessions (created_at);
CREATE INDEX IF NOT EXISTS idx_chat_messages_new_session_id ON chat_messages_new (session_id);
CREATE INDEX IF NOT EXISTS idx_chat_messages_new_created_at ON chat_messages_new (created_at);
-- Add foreign key constraints
ALTER TABLE chat_sessions
ADD CONSTRAINT fk_chat_sessions_user_id
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE chat_messages_new
ADD CONSTRAINT fk_chat_messages_new_session_id
FOREIGN KEY (session_id) REFERENCES chat_sessions(session_id) ON DELETE CASCADE;
-- Insert sample data (optional)
INSERT INTO chat_sessions (session_id, user_id, agent_id, title, message_count)
VALUES ('sample-session-123', 1, 'd04bbced-ae93-4fb3-a015-9472e4e5e539', 'Sample Chat Session', 2)
ON CONFLICT (session_id) DO NOTHING;
INSERT INTO chat_messages_new (session_id, message_type, content)
VALUES
('sample-session-123', 'user', 'Halo, ada yang bisa saya tanyakan?'),
('sample-session-123', 'assistant', 'Halo! Tentu saja, saya siap membantu Anda dengan pertanyaan apapun.')
ON CONFLICT DO NOTHING;

View File

@ -1,66 +0,0 @@
# Script to remove client_id from all entity files
Write-Host "Starting client_id removal from entity files..." -ForegroundColor Green
# List of entity files that have client_id
$entityFiles = @(
"app/database/entity/users/users.entity.go",
"app/database/entity/article_category_details/article_category_details.entity.go",
"app/database/entity/advertisement.entity.go",
"app/database/entity/activity_logs.entity.go",
"app/database/entity/articles.entity.go",
"app/database/entity/article_approvals.entity.go",
"app/database/entity/article_comments.entity.go",
"app/database/entity/audit_trails.entity.go",
"app/database/entity/article_files.entity.go",
"app/database/entity/article_categories.entity.go",
"app/database/entity/csrf_token_records.entity.go",
"app/database/entity/feedbacks.entity.go",
"app/database/entity/forgot_passwords.entity.go",
"app/database/entity/magazines.entity.go",
"app/database/entity/master_modules.entity.go",
"app/database/entity/one_time_passwords.entity.go",
"app/database/entity/magazine_files.entity.go",
"app/database/entity/master_menus.entity.go",
"app/database/entity/user_roles.entity.go",
"app/database/entity/subscription.entity.go",
"app/database/entity/user_levels.entity.go",
"app/database/entity/user_role_level_details.entity.go",
"app/database/entity/user_role_accesses.entity.go"
)
$processedFiles = 0
$totalFiles = $entityFiles.Count
foreach ($filePath in $entityFiles) {
if (Test-Path $filePath) {
$processedFiles++
Write-Progress -Activity "Processing entity files" -Status "Processing $([System.IO.Path]::GetFileName($filePath))" -PercentComplete (($processedFiles / $totalFiles) * 100)
Write-Host "Processing: $filePath" -ForegroundColor Yellow
$content = Get-Content $filePath -Raw
# Remove ClientId field definitions
$content = $content -replace '\s*ClientId\s+\*uuid\.UUID\s+`json:"client_id"[^`]*`\s*\n', ''
$content = $content -replace '\s*ClientId\s+\*uuid\.UUID\s+`json:"client_id"[^`]*`\s*', ''
# Remove ClientId field without json tag
$content = $content -replace '\s*ClientId\s+\*uuid\.UUID\s*\n', ''
$content = $content -replace '\s*ClientId\s+\*uuid\.UUID\s*', ''
# Remove uuid import if no more uuid usage
if ($content -notmatch 'uuid\.') {
$content = $content -replace '"github\.com/google/uuid"\s*\n', ''
$content = $content -replace 'github\.com/google/uuid\s*\n', ''
}
# Clean up extra empty lines
$content = $content -replace '\n\s*\n\s*\n', "`n`n"
# Write back to file
Set-Content -Path $filePath -Value $content -NoNewline
}
}
Write-Host "Entity client_id removal process completed!" -ForegroundColor Green
Write-Host "Processed $processedFiles entity files" -ForegroundColor Cyan

View File

@ -1,68 +0,0 @@
# Enhanced script to remove client_id from entity files
Write-Host "Starting enhanced client_id removal from entity files..." -ForegroundColor Green
# List of entity files that have client_id
$entityFiles = @(
"app/database/entity/users/users.entity.go",
"app/database/entity/article_category_details/article_category_details.entity.go",
"app/database/entity/advertisement.entity.go",
"app/database/entity/activity_logs.entity.go",
"app/database/entity/articles.entity.go",
"app/database/entity/article_approvals.entity.go",
"app/database/entity/article_comments.entity.go",
"app/database/entity/audit_trails.entity.go",
"app/database/entity/article_files.entity.go",
"app/database/entity/article_categories.entity.go",
"app/database/entity/csrf_token_records.entity.go",
"app/database/entity/feedbacks.entity.go",
"app/database/entity/forgot_passwords.entity.go",
"app/database/entity/magazines.entity.go",
"app/database/entity/master_modules.entity.go",
"app/database/entity/one_time_passwords.entity.go",
"app/database/entity/magazine_files.entity.go",
"app/database/entity/master_menus.entity.go",
"app/database/entity/user_roles.entity.go",
"app/database/entity/subscription.entity.go",
"app/database/entity/user_levels.entity.go",
"app/database/entity/user_role_level_details.entity.go",
"app/database/entity/user_role_accesses.entity.go"
)
$processedFiles = 0
$totalFiles = $entityFiles.Count
foreach ($filePath in $entityFiles) {
if (Test-Path $filePath) {
$processedFiles++
Write-Progress -Activity "Processing entity files" -Status "Processing $([System.IO.Path]::GetFileName($filePath))" -PercentComplete (($processedFiles / $totalFiles) * 100)
Write-Host "Processing: $filePath" -ForegroundColor Yellow
$content = Get-Content $filePath -Raw
# More specific patterns to remove ClientId field
$content = $content -replace '\s*ClientId\s+\*uuid\.UUID\s+`json:"client_id"[^`]*`\s*\n', ''
$content = $content -replace '\s*ClientId\s+\*uuid\.UUID\s+`json:"client_id"[^`]*`\s*', ''
$content = $content -replace '\s*ClientId\s+\*uuid\.UUID\s*\n', ''
$content = $content -replace '\s*ClientId\s+\*uuid\.UUID\s*', ''
# Remove ClientId field with different patterns
$content = $content -replace '\s*ClientId\s+\*uuid\.UUID\s+`json:"client_id"[^`]*`\s*\n', ''
$content = $content -replace '\s*ClientId\s+\*uuid\.UUID\s+`json:"client_id"[^`]*`\s*', ''
# Remove uuid import if no more uuid usage
if ($content -notmatch 'uuid\.') {
$content = $content -replace '"github\.com/google/uuid"\s*\n', ''
$content = $content -replace 'github\.com/google/uuid\s*\n', ''
}
# Clean up extra empty lines
$content = $content -replace '\n\s*\n\s*\n', "`n`n"
# Write back to file
Set-Content -Path $filePath -Value $content -NoNewline
}
}
Write-Host "Enhanced entity client_id removal process completed!" -ForegroundColor Green
Write-Host "Processed $processedFiles entity files" -ForegroundColor Cyan

View File

@ -1,33 +0,0 @@
# Final script to remove ClientId from all entity files
Write-Host "Final removal of ClientId from entity files..." -ForegroundColor Green
# Get all .go files in entity directory
$entityFiles = Get-ChildItem -Path "app/database/entity" -Recurse -Filter "*.go"
foreach ($file in $entityFiles) {
$content = Get-Content $file.FullName -Raw
# Skip if file doesn't contain ClientId
if ($content -notmatch "ClientId") {
continue
}
Write-Host "Processing: $($file.Name)" -ForegroundColor Yellow
# Remove ClientId field with regex
$content = $content -replace '\s*ClientId\s+\*uuid\.UUID\s+`json:"client_id"[^`]*`\s*\n', ''
$content = $content -replace '\s*ClientId\s+\*uuid\.UUID\s+`json:"client_id"[^`]*`\s*', ''
$content = $content -replace '\s*ClientId\s+\*uuid\.UUID\s*\n', ''
$content = $content -replace '\s*ClientId\s+\*uuid\.UUID\s*', ''
# Remove uuid import if no more uuid usage
if ($content -notmatch 'uuid\.') {
$content = $content -replace '"github\.com/google/uuid"\s*\n', ''
$content = $content -replace 'github\.com/google/uuid\s*\n', ''
}
# Write back to file
Set-Content -Path $file.FullName -Value $content -NoNewline
}
Write-Host "Final ClientId removal completed!" -ForegroundColor Green

View File

@ -1,79 +0,0 @@
# Enhanced script to remove client_id usage from remaining files
Write-Host "Starting enhanced client_id removal process..." -ForegroundColor Green
# List of files that still have client_id usage
$problemFiles = @(
"app/module/custom_static_pages/repository/custom_static_pages.repository.go",
"app/module/article_files/repository/article_files.repository.go",
"app/module/article_comments/repository/article_comments.repository.go",
"app/module/article_categories/repository/article_categories.repository.go",
"app/module/articles/repository/articles.repository.go",
"app/module/advertisement/repository/advertisement.repository.go",
"app/module/activity_logs/repository/activity_logs.repository.go",
"app/module/users/service/users.service.go",
"app/module/subscription/repository/subscription.repository.go",
"app/module/subscription/controller/subscription.controller.go",
"app/module/subscription/service/subscription.service.go",
"app/module/magazines/controller/magazines.controller.go",
"app/module/magazines/repository/magazines.repository.go",
"app/module/magazines/service/magazines.service.go",
"app/module/users/controller/users.controller.go",
"app/module/feedbacks/service/feedbacks.service.go",
"app/module/feedbacks/repository/feedbacks.repository.go",
"app/module/feedbacks/controller/feedbacks.controller.go"
)
foreach ($filePath in $problemFiles) {
if (Test-Path $filePath) {
Write-Host "Processing: $filePath" -ForegroundColor Yellow
$content = Get-Content $filePath -Raw
# More aggressive replacements
$content = $content -replace 'clientId \*uuid\.UUID,?\s*', ''
$content = $content -replace 'clientId \*uuid\.UUID\s*,?\s*', ''
$content = $content -replace 'clientId \*uuid\.UUID\s*', ''
# Remove clientId assignments
$content = $content -replace 'clientId := middleware\.GetClientID\(c\)\s*\n\s*', ''
$content = $content -replace 'clientId := middleware\.GetClientID\(c\)', ''
# Remove clientId from function calls - be more specific
$content = $content -replace '\(clientId,', '('
$content = $content -replace ', clientId\)', ')'
$content = $content -replace '\(clientId\)', '()'
# Remove client_id filters
$content = $content -replace 'if clientId != nil \{\s*\n\s*query = query\.Where\("client_id = \?", clientId\)\s*\n\s*\}', ''
$content = $content -replace 'if clientId != nil \{\s*\n\s*query\.Where\("client_id = \?", clientId\)\s*\n\s*\}', ''
# Remove clientId logging
$content = $content -replace '_i\.Log\.Info\(\)\.Interface\("clientId", clientId\)\.Msg\(""\)\s*\n\s*', ''
# Remove clientId comments
$content = $content -replace '// Add ClientId filter\s*\n', ''
$content = $content -replace '// Get ClientId from context\s*\n', ''
# Clean up function signatures
$content = $content -replace ',\s*,', ','
$content = $content -replace '\(\s*,', '('
$content = $content -replace ',\s*\)', ')'
$content = $content -replace '\(\s*\)', '()'
# Remove unused imports
if ($content -notmatch 'uuid\.') {
$content = $content -replace '"github\.com/google/uuid"\s*\n', ''
$content = $content -replace 'github\.com/google/uuid\s*\n', ''
}
if ($content -notmatch 'middleware\.') {
$content = $content -replace '"narasi-ahli-be/app/middleware"\s*\n', ''
$content = $content -replace 'narasi-ahli-be/app/middleware\s*\n', ''
}
# Write back to file
Set-Content -Path $filePath -Value $content -NoNewline
}
}
Write-Host "Enhanced client ID removal process completed!" -ForegroundColor Green

38
scripts/migrate.go Normal file
View File

@ -0,0 +1,38 @@
package main
import (
"narasi-ahli-be/app/database"
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/migrations"
"github.com/rs/zerolog"
"go.uber.org/fx"
)
func main() {
fx.New(
fx.Provide(database.NewDatabase),
fx.Provide(func() zerolog.Logger {
return zerolog.Nop()
}),
fx.Invoke(func(db *database.Database) {
// Run migration
err := migrations.CreateChatHistoryTables(db)
if err != nil {
panic(err)
}
// Also auto-migrate existing entities
err = db.DB.AutoMigrate(
&entity.AIChatSessions{},
&entity.AIChatMessages{},
&entity.AIChatLogs{},
)
if err != nil {
panic(err)
}
println("Migration completed successfully!")
}),
).Run()
}