overalSummary nil response

This commit is contained in:
Mazyar
2026-05-12 12:25:09 +03:30
parent 7c42b343ce
commit 0a7fdd1328
3 changed files with 77 additions and 64 deletions
+50 -48
View File
@@ -66,30 +66,31 @@ type SearchResponse struct {
// TenderResponse represents a tender in API responses // TenderResponse represents a tender in API responses
type TenderResponse struct { type TenderResponse struct {
ID string `json:"id"` ID string `json:"id"`
NoticePublicationID string `json:"notice_publication_id"` NoticePublicationID string `json:"notice_publication_id"`
NoticeTypeCode string `json:"notice_type_code"` RelatedNoticePublicationIDs []string `json:"related_notice_publication_ids,omitempty"`
Title string `json:"title"` NoticeTypeCode string `json:"notice_type_code"`
Description string `json:"description"` Title string `json:"title"`
ProcurementTypeCode string `json:"procurement_type_code"` Description string `json:"description"`
ProcedureCode string `json:"procedure_code"` ProcurementTypeCode string `json:"procurement_type_code"`
MainClassification string `json:"main_classification"` ProcedureCode string `json:"procedure_code"`
EstimatedValue float64 `json:"estimated_value"` MainClassification string `json:"main_classification"`
Currency string `json:"currency"` EstimatedValue float64 `json:"estimated_value"`
Duration string `json:"duration"` Currency string `json:"currency"`
DurationUnit string `json:"duration_unit"` Duration string `json:"duration"`
PublicationDate int64 `json:"publication_date"` DurationUnit string `json:"duration_unit"`
TenderDeadline int64 `json:"tender_deadline"` PublicationDate int64 `json:"publication_date"`
SubmissionDeadline int64 `json:"submission_deadline"` TenderDeadline int64 `json:"tender_deadline"`
ApplicationDeadline int64 `json:"application_deadline"` SubmissionDeadline int64 `json:"submission_deadline"`
SubmissionURL string `json:"submission_url"` ApplicationDeadline int64 `json:"application_deadline"`
CountryCode string `json:"country_code"` SubmissionURL string `json:"submission_url"`
BuyerOrganization *OrganizationResponse `json:"buyer_organization"` CountryCode string `json:"country_code"`
Status TenderStatus `json:"status"` BuyerOrganization *OrganizationResponse `json:"buyer_organization"`
TenderID string `json:"tender_id"` Status TenderStatus `json:"status"`
CreatedAt int64 `json:"created_at"` TenderID string `json:"tender_id"`
OverallSummary string `json:"overall_summary,omitempty"` // AI-generated summary from the pipeline CreatedAt int64 `json:"created_at"`
Language string `json:"language,omitempty"` OverallSummary string `json:"overall_summary,omitempty"` // AI-generated summary from the pipeline
Language string `json:"language,omitempty"`
} }
// TenderDocumentResponse represents a scraped tender document available for download. // TenderDocumentResponse represents a scraped tender document available for download.
@@ -135,29 +136,30 @@ func (t *Tender) ToResponseWithLanguage(language string) *TenderResponse {
} }
response := &TenderResponse{ response := &TenderResponse{
ID: t.ID.Hex(), ID: t.ID.Hex(),
NoticePublicationID: t.NoticePublicationID, NoticePublicationID: t.NoticePublicationID,
NoticeTypeCode: t.NoticeTypeCode, RelatedNoticePublicationIDs: t.RelatedNoticePublicationIDs,
Title: title, NoticeTypeCode: t.NoticeTypeCode,
Description: description, Title: title,
ProcurementTypeCode: t.ProcurementTypeCode, Description: description,
ProcedureCode: t.ProcedureCode, ProcurementTypeCode: t.ProcurementTypeCode,
MainClassification: t.MainClassification, ProcedureCode: t.ProcedureCode,
EstimatedValue: t.EstimatedValue, MainClassification: t.MainClassification,
Currency: t.Currency, EstimatedValue: t.EstimatedValue,
Duration: t.Duration, Currency: t.Currency,
DurationUnit: t.DurationUnit, Duration: t.Duration,
PublicationDate: t.PublicationDate, DurationUnit: t.DurationUnit,
TenderDeadline: t.TenderDeadline, PublicationDate: t.PublicationDate,
SubmissionDeadline: t.SubmissionDeadline, TenderDeadline: t.TenderDeadline,
ApplicationDeadline: t.ApplicationDeadline, SubmissionDeadline: t.SubmissionDeadline,
CountryCode: t.CountryCode, ApplicationDeadline: t.ApplicationDeadline,
SubmissionURL: t.SubmissionURL, CountryCode: t.CountryCode,
BuyerOrganization: org, SubmissionURL: t.SubmissionURL,
Status: t.Status, BuyerOrganization: org,
TenderID: t.TenderID, Status: t.Status,
CreatedAt: t.CreatedAt, TenderID: t.TenderID,
Language: usedLanguage, CreatedAt: t.CreatedAt,
Language: usedLanguage,
} }
return response return response
@@ -180,7 +182,7 @@ type AITranslateResponse struct {
type AISummaryResponse struct { type AISummaryResponse struct {
NoticeID string `json:"notice_id"` NoticeID string `json:"notice_id"`
OverallSummary string `json:"overall_summary"` OverallSummary string `json:"overall_summary"`
Source string `json:"source"` // "storage" or "on_demand" Source string `json:"source"` // storage | pending | unavailable
} }
// AISummarizeResponse represents the response for triggering on-demand AI summarization // AISummarizeResponse represents the response for triggering on-demand AI summarization
+9 -2
View File
@@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"time" "time"
"tm/pkg/logger" "tm/pkg/logger"
orm "tm/pkg/mongo"
"tm/pkg/response" "tm/pkg/response"
"github.com/asaskevich/govalidator" "github.com/asaskevich/govalidator"
@@ -477,11 +478,14 @@ func (h *TenderHandler) GetAISummary(c echo.Context) error {
summary, err := h.service.GetAISummary(c.Request().Context(), id) summary, err := h.service.GetAISummary(c.Request().Context(), id)
if err != nil { if err != nil {
if errors.Is(err, orm.ErrDocumentNotFound) {
return response.NotFound(c, "Tender not found")
}
h.logger.Error("Failed to get AI summary", map[string]interface{}{ h.logger.Error("Failed to get AI summary", map[string]interface{}{
"tender_id": id, "tender_id": id,
"error": err.Error(), "error": err.Error(),
}) })
return response.NotFound(c, err.Error()) return response.InternalServerError(c, "Failed to retrieve AI summary")
} }
return response.Success(c, summary, "AI summary retrieved successfully") return response.Success(c, summary, "AI summary retrieved successfully")
@@ -574,11 +578,14 @@ func (h *TenderHandler) GetPublicAISummary(c echo.Context) error {
summary, err := h.service.GetAISummary(c.Request().Context(), id) summary, err := h.service.GetAISummary(c.Request().Context(), id)
if err != nil { if err != nil {
if errors.Is(err, orm.ErrDocumentNotFound) {
return response.NotFound(c, "Tender not found")
}
h.logger.Error("Failed to get AI summary", map[string]interface{}{ h.logger.Error("Failed to get AI summary", map[string]interface{}{
"tender_id": id, "tender_id": id,
"error": err.Error(), "error": err.Error(),
}) })
return response.NotFound(c, err.Error()) return response.InternalServerError(c, "Failed to retrieve AI summary")
} }
return response.Success(c, summary, "AI summary retrieved successfully") return response.Success(c, summary, "AI summary retrieved successfully")
+18 -14
View File
@@ -827,14 +827,14 @@ func (s *tenderService) CleanupOldData(ctx context.Context, olderThan time.Durat
} }
// GetAISummary retrieves the AI-generated summary for a tender. // GetAISummary retrieves the AI-generated summary for a tender.
// It first tries to fetch from MinIO storage (async pipeline result). // It loads from MinIO storage (async pipeline result). When no summary exists yet,
// If not found or not ready, it returns an appropriate error. // the pipeline is still running, or storage is unavailable, 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) { func (s *tenderService) GetAISummary(ctx context.Context, id string) (*AISummaryResponse, error) {
if id == "" { if id == "" {
return nil, fmt.Errorf("tender ID cannot be empty") return nil, fmt.Errorf("tender ID cannot be empty")
} }
// Get tender to find the notice_publication_id
t, err := s.repository.GetByID(ctx, id) t, err := s.repository.GetByID(ctx, id)
if err != nil { if err != nil {
s.logger.Error("Failed to get tender for AI summary", map[string]interface{}{ s.logger.Error("Failed to get tender for AI summary", map[string]interface{}{
@@ -844,20 +844,24 @@ func (s *tenderService) GetAISummary(ctx context.Context, id string) (*AISummary
return nil, fmt.Errorf("failed to get tender: %w", err) return nil, fmt.Errorf("failed to get tender: %w", err)
} }
if t == nil { noticeID := strings.TrimSpace(t.NoticePublicationID)
return nil, fmt.Errorf("tender not found") emptyResp := func(source string) *AISummaryResponse {
return &AISummaryResponse{
NoticeID: noticeID,
OverallSummary: "",
Source: source,
}
} }
noticeID := t.NoticePublicationID if noticeID == "" || strings.TrimSpace(t.ContractFolderID) == "" {
if noticeID == "" { return emptyResp("unavailable"), nil
return nil, fmt.Errorf("tender has no notice publication ID")
}
if t.ContractFolderID == "" {
return nil, fmt.Errorf("tender has no contract folder id")
} }
if s.aiSummarizerStorage == nil { if s.aiSummarizerStorage == nil {
return nil, fmt.Errorf("AI summarizer storage is not configured") s.logger.Debug("AI summarizer storage not configured; returning empty AI summary", map[string]interface{}{
"tender_id": id,
})
return emptyResp("unavailable"), nil
} }
summary, usedNotice, err := s.lookupSummaryForTender(ctx, t) summary, usedNotice, err := s.lookupSummaryForTender(ctx, t)
@@ -877,10 +881,10 @@ func (s *tenderService) GetAISummary(ctx context.Context, id string) (*AISummary
}) })
if errors.Is(err, ai_summarizer.ErrSummaryNotReady) { if errors.Is(err, ai_summarizer.ErrSummaryNotReady) {
return nil, fmt.Errorf("AI summary is not ready yet, the pipeline is still processing") return emptyResp("pending"), nil
} }
if errors.Is(err, ai_summarizer.ErrObjectNotFound) || errors.Is(err, ai_summarizer.ErrNoOverallSummary) { if errors.Is(err, ai_summarizer.ErrObjectNotFound) || errors.Is(err, ai_summarizer.ErrNoOverallSummary) {
return nil, fmt.Errorf("AI summary not found — this tender has not been processed by the AI pipeline yet") return emptyResp("unavailable"), nil
} }
return nil, fmt.Errorf("failed to get AI summary: %w", err) return nil, fmt.Errorf("failed to get AI summary: %w", err)
} }