Integrated AI translation
This commit is contained in:
+120
-5
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user