ai translation refactor
This commit is contained in:
@@ -191,7 +191,7 @@ func (s *feedbackService) GetByTenderID(ctx context.Context, tenderID, companyID
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tender, err := s.tenderService.GetByID(ctx, feedback.TenderID)
|
tender, err := s.tenderService.GetByID(ctx, feedback.TenderID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to get tender", map[string]interface{}{
|
s.logger.Error("Failed to get tender", map[string]interface{}{
|
||||||
"tender_id": feedback.TenderID,
|
"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) {
|
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 {
|
if err != nil {
|
||||||
s.logger.Error("Failed to get tender", map[string]interface{}{
|
s.logger.Error("Failed to get tender", map[string]interface{}{
|
||||||
"tender_id": tenderID,
|
"tender_id": tenderID,
|
||||||
|
|||||||
@@ -255,7 +255,8 @@ func (t *Tender) ToResponse() *TenderResponse {
|
|||||||
return t.ToResponseWithLanguage("")
|
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 {
|
func (t *Tender) ToResponseWithLanguage(language string) *TenderResponse {
|
||||||
title := t.Title
|
title := t.Title
|
||||||
description := t.Description
|
description := t.Description
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ func NewHandler(service Service, logger logger.Logger) *TenderHandler {
|
|||||||
// @Tags Admin-Tenders
|
// @Tags Admin-Tenders
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Tender ID"
|
// @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}
|
// @Success 200 {object} response.APIResponse{data=TenderResponse}
|
||||||
// @Failure 404 {object} response.APIResponse
|
// @Failure 404 {object} response.APIResponse
|
||||||
// @Failure 500 {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")
|
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 {
|
if err != nil {
|
||||||
return response.NotFound(c, "Tender not found")
|
return response.NotFound(c, "Tender not found")
|
||||||
}
|
}
|
||||||
@@ -366,6 +367,7 @@ func (h *TenderHandler) RecommendTenders(c echo.Context) error {
|
|||||||
// @Tags Tenders
|
// @Tags Tenders
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Tender ID"
|
// @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{}}
|
// @Success 200 {object} response.APIResponse{data=map[string]interface{}}
|
||||||
// @Failure 404 {object} response.APIResponse
|
// @Failure 404 {object} response.APIResponse
|
||||||
// @Failure 500 {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")
|
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 {
|
if err != nil {
|
||||||
return response.NotFound(c, "Tender not found")
|
return response.NotFound(c, "Tender not found")
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-9
@@ -45,7 +45,7 @@ type TenderDocumentDownload struct {
|
|||||||
// TenderService interface defines tender business logic operations
|
// TenderService interface defines tender business logic operations
|
||||||
type Service interface {
|
type Service interface {
|
||||||
// Tender operations
|
// 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)
|
Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, error)
|
||||||
Delete(ctx context.Context, id string) error
|
Delete(ctx context.Context, id string) error
|
||||||
Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*SearchResponse, error)
|
Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*SearchResponse, error)
|
||||||
@@ -77,7 +77,10 @@ type tenderService struct {
|
|||||||
defaultLanguage string
|
defaultLanguage string
|
||||||
}
|
}
|
||||||
|
|
||||||
const aiSummaryEnrichmentTimeout = 2 * time.Second
|
const (
|
||||||
|
aiSummaryEnrichmentTimeout = 2 * time.Second
|
||||||
|
aiTranslationEnrichmentTimeout = 2 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrTenderDocumentStorageUnavailable = errors.New("tender document storage is unavailable")
|
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
|
// GetByID retrieves a tender by ID. When language is empty, the service default (usually en)
|
||||||
func (s *tenderService) GetByID(ctx context.Context, id string) (*TenderResponse, error) {
|
// 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{}{
|
s.logger.Info("Retrieving tender by ID", map[string]interface{}{
|
||||||
"tender_id": id,
|
"tender_id": id,
|
||||||
|
"language": s.resolveDetailLanguage(language),
|
||||||
})
|
})
|
||||||
|
|
||||||
tender, err := s.repository.GetByID(ctx, id)
|
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)
|
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)
|
s.enrichWithAISummary(ctx, tender, resp)
|
||||||
|
return resp
|
||||||
return resp, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// enrichWithAISummary loads overall_summary from MinIO (PROC_<contractFolderID>/<noticePublicationID>/tender.json)
|
// 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)
|
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.
|
// persistAIOverallSummary stores the resolved summary on the tender for list/search responses.
|
||||||
func (s *tenderService) persistAIOverallSummary(ctx context.Context, t *Tender, summary string) {
|
func (s *tenderService) persistAIOverallSummary(ctx context.Context, t *Tender, summary string) {
|
||||||
summary = strings.TrimSpace(summary)
|
summary = strings.TrimSpace(summary)
|
||||||
@@ -300,7 +398,7 @@ func (s *tenderService) Update(ctx context.Context, req UpdateTenderRequest) (*T
|
|||||||
"tender_id": tender.ID,
|
"tender_id": tender.ID,
|
||||||
})
|
})
|
||||||
|
|
||||||
return tender.ToResponseWithLanguage(s.defaultLanguage), nil
|
return s.buildDetailResponse(ctx, tender, ""), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete deletes a tender
|
// Delete deletes a tender
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ func (s *tenderApprovalService) GetTenderApprovalByIDWithTender(ctx context.Cont
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get tender details
|
// Get tender details
|
||||||
tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID)
|
tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
||||||
"tender_id": tenderApproval.TenderID,
|
"tender_id": tenderApproval.TenderID,
|
||||||
@@ -205,7 +205,7 @@ func (s *tenderApprovalService) GetTenderApprovalByTenderAndCompanyWithTender(ct
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get tender details
|
// Get tender details
|
||||||
tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID)
|
tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
||||||
"tender_id": tenderApproval.TenderID,
|
"tender_id": tenderApproval.TenderID,
|
||||||
@@ -299,7 +299,7 @@ func (s *tenderApprovalService) GetTenderApprovalsByCompanyIDWithTender(ctx cont
|
|||||||
}
|
}
|
||||||
for _, approval := range tenderApprovals {
|
for _, approval := range tenderApprovals {
|
||||||
// Get tender details
|
// Get tender details
|
||||||
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID)
|
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
||||||
"tender_id": approval.TenderID,
|
"tender_id": approval.TenderID,
|
||||||
@@ -444,7 +444,7 @@ func (s *tenderApprovalService) UpdateTenderApprovalWithTender(ctx context.Conte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get tender details
|
// Get tender details
|
||||||
tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID)
|
tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
||||||
"tender_id": tenderApproval.TenderID,
|
"tender_id": tenderApproval.TenderID,
|
||||||
@@ -784,7 +784,7 @@ func (s *tenderApprovalService) GetTenderApprovalsByStatusWithTender(ctx context
|
|||||||
var responses []*TenderApprovalWithTenderResponse
|
var responses []*TenderApprovalWithTenderResponse
|
||||||
for _, approval := range tenderApprovals {
|
for _, approval := range tenderApprovals {
|
||||||
// Get tender details
|
// Get tender details
|
||||||
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID)
|
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
||||||
"tender_id": approval.TenderID,
|
"tender_id": approval.TenderID,
|
||||||
@@ -832,7 +832,7 @@ func (s *tenderApprovalService) GetTenderApprovalsBySubmissionModeWithTender(ctx
|
|||||||
var responses []*TenderApprovalWithTenderResponse
|
var responses []*TenderApprovalWithTenderResponse
|
||||||
for _, approval := range tenderApprovals {
|
for _, approval := range tenderApprovals {
|
||||||
// Get tender details
|
// Get tender details
|
||||||
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID)
|
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
||||||
"tender_id": approval.TenderID,
|
"tender_id": approval.TenderID,
|
||||||
@@ -930,7 +930,7 @@ func (s *tenderApprovalService) ListTenderApprovalsWithTender(ctx context.Contex
|
|||||||
var responses []*TenderApprovalWithTenderResponse
|
var responses []*TenderApprovalWithTenderResponse
|
||||||
for _, approval := range tenderApprovals {
|
for _, approval := range tenderApprovals {
|
||||||
// Get tender details
|
// Get tender details
|
||||||
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID)
|
tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
s.logger.Warn("Failed to get tender details", map[string]interface{}{
|
||||||
"tender_id": approval.TenderID,
|
"tender_id": approval.TenderID,
|
||||||
|
|||||||
Reference in New Issue
Block a user