package workers import ( "context" "fmt" "tm/internal/notice" "tm/internal/tender" "tm/pkg/logger" "tm/pkg/mongo" "tm/pkg/notification" "tm/pkg/ollama" ) type NoticeWorker struct { Mongo *mongo.ConnectionManager Logger logger.Logger Notify notification.SDK Ollama *ollama.SDK NoticeRepo notice.Repository TenderRepo tender.TenderRepository } func NewNoticeWorker(mongo *mongo.ConnectionManager, logger logger.Logger, notify notification.SDK, noticeRepo notice.Repository, tenderRepo tender.TenderRepository, ollama *ollama.SDK) *NoticeWorker { return &NoticeWorker{ Mongo: mongo, Logger: logger, Notify: notify, Ollama: ollama, NoticeRepo: noticeRepo, TenderRepo: tenderRepo, } } func (w *NoticeWorker) Run() { w.Logger.Info("Notice worker started", map[string]interface{}{}) limit := 10 skip := 0 for { notices, _, err := w.NoticeRepo.GetUnProcessedNotices(context.Background(), limit, skip) if err != nil { w.Logger.Error("Failed to get notices", map[string]interface{}{ "error": err.Error(), }) continue } if len(notices) == 0 { break } for _, n := range notices { w.Logger.Info("Notice", map[string]interface{}{ "notice": n.ID.Hex(), }) // exists, err := w.TenderRepo.GetByNoticePublicationID(context.Background(), n.NoticePublicationID) // if err != nil { // w.Logger.Error("Failed to get tender", map[string]interface{}{ // "error": err.Error(), // }) // continue // } t, err := w.ToTender(&n) if err != nil { w.Logger.Error("Failed to create tender", map[string]interface{}{ "error": err.Error(), }) continue } err = w.TenderRepo.Create(context.Background(), t) if err != nil { w.Logger.Error("Failed to create tender", map[string]interface{}{ "error": err.Error(), }) } } skip += limit } } func (w *NoticeWorker) translateAI(msg string) (string, error) { builder := w.Ollama.NewChatBuilder().SetStream(false).SetMaxTokens(200000) chat, err := builder.AddMessage(ollama.MessageRoleUser, fmt.Sprintf("Translate the following text into fluent, natural English while keeping the original meaning and tone. Do not summarize, shorten, or add extra explanations. Just provide the translation without other additional values. %s", msg)).Build() if err != nil { return "", err } resp, err := w.Ollama.Chat(context.Background(), chat) if err != nil { return "", err } return resp.Message.Content, nil } func (w *NoticeWorker) ToTender(n *notice.Notice) (*tender.Tender, error) { // Buyer Organization buyerOrg := &tender.Organization{} if n.BuyerOrganization != nil { buyerOrg = &tender.Organization{ Name: n.BuyerOrganization.Name, CompanyID: n.BuyerOrganization.CompanyID, WebsiteURI: n.BuyerOrganization.WebsiteURI, ContactName: n.BuyerOrganization.ContactName, ContactTelephone: n.BuyerOrganization.ContactTelephone, ContactEmail: n.BuyerOrganization.ContactEmail, ContactFax: n.BuyerOrganization.ContactFax, Role: n.BuyerOrganization.Role, Address: tender.Address{ StreetName: n.BuyerOrganization.Address.StreetName, CityName: n.BuyerOrganization.Address.CityName, PostalZone: n.BuyerOrganization.Address.PostalZone, CountrySubentityCode: n.BuyerOrganization.Address.CountrySubentityCode, Department: n.BuyerOrganization.Address.Department, Region: n.BuyerOrganization.Address.Region, CountryCode: n.BuyerOrganization.Address.CountryCode, }, } } // Review Organization reviewOrg := &tender.Organization{} if n.ReviewOrganization != nil { reviewOrg = &tender.Organization{ Name: n.ReviewOrganization.Name, CompanyID: n.ReviewOrganization.CompanyID, WebsiteURI: n.ReviewOrganization.WebsiteURI, ContactName: n.ReviewOrganization.ContactName, ContactTelephone: n.ReviewOrganization.ContactTelephone, ContactEmail: n.ReviewOrganization.ContactEmail, ContactFax: n.ReviewOrganization.ContactFax, Role: n.ReviewOrganization.Role, Address: tender.Address{ StreetName: n.ReviewOrganization.Address.StreetName, CityName: n.ReviewOrganization.Address.CityName, PostalZone: n.ReviewOrganization.Address.PostalZone, CountrySubentityCode: n.ReviewOrganization.Address.CountrySubentityCode, Department: n.ReviewOrganization.Address.Department, Region: n.ReviewOrganization.Address.Region, CountryCode: n.ReviewOrganization.Address.CountryCode, }, } } // Organizations organizations := make([]tender.Organization, len(n.Organizations)) for i, org := range n.Organizations { organizations[i] = tender.Organization{ Name: org.Name, CompanyID: org.CompanyID, WebsiteURI: org.WebsiteURI, ContactName: org.ContactName, ContactTelephone: org.ContactTelephone, ContactEmail: org.ContactEmail, ContactFax: org.ContactFax, Role: org.Role, Address: tender.Address{ StreetName: org.Address.StreetName, CityName: org.Address.CityName, PostalZone: org.Address.PostalZone, CountrySubentityCode: org.Address.CountrySubentityCode, Department: org.Address.Department, Region: org.Address.Region, CountryCode: org.Address.CountryCode, }, } } // Selection Criteria selectionCriteria := make([]tender.SelectionCriterion, len(n.SelectionCriteria)) for i, criterion := range n.SelectionCriteria { selectionCriteria[i] = tender.SelectionCriterion{ TypeCode: criterion.TypeCode, Description: criterion.Description, LanguageID: criterion.LanguageID, } } // Winning Tenderer winningTenderer := &tender.Organization{} if n.WinningTenderer != nil { winningTenderer = &tender.Organization{ Name: n.WinningTenderer.Name, CompanyID: n.WinningTenderer.CompanyID, WebsiteURI: n.WinningTenderer.WebsiteURI, ContactName: n.WinningTenderer.ContactName, ContactTelephone: n.WinningTenderer.ContactTelephone, ContactEmail: n.WinningTenderer.ContactEmail, ContactFax: n.WinningTenderer.ContactFax, Role: n.WinningTenderer.Role, Address: tender.Address{ StreetName: n.WinningTenderer.Address.StreetName, CityName: n.WinningTenderer.Address.CityName, PostalZone: n.WinningTenderer.Address.PostalZone, CountrySubentityCode: n.WinningTenderer.Address.CountrySubentityCode, Department: n.WinningTenderer.Address.Department, Region: n.WinningTenderer.Address.Region, CountryCode: n.WinningTenderer.Address.CountryCode, }, } } // Modifications modifications := make([]tender.TenderModification, len(n.Modifications)) for i, modification := range n.Modifications { modifications[i] = tender.TenderModification{ ModificationDate: modification.ModificationDate, ModificationReason: modification.ModificationReason, Description: modification.Description, LanguageID: modification.LanguageID, } } // Awarded Entities awardedEntities := make([]tender.Awarded, len(n.AwardedEntities)) for i, awarded := range n.AwardedEntities { awardedEntities[i] = tender.Awarded{ Name: awarded.Name, Address: awarded.Address, Country: awarded.Country, Amount: awarded.Amount, Currency: awarded.Currency, Share: awarded.Share, AwardDate: awarded.AwardDate, ContractID: awarded.ContractID, TenderID: awarded.TenderID, LotID: awarded.LotID, CompanyID: awarded.CompanyID, OrganizationID: awarded.OrganizationID, } } title, err := w.translateAI(n.Title) if err != nil { return nil, fmt.Errorf("failed to translate title: %w", err) } description, err := w.translateAI(n.Description) if err != nil { return nil, fmt.Errorf("failed to translate description: %w", err) } return &tender.Tender{ NoticePublicationID: n.NoticePublicationID, Title: title, Description: description, ProcurementTypeCode: n.ProcurementTypeCode, ProcedureCode: n.ProcedureCode, EstimatedValue: n.EstimatedValue, Currency: n.Currency, TenderDeadline: n.TenderDeadline, SubmissionURL: n.SubmissionURL, BuyerOrganization: buyerOrg, ReviewOrganization: reviewOrg, Organizations: organizations, SelectionCriteria: selectionCriteria, Status: tender.TenderStatus(n.Status), Source: tender.TenderSource(n.Source), SourceFileURL: n.SourceFileURL, SourceFileName: n.SourceFileName, TenderID: n.TenderID, ContractNoticeID: n.ContractNoticeID, ContractFolderID: n.ContractFolderID, NoticeTypeCode: n.NoticeTypeCode, NoticeSubTypeCode: n.NoticeSubTypeCode, NoticeLanguageCode: n.NoticeLanguageCode, IssueDate: n.IssueDate, IssueTime: n.IssueTime, PublicationDate: n.PublicationDate, SubmissionDeadline: n.SubmissionDeadline, ApplicationDeadline: n.ApplicationDeadline, GazetteID: n.GazetteID, Duration: n.Duration, DurationUnit: n.DurationUnit, PlaceOfPerformance: n.PlaceOfPerformance, CountryCode: n.CountryCode, RegionCode: n.RegionCode, CityName: n.CityName, PostalCode: n.PostalCode, DocumentURI: n.DocumentURI, TenderURL: n.TenderURL, MainClassification: n.MainClassification, AdditionalClassifications: n.AdditionalClassifications, OfficialLanguages: n.OfficialLanguages, CancellationReason: n.CancellationReason, CancellationDate: n.CancellationDate, AwardDate: n.AwardDate, AwardedValue: n.AwardedValue, ContractNumber: n.ContractNumber, SuspensionReason: n.SuspensionReason, SuspensionDate: n.SuspensionDate, WinningTenderer: winningTenderer, Modifications: modifications, AwardedEntities: awardedEntities, }, nil }