package productDescriptionService import ( "context" "encoding/xml" "fmt" "io" "slices" "strings" "git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/service/langsService" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "gorm.io/gorm" "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/option" "github.com/openai/openai-go/v3/responses" ) type ProductDescriptionService struct { db *gorm.DB client openai.Client } func New() *ProductDescriptionService { return &ProductDescriptionService{ db: db.Get(), client: openai.NewClient(option.WithAPIKey("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")), } } func isValidXHTML(s string) bool { decoder := xml.NewDecoder(strings.NewReader(s)) hasStartTag := false for { tok, err := decoder.Token() if err != nil { if err == io.EOF { return hasStartTag } return false } if _, ok := tok.(xml.StartElement); ok { hasStartTag = true } } } // We assume that any user has access to all product descriptions func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) { var ProductDescription model.ProductDescription err := s.db. Table("ps_product_lang"). Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID). First(&ProductDescription).Error if err != nil { return nil, fmt.Errorf("database error: %w", err) } return &ProductDescription, nil } // Updates relevant fields with the "updates" map func (s *ProductDescriptionService) SaveProductDescription(userID uint, productID uint, productShopID uint, productLangID uint, updates map[string]string) error { // only some fields can be affected allowedFields := []string{"description", "description_short", "meta_description", "meta_title", "name", "available_now", "available_later", "usage"} for key := range updates { if !slices.Contains(allowedFields, key) { return responseErrors.ErrBadField } } // check that fields description, description_short and usage, if they exist, have a valid html format mustBeHTML := []string{"description", "description_short", "usage"} for i := 0; i < len(mustBeHTML); i++ { if text, exists := updates[mustBeHTML[i]]; exists { if !isValidXHTML(text) { return responseErrors.ErrInvalidHTML } } } record := model.ProductDescription{ ProductID: productID, ShopID: productShopID, LangID: productLangID, } err := s.db. Table("ps_product_lang"). Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID). FirstOrCreate(&record).Error if err != nil { return fmt.Errorf("database error: %w", err) } if len(updates) == 0 { return nil } updatesIface := make(map[string]interface{}, len(updates)) for k, v := range updates { updatesIface[k] = v } err = s.db. Table("ps_product_lang"). Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID). Updates(updatesIface).Error if err != nil { return fmt.Errorf("database error: %w", err) } return nil } // Updates relevant fields with the "updates" map func (s *ProductDescriptionService) TranslateProductDescription(userID uint, productID uint, productShopID uint, productFromLangID uint, productToLangID uint) (*model.ProductDescription, error) { var ProductDescription model.ProductDescription err := s.db. Table("ps_product_lang"). Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productFromLangID). First(&ProductDescription).Error if err != nil { return nil, fmt.Errorf("database error: %w", err) } // we translate all changeable fields, and we keep the exact same HTML structure in relevant fields. lang, err := langsService.LangSrv.GetLanguageById(productToLangID) if err != nil { return nil, err } request := "Translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website.\n\n" request += "\n" request += "" request += ProductDescription.Description request += "" request += "\n" request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website." request += "\n" request += "" request += ProductDescription.DescriptionShort request += "" request += "\n" request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website." request += "\n" request += "" request += ProductDescription.MetaDescription request += "" request += "\n" request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website." request += "\n" request += "" request += ProductDescription.MetaTitle request += "" request += "\n" request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website." request += "\n" request += "" request += ProductDescription.Name request += "" request += "\n" request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website." request += "\n" request += "" request += ProductDescription.AvailableNow request += "" request += "\n" request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website." request += "\n" request += "" request += ProductDescription.AvailableLater request += "" request += "\n" request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website." request += "\n" request += "" request += ProductDescription.Usage request += "" openai_response, err := s.client.Responses.New(context.Background(), responses.ResponseNewParams{ Input: responses.ResponseNewParamsInputUnion{OfString: openai.String(request)}, Model: openai.ChatModelGPT4_1Mini, // Model: openai.ChatModelGPT4_1Nano, }) if openai_response.Status != "completed" { return nil, responseErrors.ErrOpenAIResponseFail } output := openai_response.OutputText() // for debugging purposes // fi, err := os.ReadFile("/home/daniel/coding/work/b2b/app/service/productDescriptionService/test.txt") // just pass the file name // output := string(fi) success, match := GetStringInBetween(output, "", "") if !success { return nil, responseErrors.ErrOpenAIBadOutput } ProductDescription.Description = match success, match = GetStringInBetween(output, "", "") if !success { return nil, responseErrors.ErrOpenAIBadOutput } ProductDescription.DescriptionShort = match success, match = GetStringInBetween(output, "", "") if !success { return nil, responseErrors.ErrOpenAIBadOutput } ProductDescription.MetaDescription = match success, match = GetStringInBetween(output, "", "") if !success { return nil, responseErrors.ErrOpenAIBadOutput } ProductDescription.MetaTitle = match success, match = GetStringInBetween(output, "", "") if !success { return nil, responseErrors.ErrOpenAIBadOutput } ProductDescription.Name = match success, match = GetStringInBetween(output, "", "") if !success { return nil, responseErrors.ErrOpenAIBadOutput } ProductDescription.AvailableNow = match success, match = GetStringInBetween(output, "", "") if !success { return nil, responseErrors.ErrOpenAIBadOutput } ProductDescription.AvailableLater = match success, match = GetStringInBetween(output, "", "") if !success { return nil, responseErrors.ErrOpenAIBadOutput } ProductDescription.Usage = match return &ProductDescription, nil } // GetStringInBetween returns empty string if no start or end string found func GetStringInBetween(str string, start string, end string) (success bool, result string) { s := strings.Index(str, start) if s == -1 { return false, "" } s += len(start) e := strings.Index(str[s:], end) if e == -1 { return false, "" } return true, str[s : s+e] }