feat: implement RAG-based chatbot service with daily usage rate limiting and background index worker
All checks were successful
Build and Release / release (push) Successful in 1m27s
All checks were successful
Build and Release / release (push) Successful in 1m27s
This commit is contained in:
@@ -45,8 +45,18 @@ func (cx *ChatbotController) Chat(c fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
answer, err := cx.chatbotService.Chat(ctx, dto.ProjectID, dto.Question)
|
||||
claims := c.Locals("user").(*response.JWTClaims)
|
||||
|
||||
answer, err := cx.chatbotService.Chat(ctx, claims.UId, dto.ProjectID, dto.Question)
|
||||
if err != nil {
|
||||
// Trả về lỗi 429 (Too Many Requests) nếu hết lượt dùng
|
||||
if err.Error() == "you have reached your daily limit of 10 questions. Please come back tomorrow" {
|
||||
return c.Status(fiber.StatusTooManyRequests).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
|
||||
@@ -78,7 +78,7 @@ func (s *submissionController) CreateSubmission(c fiber.Ctx) error {
|
||||
// @Security BearerAuth
|
||||
// @Router /submissions/{id}/status [patch]
|
||||
func (s *submissionController) UpdateSubmissionStatus(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Second)
|
||||
defer cancel()
|
||||
id := c.Params("id")
|
||||
uid := c.Locals("uid").(string)
|
||||
|
||||
@@ -15,3 +15,25 @@ type RagChunk struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type RagIndexTask struct {
|
||||
ProjectID string `json:"project_id"`
|
||||
DeleteWikiIDs []string `json:"delete_wiki_ids"`
|
||||
DeleteEntityIDs []string `json:"delete_entity_ids"`
|
||||
Wikis []*RagWikiItem `json:"wikis"`
|
||||
Entities []*RagEntityItem `json:"entities"`
|
||||
}
|
||||
|
||||
type RagWikiItem struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Doc string `json:"doc"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type RagEntityItem struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
55
internal/repositories/usageRepository.go
Normal file
55
internal/repositories/usageRepository.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/constants"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UsageRepository interface {
|
||||
GetAIUsage(ctx context.Context, userID string) (int, error)
|
||||
IncrementAIUsage(ctx context.Context, userID string) (int, error)
|
||||
}
|
||||
|
||||
type usageRepository struct {
|
||||
c cache.Cache
|
||||
}
|
||||
|
||||
func NewUsageRepository(c cache.Cache) UsageRepository {
|
||||
return &usageRepository{
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *usageRepository) getUsageKey(userID string) string {
|
||||
dateStr := time.Now().Format("20060102")
|
||||
return fmt.Sprintf("usage:ai:%s:%s", userID, dateStr)
|
||||
}
|
||||
|
||||
func (r *usageRepository) GetAIUsage(ctx context.Context, userID string) (int, error) {
|
||||
key := r.getUsageKey(userID)
|
||||
var count int
|
||||
err := r.c.Get(ctx, key, &count)
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *usageRepository) IncrementAIUsage(ctx context.Context, userID string) (int, error) {
|
||||
key := r.getUsageKey(userID)
|
||||
rdb := r.c.GetRawClient()
|
||||
|
||||
count, err := rdb.Incr(ctx, key).Result()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if count == 1 {
|
||||
rdb.Expire(ctx, key, constants.UsageExpiration)
|
||||
}
|
||||
|
||||
return int(count), nil
|
||||
}
|
||||
@@ -2,28 +2,41 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"history-api/internal/repositories"
|
||||
"history-api/pkg/ai"
|
||||
"history-api/pkg/constants"
|
||||
)
|
||||
|
||||
type ChatbotService interface {
|
||||
Chat(ctx context.Context, projectID *string, question string) (string, error)
|
||||
Chat(ctx context.Context, userID string, projectID *string, question string) (string, error)
|
||||
}
|
||||
|
||||
type chatbotService struct {
|
||||
repo repositories.RagRepository
|
||||
ragUtils *ai.RagUtils
|
||||
repo repositories.RagRepository
|
||||
usageRepo repositories.UsageRepository
|
||||
ragUtils *ai.RagUtils
|
||||
}
|
||||
|
||||
func NewChatbotService(repo repositories.RagRepository, ragUtils *ai.RagUtils) ChatbotService {
|
||||
func NewChatbotService(repo repositories.RagRepository, usageRepo repositories.UsageRepository, ragUtils *ai.RagUtils) ChatbotService {
|
||||
return &chatbotService{
|
||||
repo: repo,
|
||||
ragUtils: ragUtils,
|
||||
repo: repo,
|
||||
usageRepo: usageRepo,
|
||||
ragUtils: ragUtils,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *chatbotService) Chat(ctx context.Context, projectID *string, question string) (string, error) {
|
||||
func (s *chatbotService) Chat(ctx context.Context, userID string, projectID *string, question string) (string, error) {
|
||||
usage, err := s.usageRepo.GetAIUsage(ctx, userID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to check usage: %w", err)
|
||||
}
|
||||
|
||||
if usage >= constants.MaxDailyAIUsage {
|
||||
return "", errors.New("you have reached your daily limit of 10 questions. Please come back tomorrow")
|
||||
}
|
||||
|
||||
qVector, err := s.ragUtils.EmbedQuery(ctx, question)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to embed question: %w", err)
|
||||
@@ -61,5 +74,13 @@ Context:
|
||||
Question: %s`, contextStr, question)
|
||||
}
|
||||
|
||||
return s.ragUtils.GenerateResponse(ctx, prompt)
|
||||
response, err := s.ragUtils.GenerateResponse(ctx, prompt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 3. Tăng số lần sử dụng sau khi gọi AI thành công
|
||||
_, _ = s.usageRepo.IncrementAIUsage(ctx, userID)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
@@ -15,12 +15,13 @@ import (
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/constants"
|
||||
"history-api/pkg/convert"
|
||||
"strconv"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
@@ -166,7 +167,6 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
entityRepo := s.entityRepo.WithTx(tx)
|
||||
geometryRepo := s.geometryRepo.WithTx(tx)
|
||||
wikiRepo := s.wikiRepo.WithTx(tx)
|
||||
ragRepo := s.ragRepo.WithTx(tx)
|
||||
|
||||
submissionUUID, err := convert.StringToUUID(submissionID)
|
||||
if err != nil {
|
||||
@@ -625,33 +625,31 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
}
|
||||
}
|
||||
|
||||
_ = ragRepo.DeleteBySourceIDs(ctx, "wiki", wikiDeleteIDs)
|
||||
_ = ragRepo.DeleteBySourceIDs(ctx, "entity", entityDeleteIDs)
|
||||
|
||||
for _, wiki := range snapshotData.Wikis {
|
||||
if wiki.Source == "inline" {
|
||||
cleanText := s.ragUtils.StripHTML(wiki.Title + "\n" + wiki.Doc)
|
||||
chunks, vectors, err := s.ragUtils.PrepareChunks(ctx, cleanText)
|
||||
if err == nil {
|
||||
_ = ragRepo.DeleteBySourceIDs(ctx, "wiki", []string{wiki.ID})
|
||||
for i, chunk := range chunks {
|
||||
_ = ragRepo.SaveChunk(ctx, "wiki", wiki.ID, commit.ProjectID, i, chunk, vectors[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
ragTask := models.RagIndexTask{
|
||||
ProjectID: commit.ProjectID,
|
||||
DeleteWikiIDs: wikiDeleteIDs,
|
||||
DeleteEntityIDs: entityDeleteIDs,
|
||||
}
|
||||
|
||||
for _, wiki := range snapshotData.Wikis {
|
||||
ragTask.Wikis = append(ragTask.Wikis, &models.RagWikiItem{
|
||||
ID: wiki.ID,
|
||||
Title: wiki.Title,
|
||||
Doc: wiki.Doc,
|
||||
Source: wiki.Source,
|
||||
})
|
||||
}
|
||||
for _, entity := range snapshotData.Entities {
|
||||
if entity.Source == "inline" {
|
||||
cleanText := s.ragUtils.StripHTML(entity.Name + "\n" + entity.Description)
|
||||
chunks, vectors, err := s.ragUtils.PrepareChunks(ctx, cleanText)
|
||||
if err == nil {
|
||||
_ = ragRepo.DeleteBySourceIDs(ctx, "entity", []string{entity.ID})
|
||||
for i, chunk := range chunks {
|
||||
_ = ragRepo.SaveChunk(ctx, "entity", entity.ID, commit.ProjectID, i, chunk, vectors[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
ragTask.Entities = append(ragTask.Entities, &models.RagEntityItem{
|
||||
ID: entity.ID,
|
||||
Name: entity.Name,
|
||||
Description: entity.Description,
|
||||
Source: entity.Source,
|
||||
})
|
||||
}
|
||||
|
||||
if err := s.c.PublishTask(ctx, constants.StreamRagName, constants.TaskTypeRagIndexSubmission, ragTask); err != nil {
|
||||
log.Error().Err(err).Str("project_id", commit.ProjectID).Msg("Failed to publish RAG index task")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -677,6 +675,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
bgCtx := context.Background()
|
||||
_ = s.c.DelByPattern(bgCtx, "entity:search*")
|
||||
_ = s.c.DelByPattern(bgCtx, "geometry:search*")
|
||||
_ = s.c.DelByPattern(bgCtx, "geometry:search:entity*")
|
||||
_ = s.c.DelByPattern(bgCtx, "wiki:search*")
|
||||
}()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user