Enhance tender response structure and update recommendation handler
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Added new fields `Rank` and `Analysis` to the `TenderResponse` struct for improved data representation. - Updated the `RecommendTenders` handler to reflect changes in API documentation, including a new summary and description for AI-ranked tender recommendations. - Improved error handling in the recommendation process, ensuring appropriate responses for missing company IDs and unavailable AI services. This update enhances the tender response capabilities and improves the clarity of the recommendation API, aligning with the overall goal of providing better insights for users.
This commit is contained in:
+73
-89
@@ -844,32 +844,7 @@ func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination
|
||||
"to": form.DeadlineTo,
|
||||
})
|
||||
|
||||
// If customer ID is provided, get customer keywords and add to search
|
||||
if form.CustomerID != nil && *form.CustomerID != "" {
|
||||
customerResp, err := s.customerService.GetByID(ctx, *form.CustomerID)
|
||||
if err == nil && customerResp != nil {
|
||||
// Only use keywords if status is ready
|
||||
if customerResp.KeywordsStatus == "ready" && len(customerResp.Keywords) > 0 {
|
||||
// Combine customer keywords with existing search query
|
||||
keywordsQuery := strings.Join(customerResp.Keywords, " ")
|
||||
if form.Search != nil && *form.Search != "" {
|
||||
combinedSearch := *form.Search + " " + keywordsQuery
|
||||
form.Search = &combinedSearch
|
||||
} else {
|
||||
form.Search = &keywordsQuery
|
||||
}
|
||||
s.logger.Info("Using customer keywords for search", map[string]interface{}{
|
||||
"customer_id": *form.CustomerID,
|
||||
"keywords": customerResp.Keywords,
|
||||
})
|
||||
} else {
|
||||
s.logger.Info("Customer keywords not ready, skipping keyword-based search", map[string]interface{}{
|
||||
"customer_id": *form.CustomerID,
|
||||
"keywords_status": customerResp.KeywordsStatus,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// Profile keywords are not used for tender search.
|
||||
|
||||
if form.DocumentsScraped {
|
||||
if err := s.enrichDocumentsScrapedFilter(ctx, form); err != nil {
|
||||
@@ -944,78 +919,87 @@ func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination
|
||||
nil
|
||||
}
|
||||
|
||||
// Recommend retrieves tenders with pagination and filtering
|
||||
// Recommend retrieves AI-ranked tenders for the authenticated customer's company.
|
||||
func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) {
|
||||
s.logger.Info("Recommend tenders", map[string]interface{}{
|
||||
"company_id": form.CompanyID,
|
||||
"customer_id": form.CustomerID,
|
||||
if form.CompanyID == nil || strings.TrimSpace(*form.CompanyID) == "" {
|
||||
return nil, errors.New("company ID is required")
|
||||
}
|
||||
if pagination.Cursor != "" {
|
||||
return nil, fmt.Errorf("%w: cursor pagination is not supported for recommendations", response.ErrInvalidPagination)
|
||||
}
|
||||
|
||||
companyID := strings.TrimSpace(*form.CompanyID)
|
||||
s.logger.Info("Recommend tenders via AI service", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
})
|
||||
|
||||
form.Status = []string{string(TenderStatusActive)}
|
||||
|
||||
// Priority: Use customer keywords if available, otherwise use company CPV codes
|
||||
if form.CustomerID != nil && *form.CustomerID != "" {
|
||||
customerResp, err := s.customerService.GetByID(ctx, *form.CustomerID)
|
||||
if err == nil && customerResp != nil {
|
||||
// Only use keywords if status is ready
|
||||
if customerResp.KeywordsStatus == "ready" && len(customerResp.Keywords) > 0 {
|
||||
// Use customer keywords for recommendation
|
||||
keywordsQuery := strings.Join(customerResp.Keywords, " ")
|
||||
form.Search = &keywordsQuery
|
||||
s.logger.Info("Using customer keywords for recommendation", map[string]interface{}{
|
||||
"customer_id": *form.CustomerID,
|
||||
"keywords": customerResp.Keywords,
|
||||
})
|
||||
} else {
|
||||
s.logger.Info("Customer keywords not ready, falling back to company CPV codes", map[string]interface{}{
|
||||
"customer_id": *form.CustomerID,
|
||||
"keywords_status": customerResp.KeywordsStatus,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to company CPV codes if no customer keywords
|
||||
if form.Search == nil || *form.Search == "" {
|
||||
if form.CompanyID != nil && *form.CompanyID != "" {
|
||||
company, err := s.companyService.GetByID(ctx, *form.CompanyID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get company for CPV codes, skipping CPV-based filtering", map[string]interface{}{
|
||||
"company_id": form.CompanyID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
// Continue without CPV-based filtering instead of failing
|
||||
} else if company.Tags != nil && len(company.Tags.CPVCodes) > 0 {
|
||||
form.Classifications = company.Tags.CPVCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if form.DocumentsScraped {
|
||||
if err := s.enrichDocumentsScrapedFilter(ctx, form); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(form.ContractFolderIDsWithDocuments) == 0 {
|
||||
return s.emptySearchResponse(pagination), nil
|
||||
}
|
||||
}
|
||||
|
||||
page, err := s.repository.Search(ctx, form, pagination)
|
||||
recommendations, err := s.companyService.GetAIRecommendations(ctx, companyID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to list tenders", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to list tenders: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
if len(recommendations) == 0 {
|
||||
return s.emptySearchResponse(pagination), nil
|
||||
}
|
||||
|
||||
tenderResponses := make([]TenderResponse, 0, len(page.Items))
|
||||
for _, tender := range page.Items {
|
||||
tenderResponses = append(tenderResponses, *tender.ToResponseWithLanguage(s.pickResponseLanguage(form.Language)))
|
||||
tenderIDs := make([]string, 0, len(recommendations))
|
||||
for _, rec := range recommendations {
|
||||
if id := strings.TrimSpace(rec.TenderID); id != "" {
|
||||
tenderIDs = append(tenderIDs, id)
|
||||
}
|
||||
}
|
||||
if len(tenderIDs) == 0 {
|
||||
return s.emptySearchResponse(pagination), nil
|
||||
}
|
||||
|
||||
tenders, err := s.repository.GetByIDs(ctx, tenderIDs)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to load recommended tenders", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to load recommended tenders: %w", err)
|
||||
}
|
||||
|
||||
tenderByID := make(map[string]Tender, len(tenders))
|
||||
for i := range tenders {
|
||||
tenderByID[tenders[i].GetID()] = tenders[i]
|
||||
}
|
||||
|
||||
lang := s.pickResponseLanguage(form.Language)
|
||||
now := time.Now().Unix()
|
||||
ordered := make([]TenderResponse, 0, len(recommendations))
|
||||
for _, rec := range recommendations {
|
||||
tenderID := strings.TrimSpace(rec.TenderID)
|
||||
tender, ok := tenderByID[tenderID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if form.OnlyActiveDeadlines && tender.TenderDeadline <= now {
|
||||
continue
|
||||
}
|
||||
if len(form.Status) > 0 && tender.Status != TenderStatusActive {
|
||||
continue
|
||||
}
|
||||
|
||||
resp := tender.ToResponseWithLanguage(lang)
|
||||
resp.Rank = rec.Rank
|
||||
resp.Analysis = rec.Analysis
|
||||
ordered = append(ordered, *resp)
|
||||
}
|
||||
|
||||
total := int64(len(ordered))
|
||||
start := pagination.Offset
|
||||
if start > len(ordered) {
|
||||
start = len(ordered)
|
||||
}
|
||||
end := start + pagination.Limit
|
||||
if end > len(ordered) {
|
||||
end = len(ordered)
|
||||
}
|
||||
|
||||
return &SearchResponse{
|
||||
Tenders: tenderResponses,
|
||||
Metadata: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
|
||||
Tenders: ordered[start:end],
|
||||
Metadata: pagination.ListMeta(total, "", end < len(ordered), start),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user