Refactor AI summarization functionality by removing document summarization worker
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Removed the DocumentSummarizationWorker and its related scheduling logic from the worker bootstrap. - Updated the AI summarizer client initialization comment for clarity. - Added a new error type for cases when tender documents have not been scraped yet, enhancing error handling in the tender service. - Modified API documentation to reflect changes in AI summary retrieval logic, ensuring accurate descriptions of on-demand summarization behavior. This update streamlines the AI summarization process by eliminating the document summarization worker, improving overall system efficiency and clarity in error handling.
This commit is contained in:
+157
-77
@@ -168,44 +168,27 @@ func (s *tenderService) buildDetailResponse(ctx context.Context, tender *Tender,
|
||||
return resp
|
||||
}
|
||||
|
||||
// enrichWithAISummary loads overall_summary from MinIO (PROC_<contractFolderID>/<noticePublicationID>/tender.json)
|
||||
// and attaches it to the response. The primary notice publication id is tried first, then related ids.
|
||||
// A previously stored ai_overall_summary on the tender (already mapped in ToResponse) is kept when MinIO
|
||||
// has no ready summary. On success the tender document is updated so list/search can return it without
|
||||
// another MinIO round-trip.
|
||||
// enrichWithAISummary attaches the overall AI summary to a tender detail response.
|
||||
// It reads from MinIO when available; for scraped tenders without a summary it triggers
|
||||
// on-demand summarization via the AI service (same path as GetAISummary).
|
||||
func (s *tenderService) enrichWithAISummary(ctx context.Context, t *Tender, resp *TenderResponse) {
|
||||
if t == nil || resp == nil {
|
||||
return
|
||||
}
|
||||
if s.aiSummarizerStorage == nil || strings.TrimSpace(t.ContractFolderID) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
enrichCtx, cancel := context.WithTimeout(ctx, aiSummaryEnrichmentTimeout)
|
||||
defer cancel()
|
||||
|
||||
summary, usedNotice, err := s.lookupSummaryForTender(enrichCtx, t)
|
||||
result, err := s.resolveAISummary(ctx, t)
|
||||
if err != nil {
|
||||
s.logger.Debug("AI summary not available for tender", map[string]interface{}{
|
||||
"tender_id": resp.ID,
|
||||
"contract_folder_id": t.ContractFolderID,
|
||||
"notice_id": t.NoticePublicationID,
|
||||
"error": err.Error(),
|
||||
s.logger.Debug("AI summary resolution failed for tender detail", map[string]interface{}{
|
||||
"tender_id": resp.ID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
resp.OverallSummary = summary
|
||||
if usedNotice != t.NoticePublicationID {
|
||||
s.logger.Debug("AI summary resolved from related notice", map[string]interface{}{
|
||||
"tender_id": resp.ID,
|
||||
"contract_folder_id": t.ContractFolderID,
|
||||
"primary_notice_id": t.NoticePublicationID,
|
||||
"used_notice_id": usedNotice,
|
||||
})
|
||||
if result == nil || strings.TrimSpace(result.OverallSummary) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
s.persistAIOverallSummary(enrichCtx, t, summary)
|
||||
resp.OverallSummary = result.OverallSummary
|
||||
}
|
||||
|
||||
// enrichWithTranslation overlays title/description from MinIO when the pipeline has finished
|
||||
@@ -1131,8 +1114,8 @@ func (s *tenderService) CleanupOldData(ctx context.Context, olderThan time.Durat
|
||||
}
|
||||
|
||||
// GetAISummary retrieves the AI-generated summary for a tender.
|
||||
// It loads from MinIO storage (async pipeline result). When no summary exists yet,
|
||||
// the pipeline is still running, or storage is unavailable, it returns 200-compatible
|
||||
// It reads from MinIO when available; for scraped tenders without a summary it triggers
|
||||
// on-demand summarization. When no summary can be produced it returns 200-compatible
|
||||
// data with an empty overall_summary and a Source hint instead of an error.
|
||||
func (s *tenderService) GetAISummary(ctx context.Context, id string) (*AISummaryResponse, error) {
|
||||
if id == "" {
|
||||
@@ -1151,6 +1134,12 @@ func (s *tenderService) GetAISummary(ctx context.Context, id string) (*AISummary
|
||||
return nil, fmt.Errorf("tender not found")
|
||||
}
|
||||
|
||||
return s.resolveAISummary(ctx, t)
|
||||
}
|
||||
|
||||
// resolveAISummary returns the overall AI summary for a tender, triggering on-demand
|
||||
// summarization when documents are scraped but no summary exists in storage yet.
|
||||
func (s *tenderService) resolveAISummary(ctx context.Context, t *Tender) (*AISummaryResponse, error) {
|
||||
noticeID := strings.TrimSpace(t.NoticePublicationID)
|
||||
emptyResp := func(source string) *AISummaryResponse {
|
||||
return &AISummaryResponse{
|
||||
@@ -1164,45 +1153,146 @@ func (s *tenderService) GetAISummary(ctx context.Context, id string) (*AISummary
|
||||
return emptyResp("unavailable"), nil
|
||||
}
|
||||
|
||||
if s.aiSummarizerStorage == nil {
|
||||
s.logger.Debug("AI summarizer storage not configured; returning empty AI summary", map[string]interface{}{
|
||||
"tender_id": id,
|
||||
if s.aiSummarizerStorage == nil && s.aiSummarizerClient == nil {
|
||||
s.logger.Debug("AI summarizer not configured; returning empty AI summary", map[string]interface{}{
|
||||
"tender_id": t.ID.Hex(),
|
||||
})
|
||||
return emptyResp("unavailable"), nil
|
||||
}
|
||||
|
||||
summary, usedNotice, err := s.lookupSummaryForTender(ctx, t)
|
||||
if err == nil {
|
||||
return &AISummaryResponse{
|
||||
NoticeID: usedNotice,
|
||||
OverallSummary: summary,
|
||||
Source: "storage",
|
||||
}, nil
|
||||
lookupCtx := ctx
|
||||
if deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) > aiSummaryEnrichmentTimeout {
|
||||
var cancel context.CancelFunc
|
||||
lookupCtx, cancel = context.WithTimeout(ctx, aiSummaryEnrichmentTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
s.logger.Info("AI summary not available from storage", map[string]interface{}{
|
||||
"tender_id": id,
|
||||
"contract_folder_id": t.ContractFolderID,
|
||||
"notice_id": noticeID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
if s.aiSummarizerStorage != nil {
|
||||
summary, usedNotice, err := s.lookupSummaryForTender(lookupCtx, t)
|
||||
if err == nil {
|
||||
s.persistAIOverallSummary(ctx, t, summary)
|
||||
return &AISummaryResponse{
|
||||
NoticeID: usedNotice,
|
||||
OverallSummary: summary,
|
||||
Source: "storage",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if errors.Is(err, ai_summarizer.ErrSummaryNotReady) {
|
||||
return emptyResp("pending"), nil
|
||||
if !errors.Is(err, ai_summarizer.ErrSummaryNotReady) &&
|
||||
!errors.Is(err, ai_summarizer.ErrObjectNotFound) &&
|
||||
!errors.Is(err, ai_summarizer.ErrNoOverallSummary) {
|
||||
if errors.Is(err, ai_summarizer.ErrMinIOUnavailable) {
|
||||
s.logger.Error("Cannot retrieve AI summary: MinIO connection unavailable", map[string]interface{}{
|
||||
"tender_id": t.ID.Hex(),
|
||||
"contract_folder_id": t.ContractFolderID,
|
||||
"notice_id": noticeID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to get AI summary: MinIO connection unavailable: %w", err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get AI summary: %w", err)
|
||||
}
|
||||
}
|
||||
if errors.Is(err, ai_summarizer.ErrObjectNotFound) || errors.Is(err, ai_summarizer.ErrNoOverallSummary) {
|
||||
|
||||
if !s.tenderHasScrapedDocuments(t) {
|
||||
return emptyResp("unavailable"), nil
|
||||
}
|
||||
if errors.Is(err, ai_summarizer.ErrMinIOUnavailable) {
|
||||
s.logger.Error("Cannot retrieve AI summary: MinIO connection unavailable", map[string]interface{}{
|
||||
"tender_id": id,
|
||||
|
||||
if s.aiSummarizerClient == nil {
|
||||
return emptyResp("unavailable"), nil
|
||||
}
|
||||
|
||||
resp, err := s.fetchAISummaryOnDemand(ctx, t)
|
||||
if err != nil {
|
||||
s.logger.Error("On-demand AI summarization on tender read failed", map[string]interface{}{
|
||||
"tender_id": t.ID.Hex(),
|
||||
"contract_folder_id": t.ContractFolderID,
|
||||
"notice_id": noticeID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to get AI summary: MinIO connection unavailable: %w", err)
|
||||
return emptyResp("pending"), nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get AI summary: %w", err)
|
||||
|
||||
overall := derefString(resp.OverallSummary)
|
||||
usedNotice := resp.ResolvedNoticePublicationID()
|
||||
if usedNotice == "" {
|
||||
usedNotice = noticeID
|
||||
}
|
||||
|
||||
if overall == "" && s.aiSummarizerStorage != nil {
|
||||
if summary, notice, lookupErr := s.lookupSummaryForTender(ctx, t); lookupErr == nil {
|
||||
overall = summary
|
||||
usedNotice = notice
|
||||
}
|
||||
}
|
||||
|
||||
if overall != "" {
|
||||
s.persistAIOverallSummary(ctx, t, overall)
|
||||
return &AISummaryResponse{
|
||||
NoticeID: usedNotice,
|
||||
OverallSummary: overall,
|
||||
Source: "generated",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return emptyResp("pending"), nil
|
||||
}
|
||||
|
||||
func (s *tenderService) tenderHasScrapedDocuments(t *Tender) bool {
|
||||
if t == nil {
|
||||
return false
|
||||
}
|
||||
if t.ProcessingMetadata.DocumentsScraped {
|
||||
return true
|
||||
}
|
||||
return len(t.ScrapedDocuments) > 0
|
||||
}
|
||||
|
||||
func (s *tenderService) fetchAISummaryOnDemand(ctx context.Context, t *Tender) (*ai_summarizer.SummarizeResponse, error) {
|
||||
if s.aiSummarizerClient == nil {
|
||||
return nil, ErrAINotConfigured
|
||||
}
|
||||
if strings.TrimSpace(t.NoticePublicationID) == "" {
|
||||
return nil, ErrTenderMissingNoticeID
|
||||
}
|
||||
if strings.TrimSpace(t.ContractFolderID) == "" {
|
||||
return nil, ErrTenderMissingContractFolderID
|
||||
}
|
||||
|
||||
reqBody := ai_summarizer.NewSummarizeRequest(t.ContractFolderID, t.NoticePublicationID)
|
||||
s.logger.Info("Triggering on-demand AI summarization", map[string]interface{}{
|
||||
"tender_id": t.ID.Hex(),
|
||||
"contract_folder_id": t.ContractFolderID,
|
||||
"notice_publication_id": t.NoticePublicationID,
|
||||
})
|
||||
|
||||
resp, err := s.aiSummarizerClient.FetchSummaryOnDemand(ctx, reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AI summarization request failed: %w", err)
|
||||
}
|
||||
|
||||
s.markTenderSummarized(ctx, t)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *tenderService) markTenderSummarized(ctx context.Context, t *Tender) {
|
||||
now := time.Now().Unix()
|
||||
t.ProcessingMetadata.DocumentsSummarized = true
|
||||
t.ProcessingMetadata.DocumentsSummarizedAt = now
|
||||
t.UpdatedAt = now
|
||||
if err := s.repository.Update(ctx, t); err != nil {
|
||||
s.logger.Warn("Failed to update tender summarization status", map[string]interface{}{
|
||||
"tender_id": t.ID.Hex(),
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func derefString(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(*value)
|
||||
}
|
||||
|
||||
// TriggerAISummarize triggers on-demand AI summarization for a tender.
|
||||
@@ -1216,7 +1306,6 @@ func (s *tenderService) TriggerAISummarize(ctx context.Context, id string) (*AIS
|
||||
return nil, ErrAINotConfigured
|
||||
}
|
||||
|
||||
// Get tender to build the request
|
||||
t, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get tender for on-demand summarization", map[string]interface{}{
|
||||
@@ -1236,23 +1325,11 @@ func (s *tenderService) TriggerAISummarize(ctx context.Context, id string) (*AIS
|
||||
return nil, ErrTenderNotFound
|
||||
}
|
||||
|
||||
if t.NoticePublicationID == "" {
|
||||
return nil, ErrTenderMissingNoticeID
|
||||
}
|
||||
if strings.TrimSpace(t.ContractFolderID) == "" {
|
||||
return nil, ErrTenderMissingContractFolderID
|
||||
if !s.tenderHasScrapedDocuments(t) {
|
||||
return nil, ErrTenderDocumentsNotScraped
|
||||
}
|
||||
|
||||
reqBody := ai_summarizer.NewSummarizeRequest(t.ContractFolderID, t.NoticePublicationID)
|
||||
|
||||
s.logger.Info("Triggering on-demand AI summarization", map[string]interface{}{
|
||||
"tender_id": id,
|
||||
"contract_folder_id": t.ContractFolderID,
|
||||
"notice_publication_id": t.NoticePublicationID,
|
||||
})
|
||||
|
||||
// Call the AI service
|
||||
resp, err := s.aiSummarizerClient.FetchSummaryOnDemand(ctx, reqBody)
|
||||
resp, err := s.fetchAISummaryOnDemand(ctx, t)
|
||||
if err != nil {
|
||||
s.logger.Error("On-demand AI summarization failed", map[string]interface{}{
|
||||
"tender_id": id,
|
||||
@@ -1260,18 +1337,17 @@ func (s *tenderService) TriggerAISummarize(ctx context.Context, id string) (*AIS
|
||||
"notice_publication_id": t.NoticePublicationID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("AI summarization request failed: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Map the response to our response type
|
||||
summaries := make([]AIDocumentSummary, 0, len(resp.Summaries))
|
||||
for _, s := range resp.Summaries {
|
||||
for _, item := range resp.Summaries {
|
||||
summaries = append(summaries, AIDocumentSummary{
|
||||
DocumentName: s.DocumentName,
|
||||
DocumentType: s.DocumentType,
|
||||
Summary: s.Summary,
|
||||
SummaryModel: s.SummaryModel,
|
||||
Error: s.Error,
|
||||
DocumentName: item.DocumentName,
|
||||
DocumentType: item.DocumentType,
|
||||
Summary: item.Summary,
|
||||
SummaryModel: item.SummaryModel,
|
||||
Error: item.Error,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1291,6 +1367,10 @@ func (s *tenderService) TriggerAISummarize(ctx context.Context, id string) (*AIS
|
||||
contractFolderID = t.ContractFolderID
|
||||
}
|
||||
|
||||
if overall := derefString(resp.OverallSummary); overall != "" {
|
||||
s.persistAIOverallSummary(ctx, t, overall)
|
||||
}
|
||||
|
||||
return &AISummarizeResponse{
|
||||
ContractFolderID: contractFolderID,
|
||||
NoticePublicationID: noticePublicationID,
|
||||
|
||||
Reference in New Issue
Block a user