Fix summarizer worker infinite loop and map AI trigger errors to safe HTTP responses.
Paginate un-summarized tenders and mark rows missing notice/folder IDs as handled so the worker cannot spin forever. Return 400/404 for validation and not-found cases on AI summarize/analyze triggers instead of leaking internal errors as 500.
This commit is contained in:
@@ -54,8 +54,9 @@ func (w *DocumentSummarizationWorker) Run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
limit := 5
|
limit := 5
|
||||||
|
skip := 0
|
||||||
for {
|
for {
|
||||||
tenders, totalCount, err := w.TenderRepo.GetUnSummarizedTenders(context.Background(), limit, 0)
|
tenders, totalCount, err := w.TenderRepo.GetUnSummarizedTenders(context.Background(), limit, skip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Logger.Error("Failed to get un-summarized tenders", map[string]interface{}{
|
w.Logger.Error("Failed to get un-summarized tenders", map[string]interface{}{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
@@ -66,6 +67,7 @@ func (w *DocumentSummarizationWorker) Run() {
|
|||||||
w.Logger.Info("Found tenders for document summarization", map[string]interface{}{
|
w.Logger.Info("Found tenders for document summarization", map[string]interface{}{
|
||||||
"count": len(tenders),
|
"count": len(tenders),
|
||||||
"total_count": totalCount,
|
"total_count": totalCount,
|
||||||
|
"skip": skip,
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(tenders) == 0 {
|
if len(tenders) == 0 {
|
||||||
@@ -80,6 +82,11 @@ func (w *DocumentSummarizationWorker) Run() {
|
|||||||
})
|
})
|
||||||
w.summarizeTender(t)
|
w.summarizeTender(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
skip += len(tenders)
|
||||||
|
if int64(skip) >= totalCount {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Logger.Info("Document summarization worker completed", map[string]interface{}{})
|
w.Logger.Info("Document summarization worker completed", map[string]interface{}{})
|
||||||
@@ -90,12 +97,14 @@ func (w *DocumentSummarizationWorker) summarizeTender(t *tender.Tender) {
|
|||||||
w.Logger.Warn("Skipping tender summarization: missing notice_publication_id", map[string]interface{}{
|
w.Logger.Warn("Skipping tender summarization: missing notice_publication_id", map[string]interface{}{
|
||||||
"tender_id": t.ID.Hex(),
|
"tender_id": t.ID.Hex(),
|
||||||
})
|
})
|
||||||
|
w.markTenderSummarized(t)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(t.ContractFolderID) == "" {
|
if strings.TrimSpace(t.ContractFolderID) == "" {
|
||||||
w.Logger.Warn("Skipping tender summarization: missing contract_folder_id", map[string]interface{}{
|
w.Logger.Warn("Skipping tender summarization: missing contract_folder_id", map[string]interface{}{
|
||||||
"tender_id": t.ID.Hex(),
|
"tender_id": t.ID.Hex(),
|
||||||
})
|
})
|
||||||
|
w.markTenderSummarized(t)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package tender
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// Sentinel errors for AI trigger operations and HTTP mapping.
|
||||||
|
var (
|
||||||
|
ErrEmptyTenderID = errors.New("tender: id is required")
|
||||||
|
ErrInvalidTenderID = errors.New("tender: invalid id")
|
||||||
|
ErrTenderNotFound = errors.New("tender: not found")
|
||||||
|
ErrTenderMissingNoticeID = errors.New("tender: missing notice publication id")
|
||||||
|
ErrTenderMissingContractFolderID = errors.New("tender: missing contract folder id")
|
||||||
|
ErrAINotConfigured = errors.New("tender: ai service not configured")
|
||||||
|
)
|
||||||
@@ -589,7 +589,7 @@ func (h *TenderHandler) TriggerAISummarize(c echo.Context) error {
|
|||||||
"tender_id": id,
|
"tender_id": id,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return response.InternalServerError(c, err.Error())
|
return mapAITriggerHTTPError(c, err, "AI summarization")
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Success(c, result, "AI summarization completed successfully")
|
return response.Success(c, result, "AI summarization completed successfully")
|
||||||
@@ -619,12 +619,28 @@ func (h *TenderHandler) TriggerAIAnalyze(c echo.Context) error {
|
|||||||
"tender_id": id,
|
"tender_id": id,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return response.InternalServerError(c, err.Error())
|
return mapAITriggerHTTPError(c, err, "AI analysis")
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Success(c, result, "AI analysis completed successfully")
|
return response.Success(c, result, "AI analysis completed successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mapAITriggerHTTPError maps AI trigger service errors to safe HTTP responses.
|
||||||
|
func mapAITriggerHTTPError(c echo.Context, err error, action string) error {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, ErrEmptyTenderID), errors.Is(err, ErrInvalidTenderID):
|
||||||
|
return response.BadRequest(c, "Invalid request", "Invalid tender ID")
|
||||||
|
case errors.Is(err, ErrTenderNotFound), errors.Is(err, orm.ErrDocumentNotFound):
|
||||||
|
return response.NotFound(c, "Tender not found")
|
||||||
|
case errors.Is(err, ErrTenderMissingNoticeID), errors.Is(err, ErrTenderMissingContractFolderID):
|
||||||
|
return response.BadRequest(c, "Tender not eligible", "Tender is missing required identifiers for AI processing")
|
||||||
|
case errors.Is(err, ErrAINotConfigured):
|
||||||
|
return response.InternalServerError(c, "AI service is not available")
|
||||||
|
default:
|
||||||
|
return response.InternalServerError(c, action+" failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TriggerAITranslate triggers on-demand AI translation for a tender
|
// TriggerAITranslate triggers on-demand AI translation for a tender
|
||||||
// @Summary Trigger AI translation
|
// @Summary Trigger AI translation
|
||||||
// @Description Trigger on-demand AI translation for tender title and description (cached in MinIO by the AI service)
|
// @Description Trigger on-demand AI translation for tender title and description (cached in MinIO by the AI service)
|
||||||
|
|||||||
+23
-10
@@ -15,6 +15,7 @@ import (
|
|||||||
"tm/internal/customer"
|
"tm/internal/customer"
|
||||||
"tm/pkg/ai_summarizer"
|
"tm/pkg/ai_summarizer"
|
||||||
"tm/pkg/logger"
|
"tm/pkg/logger"
|
||||||
|
orm "tm/pkg/mongo"
|
||||||
"tm/pkg/response"
|
"tm/pkg/response"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1187,11 +1188,11 @@ func (s *tenderService) GetAISummary(ctx context.Context, id string) (*AISummary
|
|||||||
// It calls the external AI service HTTP API to generate summaries.
|
// It calls the external AI service HTTP API to generate summaries.
|
||||||
func (s *tenderService) TriggerAISummarize(ctx context.Context, id string) (*AISummarizeResponse, error) {
|
func (s *tenderService) TriggerAISummarize(ctx context.Context, id string) (*AISummarizeResponse, error) {
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return nil, fmt.Errorf("tender ID cannot be empty")
|
return nil, ErrEmptyTenderID
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.aiSummarizerClient == nil {
|
if s.aiSummarizerClient == nil {
|
||||||
return nil, fmt.Errorf("AI summarizer service is not configured")
|
return nil, ErrAINotConfigured
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get tender to build the request
|
// Get tender to build the request
|
||||||
@@ -1201,18 +1202,24 @@ func (s *tenderService) TriggerAISummarize(ctx context.Context, id string) (*AIS
|
|||||||
"tender_id": id,
|
"tender_id": id,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
if errors.Is(err, orm.ErrDocumentNotFound) {
|
||||||
|
return nil, ErrTenderNotFound
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "invalid id format") {
|
||||||
|
return nil, ErrInvalidTenderID
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("failed to get tender: %w", err)
|
return nil, fmt.Errorf("failed to get tender: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return nil, fmt.Errorf("tender not found")
|
return nil, ErrTenderNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.NoticePublicationID == "" {
|
if t.NoticePublicationID == "" {
|
||||||
return nil, fmt.Errorf("tender has no notice publication ID")
|
return nil, ErrTenderMissingNoticeID
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(t.ContractFolderID) == "" {
|
if strings.TrimSpace(t.ContractFolderID) == "" {
|
||||||
return nil, fmt.Errorf("tender has no contract folder ID")
|
return nil, ErrTenderMissingContractFolderID
|
||||||
}
|
}
|
||||||
|
|
||||||
reqBody := ai_summarizer.NewSummarizeRequest(t.ContractFolderID, t.NoticePublicationID)
|
reqBody := ai_summarizer.NewSummarizeRequest(t.ContractFolderID, t.NoticePublicationID)
|
||||||
@@ -1274,10 +1281,10 @@ func (s *tenderService) TriggerAISummarize(ctx context.Context, id string) (*AIS
|
|||||||
// TriggerAIAnalyze triggers on-demand agentic analysis for a tender.
|
// TriggerAIAnalyze triggers on-demand agentic analysis for a tender.
|
||||||
func (s *tenderService) TriggerAIAnalyze(ctx context.Context, id string) (*AIAnalyzeResponse, error) {
|
func (s *tenderService) TriggerAIAnalyze(ctx context.Context, id string) (*AIAnalyzeResponse, error) {
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return nil, fmt.Errorf("tender ID cannot be empty")
|
return nil, ErrEmptyTenderID
|
||||||
}
|
}
|
||||||
if s.aiSummarizerClient == nil {
|
if s.aiSummarizerClient == nil {
|
||||||
return nil, fmt.Errorf("AI summarizer service is not configured")
|
return nil, ErrAINotConfigured
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := s.repository.GetByID(ctx, id)
|
t, err := s.repository.GetByID(ctx, id)
|
||||||
@@ -1286,16 +1293,22 @@ func (s *tenderService) TriggerAIAnalyze(ctx context.Context, id string) (*AIAna
|
|||||||
"tender_id": id,
|
"tender_id": id,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
|
if errors.Is(err, orm.ErrDocumentNotFound) {
|
||||||
|
return nil, ErrTenderNotFound
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "invalid id format") {
|
||||||
|
return nil, ErrInvalidTenderID
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("failed to get tender: %w", err)
|
return nil, fmt.Errorf("failed to get tender: %w", err)
|
||||||
}
|
}
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return nil, fmt.Errorf("tender not found")
|
return nil, ErrTenderNotFound
|
||||||
}
|
}
|
||||||
if t.NoticePublicationID == "" {
|
if t.NoticePublicationID == "" {
|
||||||
return nil, fmt.Errorf("tender has no notice publication ID")
|
return nil, ErrTenderMissingNoticeID
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(t.ContractFolderID) == "" {
|
if strings.TrimSpace(t.ContractFolderID) == "" {
|
||||||
return nil, fmt.Errorf("tender has no contract folder ID")
|
return nil, ErrTenderMissingContractFolderID
|
||||||
}
|
}
|
||||||
|
|
||||||
reqBody := ai_summarizer.NewAnalyzeRequest(t.ContractFolderID, t.NoticePublicationID)
|
reqBody := ai_summarizer.NewAnalyzeRequest(t.ContractFolderID, t.NoticePublicationID)
|
||||||
|
|||||||
Reference in New Issue
Block a user