ai translation refactor

This commit is contained in:
Mazyar
2026-05-19 21:58:44 +03:30
parent e9ec33f0d4
commit 2d5df8556f
5 changed files with 122 additions and 21 deletions
+107 -9
View File
@@ -45,7 +45,7 @@ type TenderDocumentDownload struct {
// TenderService interface defines tender business logic operations
type Service interface {
// Tender operations
GetByID(ctx context.Context, id string) (*TenderResponse, error)
GetByID(ctx context.Context, id string, language string) (*TenderResponse, error)
Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, error)
Delete(ctx context.Context, id string) error
Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*SearchResponse, error)
@@ -77,7 +77,10 @@ type tenderService struct {
defaultLanguage string
}
const aiSummaryEnrichmentTimeout = 2 * time.Second
const (
aiSummaryEnrichmentTimeout = 2 * time.Second
aiTranslationEnrichmentTimeout = 2 * time.Second
)
var (
ErrTenderDocumentStorageUnavailable = errors.New("tender document storage is unavailable")
@@ -101,10 +104,12 @@ func NewService(repository TenderRepository, companyService company.Service, cus
}
}
// GetTenderByID retrieves a tender by ID
func (s *tenderService) GetByID(ctx context.Context, id string) (*TenderResponse, error) {
// GetByID retrieves a tender by ID. When language is empty, the service default (usually en)
// is used to overlay title/description from MinIO translations when available.
func (s *tenderService) GetByID(ctx context.Context, id string, language string) (*TenderResponse, error) {
s.logger.Info("Retrieving tender by ID", map[string]interface{}{
"tender_id": id,
"language": s.resolveDetailLanguage(language),
})
tender, err := s.repository.GetByID(ctx, id)
@@ -116,12 +121,15 @@ func (s *tenderService) GetByID(ctx context.Context, id string) (*TenderResponse
return nil, fmt.Errorf("failed to retrieve tender: %w", err)
}
resp := tender.ToResponseWithLanguage(s.defaultLanguage)
return s.buildDetailResponse(ctx, tender, language), nil
}
// Enrich with AI summary from MinIO storage (best-effort, non-blocking)
// buildDetailResponse maps a tender to API form and enriches translation (MinIO) and summary.
func (s *tenderService) buildDetailResponse(ctx context.Context, tender *Tender, language string) *TenderResponse {
resp := tender.ToResponseWithLanguage("")
s.enrichWithTranslation(ctx, tender, resp, language)
s.enrichWithAISummary(ctx, tender, resp)
return resp, nil
return resp
}
// enrichWithAISummary loads overall_summary from MinIO (PROC_<contractFolderID>/<noticePublicationID>/tender.json)
@@ -164,6 +172,96 @@ func (s *tenderService) enrichWithAISummary(ctx context.Context, t *Tender, resp
s.persistAIOverallSummary(enrichCtx, t, summary)
}
// enrichWithTranslation overlays title/description from MinIO when the pipeline has finished
// for the requested language. Does not call the AI translate API on GET.
func (s *tenderService) enrichWithTranslation(ctx context.Context, t *Tender, resp *TenderResponse, language string) {
if t == nil || resp == nil {
return
}
if s.aiSummarizerStorage == nil || strings.TrimSpace(t.ContractFolderID) == "" {
return
}
targetLanguage := s.resolveDetailLanguage(language)
enrichCtx, cancel := context.WithTimeout(ctx, aiTranslationEnrichmentTimeout)
defer cancel()
stored, usedNotice, err := s.lookupTranslationForTender(enrichCtx, t, targetLanguage)
if err != nil {
s.logger.Debug("Translation not available for tender detail", map[string]interface{}{
"tender_id": resp.ID,
"contract_folder_id": t.ContractFolderID,
"notice_id": t.NoticePublicationID,
"language": targetLanguage,
"error": err.Error(),
})
return
}
if strings.TrimSpace(stored.Title) == "" {
return
}
resp.Title = stored.Title
resp.Description = stored.Description
resp.Language = targetLanguage
if usedNotice != t.NoticePublicationID {
s.logger.Debug("Translation 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,
"language": targetLanguage,
})
}
}
// lookupTranslationForTender returns stored translation text, trying primary then related notice ids.
func (s *tenderService) lookupTranslationForTender(ctx context.Context, t *Tender, language string) (ai_summarizer.StoredTranslation, string, error) {
if s.aiSummarizerStorage == nil {
return ai_summarizer.StoredTranslation{}, "", fmt.Errorf("AI summarizer storage is not configured")
}
candidates := orderedNoticeCandidates(t)
if len(candidates) == 0 {
return ai_summarizer.StoredTranslation{}, "", ai_summarizer.ErrObjectNotFound
}
var lastErr error
for _, noticeID := range candidates {
stored, err := s.aiSummarizerStorage.GetTranslationFromStorage(ctx, t.ContractFolderID, noticeID, language)
if err == nil {
return stored, noticeID, nil
}
lastErr = err
if !errors.Is(err, ai_summarizer.ErrTranslationNotReady) &&
!errors.Is(err, ai_summarizer.ErrObjectNotFound) &&
!errors.Is(err, ai_summarizer.ErrNoTranslation) {
if errors.Is(err, ai_summarizer.ErrMinIOUnavailable) {
s.logger.Error("MinIO unavailable while fetching translation", map[string]interface{}{
"tender_id": t.ID.Hex(),
"contract_folder_id": t.ContractFolderID,
"notice_id": noticeID,
"language": language,
"error": err.Error(),
})
}
return ai_summarizer.StoredTranslation{}, "", err
}
}
if lastErr == nil {
lastErr = ai_summarizer.ErrObjectNotFound
}
return ai_summarizer.StoredTranslation{}, "", lastErr
}
// resolveDetailLanguage returns the language code used for detail responses.
func (s *tenderService) resolveDetailLanguage(language string) string {
clean := strings.ToLower(strings.TrimSpace(language))
if clean == "" {
return s.defaultLanguage
}
return clean
}
// persistAIOverallSummary stores the resolved summary on the tender for list/search responses.
func (s *tenderService) persistAIOverallSummary(ctx context.Context, t *Tender, summary string) {
summary = strings.TrimSpace(summary)
@@ -300,7 +398,7 @@ func (s *tenderService) Update(ctx context.Context, req UpdateTenderRequest) (*T
"tender_id": tender.ID,
})
return tender.ToResponseWithLanguage(s.defaultLanguage), nil
return s.buildDetailResponse(ctx, tender, ""), nil
}
// Delete deletes a tender