ai translation refactor
This commit is contained in:
@@ -191,7 +191,7 @@ func (s *feedbackService) GetByTenderID(ctx context.Context, tenderID, companyID
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tender, err := s.tenderService.GetByID(ctx, feedback.TenderID)
|
||||
tender, err := s.tenderService.GetByID(ctx, feedback.TenderID, "")
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get tender", map[string]interface{}{
|
||||
"tender_id": feedback.TenderID,
|
||||
@@ -234,7 +234,7 @@ func (s *feedbackService) GetByTenderID(ctx context.Context, tenderID, companyID
|
||||
}
|
||||
|
||||
func (s *feedbackService) getDetails(ctx context.Context, tenderID, companyID string) (*tenderResponse, *companyResponse, error) {
|
||||
tender, err := s.tenderService.GetByID(ctx, tenderID)
|
||||
tender, err := s.tenderService.GetByID(ctx, tenderID, "")
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get tender", map[string]interface{}{
|
||||
"tender_id": tenderID,
|
||||
|
||||
@@ -255,7 +255,8 @@ func (t *Tender) ToResponse() *TenderResponse {
|
||||
return t.ToResponseWithLanguage("")
|
||||
}
|
||||
|
||||
// ToResponseWithLanguage converts a Tender entity to TenderResponse.
|
||||
// ToResponseWithLanguage converts a Tender entity to TenderResponse using Mongo fields.
|
||||
// Detail endpoints overlay MinIO translations in the service layer.
|
||||
func (t *Tender) ToResponseWithLanguage(language string) *TenderResponse {
|
||||
title := t.Title
|
||||
description := t.Description
|
||||
|
||||
@@ -38,6 +38,7 @@ func NewHandler(service Service, logger logger.Logger) *TenderHandler {
|
||||
// @Tags Admin-Tenders
|
||||
// @Produce json
|
||||
// @Param id path string true "Tender ID"
|
||||
// @Param language query string false "Translation language code (defaults to en)"
|
||||
// @Success 200 {object} response.APIResponse{data=TenderResponse}
|
||||
// @Failure 404 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
@@ -49,7 +50,7 @@ func (h *TenderHandler) Get(c echo.Context) error {
|
||||
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty")
|
||||
}
|
||||
|
||||
tender, err := h.service.GetByID(c.Request().Context(), id)
|
||||
tender, err := h.service.GetByID(c.Request().Context(), id, c.QueryParam("language"))
|
||||
if err != nil {
|
||||
return response.NotFound(c, "Tender not found")
|
||||
}
|
||||
@@ -366,6 +367,7 @@ func (h *TenderHandler) RecommendTenders(c echo.Context) error {
|
||||
// @Tags Tenders
|
||||
// @Produce json
|
||||
// @Param id path string true "Tender ID"
|
||||
// @Param language query string false "Translation language code (defaults to en)"
|
||||
// @Success 200 {object} response.APIResponse{data=map[string]interface{}}
|
||||
// @Failure 404 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
@@ -377,7 +379,7 @@ func (h *TenderHandler) GetPublicTenderDetails(c echo.Context) error {
|
||||
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty")
|
||||
}
|
||||
|
||||
tender, err := h.service.GetByID(c.Request().Context(), id)
|
||||
tender, err := h.service.GetByID(c.Request().Context(), id, c.QueryParam("language"))
|
||||
if err != nil {
|
||||
return response.NotFound(c, "Tender not found")
|
||||
}
|
||||
|
||||
+107
-9
@@ -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
|
||||
|
||||
@@ -134,7 +134,7 @@ func (s *tenderApprovalService) GetTenderApprovalByIDWithTender(ctx context.Cont
|
||||
}
|
||||
|
||||
// Get tender details
|
||||
tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID)
|
||||
tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID, "")
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
||||
"tender_id": tenderApproval.TenderID,
|
||||
@@ -205,7 +205,7 @@ func (s *tenderApprovalService) GetTenderApprovalByTenderAndCompanyWithTender(ct
|
||||
}
|
||||
|
||||
// Get tender details
|
||||
tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID)
|
||||
tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID, "")
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
||||
"tender_id": tenderApproval.TenderID,
|
||||
@@ -299,7 +299,7 @@ func (s *tenderApprovalService) GetTenderApprovalsByCompanyIDWithTender(ctx cont
|
||||
}
|
||||
for _, approval := range tenderApprovals {
|
||||
// Get tender details
|
||||
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID)
|
||||
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID, "")
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
||||
"tender_id": approval.TenderID,
|
||||
@@ -444,7 +444,7 @@ func (s *tenderApprovalService) UpdateTenderApprovalWithTender(ctx context.Conte
|
||||
}
|
||||
|
||||
// Get tender details
|
||||
tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID)
|
||||
tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID, "")
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
||||
"tender_id": tenderApproval.TenderID,
|
||||
@@ -784,7 +784,7 @@ func (s *tenderApprovalService) GetTenderApprovalsByStatusWithTender(ctx context
|
||||
var responses []*TenderApprovalWithTenderResponse
|
||||
for _, approval := range tenderApprovals {
|
||||
// Get tender details
|
||||
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID)
|
||||
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID, "")
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
||||
"tender_id": approval.TenderID,
|
||||
@@ -832,7 +832,7 @@ func (s *tenderApprovalService) GetTenderApprovalsBySubmissionModeWithTender(ctx
|
||||
var responses []*TenderApprovalWithTenderResponse
|
||||
for _, approval := range tenderApprovals {
|
||||
// Get tender details
|
||||
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID)
|
||||
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID, "")
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
||||
"tender_id": approval.TenderID,
|
||||
@@ -930,7 +930,7 @@ func (s *tenderApprovalService) ListTenderApprovalsWithTender(ctx context.Contex
|
||||
var responses []*TenderApprovalWithTenderResponse
|
||||
for _, approval := range tenderApprovals {
|
||||
// Get tender details
|
||||
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID)
|
||||
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID, "")
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
||||
"tender_id": approval.TenderID,
|
||||
|
||||
Reference in New Issue
Block a user