Integrated AI translation

This commit is contained in:
Mazyar
2026-05-08 16:17:33 +03:30
parent 65a19d1514
commit b480ed2371
9 changed files with 357 additions and 24 deletions
+120 -5
View File
@@ -16,6 +16,7 @@ import (
// AISummarizerClient defines the interface for on-demand AI summarization
type AISummarizerClient interface {
FetchSummaryOnDemand(ctx context.Context, reqBody ai_summarizer.SummarizeRequest) (*ai_summarizer.SummarizeResponse, error)
FetchTranslationOnDemand(ctx context.Context, reqBody ai_summarizer.TranslateRequest) (*ai_summarizer.TranslateResponse, error)
}
// AISummarizerStorage defines the interface for fetching summaries from MinIO storage
@@ -36,6 +37,7 @@ type Service interface {
// AI summarization operations
GetAISummary(ctx context.Context, id string) (*AISummaryResponse, error)
TriggerAISummarize(ctx context.Context, id string) (*AISummarizeResponse, error)
TriggerAITranslate(ctx context.Context, id, language string) (*AITranslateResponse, error)
GetAllAISummaries(ctx context.Context) ([]ai_summarizer.TenderSummaryItem, error)
// Maintenance operations
@@ -51,10 +53,15 @@ type tenderService struct {
logger logger.Logger
aiSummarizerClient AISummarizerClient
aiSummarizerStorage AISummarizerStorage
defaultLanguage string
}
// NewService creates a new tender service
func NewService(repository TenderRepository, companyService company.Service, customerService customer.Service, logger logger.Logger, aiClient AISummarizerClient, aiStorage AISummarizerStorage) Service {
func NewService(repository TenderRepository, companyService company.Service, customerService customer.Service, logger logger.Logger, aiClient AISummarizerClient, aiStorage AISummarizerStorage, defaultLanguage string) Service {
if strings.TrimSpace(defaultLanguage) == "" {
defaultLanguage = "en"
}
return &tenderService{
repository: repository,
companyService: companyService,
@@ -62,6 +69,7 @@ func NewService(repository TenderRepository, companyService company.Service, cus
logger: logger,
aiSummarizerClient: aiClient,
aiSummarizerStorage: aiStorage,
defaultLanguage: strings.ToLower(defaultLanguage),
}
}
@@ -81,6 +89,11 @@ func (s *tenderService) GetByID(ctx context.Context, id string) (*TenderResponse
}
resp := tender.ToResponse()
if tr, ok := tender.Translations[s.defaultLanguage]; ok {
resp.Title = tr.Title
resp.Description = tr.Description
resp.Language = s.defaultLanguage
}
// Enrich with AI summary from MinIO storage (best-effort, non-blocking)
s.enrichWithAISummary(ctx, resp)
@@ -126,11 +139,14 @@ func (s *tenderService) Update(ctx context.Context, req UpdateTenderRequest) (*T
}
// Update fields if provided
translationSourceChanged := false
if req.Title != nil {
tender.Title = *req.Title
translationSourceChanged = true
}
if req.Description != nil {
tender.Description = *req.Description
translationSourceChanged = true
}
if req.Status != nil {
tender.Status = *req.Status
@@ -144,6 +160,11 @@ func (s *tenderService) Update(ctx context.Context, req UpdateTenderRequest) (*T
if req.TenderDeadline != nil {
tender.TenderDeadline = *req.TenderDeadline
}
if translationSourceChanged {
tender.Translations = nil
tender.ProcessingMetadata.TranslatedData = nil
tender.ProcessingMetadata.TranslatedAt = 0
}
if err := s.repository.Update(ctx, tender); err != nil {
s.logger.Error("Failed to update tender", map[string]interface{}{
@@ -264,7 +285,7 @@ func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination
if form.CompanyID == nil || *form.CompanyID == "" {
tenderResponses := make([]TenderResponse, 0, len(tenders))
for _, tender := range tenders {
tenderResponses = append(tenderResponses, *tender.ToResponse())
tenderResponses = append(tenderResponses, *tender.ToResponseWithLanguage(s.pickResponseLanguage(form.Language)))
}
return &SearchResponse{
@@ -283,7 +304,7 @@ func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination
// Continue without match percentage if company not found
tenderResponses := make([]TenderResponse, 0, len(tenders))
for _, tender := range tenders {
tenderResponses = append(tenderResponses, *tender.ToResponse())
tenderResponses = append(tenderResponses, *tender.ToResponseWithLanguage(s.pickResponseLanguage(form.Language)))
}
return &SearchResponse{
@@ -306,7 +327,7 @@ func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination
tenderResponses := make([]TenderResponse, 0, len(tenders))
for _, tender := range tenders {
tenderResponses = append(tenderResponses, *tender.ToResponse())
tenderResponses = append(tenderResponses, *tender.ToResponseWithLanguage(s.pickResponseLanguage(form.Language)))
}
return &SearchResponse{
@@ -373,7 +394,7 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat
tenderResponses := make([]TenderResponse, 0, len(tenders))
for _, tender := range tenders {
tenderResponses = append(tenderResponses, *tender.ToResponse())
tenderResponses = append(tenderResponses, *tender.ToResponseWithLanguage(s.pickResponseLanguage(form.Language)))
}
return &SearchResponse{
@@ -662,3 +683,97 @@ func (s *tenderService) GetAllAISummaries(ctx context.Context) ([]ai_summarizer.
return results, nil
}
// TriggerAITranslate triggers on-demand AI translation for a tender and stores
// translated result per language in MongoDB for future reuse.
func (s *tenderService) TriggerAITranslate(ctx context.Context, id, language string) (*AITranslateResponse, error) {
if id == "" {
return nil, fmt.Errorf("tender ID cannot be empty")
}
if s.aiSummarizerClient == nil {
return nil, fmt.Errorf("AI summarizer service is not configured")
}
targetLanguage := strings.ToLower(strings.TrimSpace(language))
if targetLanguage == "" {
targetLanguage = s.defaultLanguage
}
t, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get tender for AI translation", map[string]interface{}{
"tender_id": id,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to get tender: %w", err)
}
if t == nil {
return nil, fmt.Errorf("tender not found")
}
if t.NoticePublicationID == "" {
return nil, fmt.Errorf("tender has no notice publication ID")
}
if tr, ok := t.Translations[targetLanguage]; ok {
return &AITranslateResponse{
NoticeID: t.NoticePublicationID,
Language: targetLanguage,
TranslatedTitle: tr.Title,
TranslatedDescription: tr.Description,
}, nil
}
resp, err := s.aiSummarizerClient.FetchTranslationOnDemand(ctx, ai_summarizer.TranslateRequest{
NoticeID: t.NoticePublicationID,
Title: t.Title,
Description: t.Description,
Language: targetLanguage,
})
if err != nil {
s.logger.Error("On-demand AI translation failed", map[string]interface{}{
"tender_id": id,
"notice_id": t.NoticePublicationID,
"language": targetLanguage,
"error": err.Error(),
})
return nil, fmt.Errorf("AI translation request failed: %w", err)
}
if t.Translations == nil {
t.Translations = make(map[string]TranslationEntry)
}
now := time.Now().Unix()
t.Translations[targetLanguage] = TranslationEntry{
Title: resp.TranslatedTitle,
Description: resp.TranslatedDescription,
Language: targetLanguage,
TranslatedAt: now,
}
if t.ProcessingMetadata.TranslatedData == nil {
t.ProcessingMetadata.TranslatedData = make(map[string]string)
}
t.ProcessingMetadata.TranslatedData[targetLanguage] = "done"
t.ProcessingMetadata.TranslatedAt = now
if err := s.repository.Update(ctx, t); err != nil {
return nil, fmt.Errorf("failed to persist translation: %w", err)
}
return &AITranslateResponse{
NoticeID: resp.NoticeID,
Language: targetLanguage,
TranslatedTitle: resp.TranslatedTitle,
TranslatedDescription: resp.TranslatedDescription,
}, nil
}
func (s *tenderService) pickResponseLanguage(language *string) string {
if language != nil {
clean := strings.ToLower(strings.TrimSpace(*language))
if clean != "" {
return clean
}
}
return s.defaultLanguage
}