Implement AI recommendations for multiple companies and enhance company context handling
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Added functionality to retrieve merged AI recommendations for multiple companies, improving the relevance of tender suggestions based on company-specific data. - Introduced normalization functions to clean and deduplicate company IDs, ensuring accurate processing of recommendations. - Enhanced the company context resolution in customer middleware to support multiple assigned companies, improving the handling of company-specific requests. - Updated the tender recommendation logic to utilize the new merged recommendations and handle exclusions for rejected tenders accordingly. - Added unit tests to verify the new recommendation merging logic and company ID normalization, ensuring robust functionality. This update significantly enhances the tender recommendation process by allowing for more comprehensive and relevant suggestions based on multiple company contexts, improving user experience and satisfaction.
This commit is contained in:
@@ -32,6 +32,7 @@ type SearchForm struct {
|
||||
CountryCodes []string `query:"country_codes" valid:"optional"`
|
||||
RegionCodes []string `query:"region_codes" valid:"optional"`
|
||||
CompanyID *string `query:"company_id" valid:"optional"`
|
||||
CompanyIDs []string `query:"-" valid:"optional"`
|
||||
CustomerID *string `query:"customer_id" valid:"optional"`
|
||||
Status []string `query:"status" valid:"optional"`
|
||||
MainClassification *string `query:"main_classification" valid:"optional"`
|
||||
|
||||
@@ -354,7 +354,22 @@ func (h *TenderHandler) RecommendTenders(c echo.Context) error {
|
||||
form.OnlyActiveDeadlines = true
|
||||
form.ExcludeRejectedTenders = true
|
||||
|
||||
if customerCompanyID, ok := c.Get("company_id").(string); ok && customerCompanyID != "" {
|
||||
if customerIDStr, ok := c.Get("customer_id").(string); ok && customerIDStr != "" {
|
||||
form.CustomerID = &customerIDStr
|
||||
}
|
||||
|
||||
requestedCompanyID := ""
|
||||
if form.CompanyID != nil {
|
||||
requestedCompanyID = strings.TrimSpace(*form.CompanyID)
|
||||
}
|
||||
|
||||
if requestedCompanyID != "" {
|
||||
if customerCompanyID, ok := c.Get("company_id").(string); ok && customerCompanyID != "" {
|
||||
form.CompanyID = &customerCompanyID
|
||||
}
|
||||
} else if customerCompanyIDs, ok := c.Get("company_ids").([]string); ok && len(customerCompanyIDs) > 0 {
|
||||
form.CompanyIDs = append([]string(nil), customerCompanyIDs...)
|
||||
} else if customerCompanyID, ok := c.Get("company_id").(string); ok && customerCompanyID != "" {
|
||||
form.CompanyID = &customerCompanyID
|
||||
}
|
||||
|
||||
@@ -820,4 +835,3 @@ func (h *TenderHandler) GetAllAISummaries(c echo.Context) error {
|
||||
|
||||
return response.Success(c, summaries, "AI summaries retrieved successfully")
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,23 @@ type CompanyRejectedTenderLister interface {
|
||||
ListRejectedTenderIDsByCompanyID(ctx context.Context, companyID string) ([]string, error)
|
||||
}
|
||||
|
||||
func normalizeRecommendationCompanyScope(companyIDs []string) []string {
|
||||
normalized := make([]string, 0, len(companyIDs))
|
||||
seen := make(map[string]struct{}, len(companyIDs))
|
||||
for _, companyID := range companyIDs {
|
||||
clean := strings.TrimSpace(companyID)
|
||||
if clean == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[clean]; ok {
|
||||
continue
|
||||
}
|
||||
seen[clean] = struct{}{}
|
||||
normalized = append(normalized, clean)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func (s *tenderService) buildRejectedTenderIDSet(ctx context.Context, companyID string) (map[string]struct{}, error) {
|
||||
excluded := make(map[string]struct{})
|
||||
|
||||
@@ -27,6 +44,23 @@ func (s *tenderService) buildRejectedTenderIDSet(ctx context.Context, companyID
|
||||
return excluded, nil
|
||||
}
|
||||
|
||||
func (s *tenderService) buildRejectedTenderIDSetForCompanies(ctx context.Context, companyIDs []string) (map[string]struct{}, error) {
|
||||
excluded := make(map[string]struct{})
|
||||
for _, companyID := range normalizeRecommendationCompanyScope(companyIDs) {
|
||||
if s.rejectedTenderLister == nil {
|
||||
return excluded, nil
|
||||
}
|
||||
|
||||
ids, err := s.rejectedTenderLister.ListRejectedTenderIDsByCompanyID(ctx, companyID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list rejected tender exclusions: %w", err)
|
||||
}
|
||||
|
||||
addTenderIDsToSet(excluded, ids)
|
||||
}
|
||||
return excluded, nil
|
||||
}
|
||||
|
||||
func addTenderIDsToSet(set map[string]struct{}, ids []string) {
|
||||
for _, id := range ids {
|
||||
id = strings.TrimSpace(id)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tender
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
@@ -8,6 +9,14 @@ import (
|
||||
"tm/pkg/mongo"
|
||||
)
|
||||
|
||||
type stubRejectedTenderLister struct {
|
||||
byCompany map[string][]string
|
||||
}
|
||||
|
||||
func (s stubRejectedTenderLister) ListRejectedTenderIDsByCompanyID(_ context.Context, companyID string) ([]string, error) {
|
||||
return s.byCompany[companyID], nil
|
||||
}
|
||||
|
||||
func TestIsRecommendedTenderExcluded(t *testing.T) {
|
||||
tender := Tender{
|
||||
Model: mongo.Model{},
|
||||
@@ -61,3 +70,28 @@ func TestIsRecommendedTenderExcluded(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRejectedTenderIDSetForCompanies(t *testing.T) {
|
||||
service := &tenderService{
|
||||
rejectedTenderLister: stubRejectedTenderLister{
|
||||
byCompany: map[string][]string{
|
||||
"company-a": {" tender-1 ", "tender-2"},
|
||||
"company-b": {"tender-2", "tender-3"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got, err := service.buildRejectedTenderIDSetForCompanies(context.Background(), []string{"company-a", "company-b", "company-a"})
|
||||
if err != nil {
|
||||
t.Fatalf("buildRejectedTenderIDSetForCompanies() error = %v", err)
|
||||
}
|
||||
|
||||
for _, tenderID := range []string{"tender-1", "tender-2", "tender-3"} {
|
||||
if _, ok := got[tenderID]; !ok {
|
||||
t.Fatalf("expected excluded set to contain %q", tenderID)
|
||||
}
|
||||
}
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("buildRejectedTenderIDSetForCompanies() len = %d, want 3", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -932,19 +932,33 @@ func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination
|
||||
|
||||
// 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) {
|
||||
if form.CompanyID == nil || strings.TrimSpace(*form.CompanyID) == "" {
|
||||
companyIDs := normalizeRecommendationCompanyScope(form.CompanyIDs)
|
||||
companyID := ""
|
||||
if form.CompanyID != nil {
|
||||
companyID = strings.TrimSpace(*form.CompanyID)
|
||||
}
|
||||
|
||||
if companyID == "" && len(companyIDs) == 0 {
|
||||
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,
|
||||
"company_id": companyID,
|
||||
"company_ids": companyIDs,
|
||||
})
|
||||
|
||||
recommendations, err := s.companyService.GetAIRecommendations(ctx, companyID)
|
||||
var (
|
||||
recommendations []company.RecommendedTenderResponse
|
||||
err error
|
||||
)
|
||||
if len(companyIDs) > 0 {
|
||||
recommendations, err = s.companyService.GetMergedAIRecommendations(ctx, companyIDs)
|
||||
} else {
|
||||
recommendations, err = s.companyService.GetAIRecommendations(ctx, companyID)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -959,11 +973,16 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat
|
||||
|
||||
var excluded map[string]struct{}
|
||||
if form.ExcludeRejectedTenders {
|
||||
excluded, err = s.buildRejectedTenderIDSet(ctx, companyID)
|
||||
if len(companyIDs) > 0 {
|
||||
excluded, err = s.buildRejectedTenderIDSetForCompanies(ctx, companyIDs)
|
||||
} else {
|
||||
excluded, err = s.buildRejectedTenderIDSet(ctx, companyID)
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to build recommendation exclusions", map[string]interface{}{
|
||||
"company_id": companyID,
|
||||
"error": err.Error(),
|
||||
"company_id": companyID,
|
||||
"company_ids": companyIDs,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to build recommendation exclusions: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user