Enhance company service with AI recommendation caching and onboarding improvements
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Updated the `companyService` to include Redis caching for AI recommendations, improving performance and reducing redundant AI calls. - Introduced asynchronous AI onboarding triggered after company profile updates, enhancing user experience by offloading processing. - Added configuration for recommendation cache TTL in the `AISummarizerConfig`, allowing for flexible cache management. - Implemented methods for caching, retrieving, and invalidating AI recommendations in the `companyService`, ensuring efficient data handling. This update enhances the company's AI recommendation capabilities, providing faster responses and a more efficient onboarding process.
This commit is contained in:
@@ -69,6 +69,8 @@ type AISummarizerConfig struct {
|
|||||||
APIRetryCount int `env:"AI_SUMMARIZER_API_RETRY_COUNT" envDefault:"2"`
|
APIRetryCount int `env:"AI_SUMMARIZER_API_RETRY_COUNT" envDefault:"2"`
|
||||||
APIRetryDelay time.Duration `env:"AI_SUMMARIZER_API_RETRY_DELAY" envDefault:"3s"`
|
APIRetryDelay time.Duration `env:"AI_SUMMARIZER_API_RETRY_DELAY" envDefault:"3s"`
|
||||||
DefaultLanguage string `env:"AI_SUMMARIZER_DEFAULT_LANGUAGE" envDefault:"en"`
|
DefaultLanguage string `env:"AI_SUMMARIZER_DEFAULT_LANGUAGE" envDefault:"en"`
|
||||||
|
// RecommendationCacheTTL caches AI /recommend responses per company. Set to 0 to disable.
|
||||||
|
RecommendationCacheTTL time.Duration `env:"AI_RECOMMENDATION_CACHE_TTL" envDefault:"15m"`
|
||||||
|
|
||||||
// MinIO storage settings
|
// MinIO storage settings
|
||||||
MinioEndpoint string `env:"AI_SUMMARIZER_MINIO_ENDPOINT" envDefault:""`
|
MinioEndpoint string `env:"AI_SUMMARIZER_MINIO_ENDPOINT" envDefault:""`
|
||||||
|
|||||||
+1
-1
@@ -220,7 +220,7 @@ func main() {
|
|||||||
auditLogger := audit.NewLogger(logger)
|
auditLogger := audit.NewLogger(logger)
|
||||||
userService := user.NewService(userRepository, logger, userAuthService, userValidator, notificationSDK, redisClient, auditLogger)
|
userService := user.NewService(userRepository, logger, userAuthService, userValidator, notificationSDK, redisClient, auditLogger)
|
||||||
categoryService := company_category.NewService(categoryRepository, logger)
|
categoryService := company_category.NewService(categoryRepository, logger)
|
||||||
companyService := company.NewService(companyRepository, categoryService, fileStoreService, aiRecommendationClient, logger)
|
companyService := company.NewService(companyRepository, categoryService, fileStoreService, aiRecommendationClient, redisClient, conf.AISummarizer.RecommendationCacheTTL, logger)
|
||||||
customerService := customer.NewService(customerRepository, logger, customerAuthService, companyService, redisClient, notificationSDK, customerValidator, auditLogger)
|
customerService := customer.NewService(customerRepository, logger, customerAuthService, companyService, redisClient, notificationSDK, customerValidator, auditLogger)
|
||||||
tenderService := tender.NewService(tenderRepository, companyService, customerService, logger, aiSummarizerClient, aiSummarizerStorage, conf.AISummarizer.DefaultLanguage)
|
tenderService := tender.NewService(tenderRepository, companyService, customerService, logger, aiSummarizerClient, aiSummarizerStorage, conf.AISummarizer.DefaultLanguage)
|
||||||
feedbackService := feedback.NewService(feedbackRepo, tenderService, companyService, logger)
|
feedbackService := feedback.NewService(feedbackRepo, tenderService, companyService, logger)
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ func (s *companyService) UploadDocuments(ctx context.Context, companyID, uploade
|
|||||||
"documents_total": len(company.DocumentFileIDs),
|
"documents_total": len(company.DocumentFileIDs),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
s.triggerAIOnboardingAsync(companyID)
|
||||||
|
|
||||||
return &UploadDocumentsResponse{
|
return &UploadDocumentsResponse{
|
||||||
DocumentFileIDs: company.DocumentFileIDs,
|
DocumentFileIDs: company.DocumentFileIDs,
|
||||||
Uploaded: uploaded,
|
Uploaded: uploaded,
|
||||||
|
|||||||
@@ -9,11 +9,19 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
|
||||||
"tm/pkg/ai_summarizer"
|
"tm/pkg/ai_summarizer"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrAIRecommendationNotConfigured = errors.New("AI recommendation service is not configured")
|
var ErrAIRecommendationNotConfigured = errors.New("AI recommendation service is not configured")
|
||||||
|
|
||||||
|
const aiRecommendationCacheKeyPrefix = "ai:recommendations:"
|
||||||
|
|
||||||
|
func aiRecommendationCacheKey(companyID string) string {
|
||||||
|
return aiRecommendationCacheKeyPrefix + companyID
|
||||||
|
}
|
||||||
|
|
||||||
// AIRecommendationClient defines the interface for company tender recommendation AI operations.
|
// AIRecommendationClient defines the interface for company tender recommendation AI operations.
|
||||||
type AIRecommendationClient interface {
|
type AIRecommendationClient interface {
|
||||||
StartOnboarding(ctx context.Context, reqBody ai_summarizer.OnboardingRequest) (*ai_summarizer.OnboardingResponse, error)
|
StartOnboarding(ctx context.Context, reqBody ai_summarizer.OnboardingRequest) (*ai_summarizer.OnboardingResponse, error)
|
||||||
@@ -77,18 +85,56 @@ func (s *companyService) StartAIOnboarding(ctx context.Context, companyID string
|
|||||||
return nil, fmt.Errorf("failed to start AI onboarding: %w", err)
|
return nil, fmt.Errorf("failed to start AI onboarding: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.invalidateAIRecommendationCache(ctx, companyID)
|
||||||
|
|
||||||
return &OnboardingResponse{Status: result.Status}, nil
|
return &OnboardingResponse{Status: result.Status}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// triggerAIOnboardingAsync starts AI onboarding in the background after company profile changes.
|
||||||
|
func (s *companyService) triggerAIOnboardingAsync(companyID string) {
|
||||||
|
if s.aiRecommendationClient == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
companyID = strings.TrimSpace(companyID)
|
||||||
|
if companyID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ctx := context.Background()
|
||||||
|
s.logger.Info("Starting async AI company onboarding", map[string]interface{}{
|
||||||
|
"company_id": companyID,
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, err := s.StartAIOnboarding(ctx, companyID); err != nil {
|
||||||
|
s.logger.Error("Async AI company onboarding failed", map[string]interface{}{
|
||||||
|
"company_id": companyID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Async AI company onboarding completed", map[string]interface{}{
|
||||||
|
"company_id": companyID,
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
// GetAIRecommendations retrieves ranked tender recommendations from the AI team.
|
// GetAIRecommendations retrieves ranked tender recommendations from the AI team.
|
||||||
func (s *companyService) GetAIRecommendations(ctx context.Context, companyID string) ([]RecommendedTenderResponse, error) {
|
func (s *companyService) GetAIRecommendations(ctx context.Context, companyID string) ([]RecommendedTenderResponse, error) {
|
||||||
if s.aiRecommendationClient == nil {
|
if s.aiRecommendationClient == nil {
|
||||||
return nil, ErrAIRecommendationNotConfigured
|
return nil, ErrAIRecommendationNotConfigured
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(companyID) == "" {
|
companyID = strings.TrimSpace(companyID)
|
||||||
|
if companyID == "" {
|
||||||
return nil, errors.New("company ID is required")
|
return nil, errors.New("company ID is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cached, ok := s.getCachedAIRecommendations(ctx, companyID); ok {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := s.repository.GetByID(ctx, companyID); err != nil {
|
if _, err := s.repository.GetByID(ctx, companyID); err != nil {
|
||||||
s.logger.Error("Failed to load company for AI recommendations", map[string]interface{}{
|
s.logger.Error("Failed to load company for AI recommendations", map[string]interface{}{
|
||||||
"company_id": companyID,
|
"company_id": companyID,
|
||||||
@@ -121,9 +167,80 @@ func (s *companyService) GetAIRecommendations(ctx context.Context, companyID str
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.cacheAIRecommendations(ctx, companyID, responses)
|
||||||
|
|
||||||
return responses, nil
|
return responses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *companyService) getCachedAIRecommendations(ctx context.Context, companyID string) ([]RecommendedTenderResponse, bool) {
|
||||||
|
if s.redisClient == nil || s.recommendationCacheTTL <= 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := s.redisClient.Get(ctx, aiRecommendationCacheKey(companyID))
|
||||||
|
if err != nil {
|
||||||
|
if err != redis.Nil {
|
||||||
|
s.logger.Warn("Failed to read AI recommendation cache", map[string]interface{}{
|
||||||
|
"company_id": companyID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var responses []RecommendedTenderResponse
|
||||||
|
if err := json.Unmarshal([]byte(raw), &responses); err != nil {
|
||||||
|
s.logger.Warn("Failed to decode AI recommendation cache", map[string]interface{}{
|
||||||
|
"company_id": companyID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
_ = s.redisClient.Del(ctx, aiRecommendationCacheKey(companyID))
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Debug("AI tender recommendations served from cache", map[string]interface{}{
|
||||||
|
"company_id": companyID,
|
||||||
|
"count": len(responses),
|
||||||
|
})
|
||||||
|
|
||||||
|
return responses, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *companyService) cacheAIRecommendations(ctx context.Context, companyID string, responses []RecommendedTenderResponse) {
|
||||||
|
if s.redisClient == nil || s.recommendationCacheTTL <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := json.Marshal(responses)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("Failed to encode AI recommendations for cache", map[string]interface{}{
|
||||||
|
"company_id": companyID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.redisClient.Set(ctx, aiRecommendationCacheKey(companyID), string(encoded), s.recommendationCacheTTL); err != nil {
|
||||||
|
s.logger.Warn("Failed to store AI recommendation cache", map[string]interface{}{
|
||||||
|
"company_id": companyID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *companyService) invalidateAIRecommendationCache(ctx context.Context, companyID string) {
|
||||||
|
if s.redisClient == nil || s.recommendationCacheTTL <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.redisClient.Del(ctx, aiRecommendationCacheKey(companyID)); err != nil {
|
||||||
|
s.logger.Warn("Failed to invalidate AI recommendation cache", map[string]interface{}{
|
||||||
|
"company_id": companyID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *companyService) buildOnboardingDocuments(fileIDs []string) (string, error) {
|
func (s *companyService) buildOnboardingDocuments(fileIDs []string) (string, error) {
|
||||||
if len(fileIDs) == 0 {
|
if len(fileIDs) == 0 {
|
||||||
return "[]", nil
|
return "[]", nil
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ package company
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"time"
|
||||||
|
|
||||||
"tm/internal/company_category"
|
"tm/internal/company_category"
|
||||||
"tm/pkg/filestore"
|
"tm/pkg/filestore"
|
||||||
"tm/pkg/logger"
|
"tm/pkg/logger"
|
||||||
|
"tm/pkg/redis"
|
||||||
"tm/pkg/response"
|
"tm/pkg/response"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,6 +52,8 @@ type companyService struct {
|
|||||||
categoryService company_category.Service
|
categoryService company_category.Service
|
||||||
fileStore filestore.FileStoreService
|
fileStore filestore.FileStoreService
|
||||||
aiRecommendationClient AIRecommendationClient
|
aiRecommendationClient AIRecommendationClient
|
||||||
|
redisClient redis.Client
|
||||||
|
recommendationCacheTTL time.Duration
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +63,8 @@ func NewService(
|
|||||||
categoryService company_category.Service,
|
categoryService company_category.Service,
|
||||||
fileStore filestore.FileStoreService,
|
fileStore filestore.FileStoreService,
|
||||||
aiRecommendationClient AIRecommendationClient,
|
aiRecommendationClient AIRecommendationClient,
|
||||||
|
redisClient redis.Client,
|
||||||
|
recommendationCacheTTL time.Duration,
|
||||||
logger logger.Logger,
|
logger logger.Logger,
|
||||||
) Service {
|
) Service {
|
||||||
return &companyService{
|
return &companyService{
|
||||||
@@ -67,6 +72,8 @@ func NewService(
|
|||||||
categoryService: categoryService,
|
categoryService: categoryService,
|
||||||
fileStore: fileStore,
|
fileStore: fileStore,
|
||||||
aiRecommendationClient: aiRecommendationClient,
|
aiRecommendationClient: aiRecommendationClient,
|
||||||
|
redisClient: redisClient,
|
||||||
|
recommendationCacheTTL: recommendationCacheTTL,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,6 +157,8 @@ func (s *companyService) Create(ctx context.Context, form *CompanyForm) (*Compan
|
|||||||
"type": company.Type,
|
"type": company.Type,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
s.triggerAIOnboardingAsync(company.GetID())
|
||||||
|
|
||||||
return company.ToResponse(nil), nil
|
return company.ToResponse(nil), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,6 +332,8 @@ func (s *companyService) Update(ctx context.Context, id string, form *CompanyFor
|
|||||||
"name": company.Name,
|
"name": company.Name,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
s.triggerAIOnboardingAsync(company.GetID())
|
||||||
|
|
||||||
return company.ToResponse(nil), nil
|
return company.ToResponse(nil), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user