299 lines
10 KiB
Go
299 lines
10 KiB
Go
package aiprompts
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
reQuotedNameSlot = regexp.MustCompile(`"([^"]+)"`)
|
|
reNameFormulaTail = regexp.MustCompile(`(?is)formuli\s*:\s*(.*?)(?:\.?\s*Ne uporabljaj|\.?$)`)
|
|
)
|
|
|
|
// TitleFormulaElement is one ordered slot in categories.title_template.
|
|
type TitleFormulaElement struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"` // "text" | "variable"
|
|
Label string `json:"label,omitempty"`
|
|
Value string `json:"value"`
|
|
Description string `json:"description,omitempty"`
|
|
Example string `json:"example,omitempty"`
|
|
}
|
|
|
|
// TitleFormula is the JSON shape stored in categories.title_template.
|
|
type TitleFormula struct {
|
|
Separator string `json:"separator"`
|
|
Elements []TitleFormulaElement `json:"elements"`
|
|
}
|
|
|
|
// DefaultRetailTitleFormula matches the dominant old A1/PHP <name> pattern:
|
|
// product type + brand + full model (never brand alone).
|
|
func DefaultRetailTitleFormula() TitleFormula {
|
|
return TitleFormula{
|
|
Separator: " ",
|
|
Elements: []TitleFormulaElement{
|
|
{ID: "0-variable-product_type", Type: "variable", Label: "Product type", Value: "product_type", Description: "Specific product type (sentence case)", Example: "Gaming monitor"},
|
|
{ID: "1-variable-brand", Type: "variable", Label: "Brand", Value: "brand", Description: "Brand with correct capitalization", Example: "Samsung"},
|
|
{ID: "2-variable-product_model", Type: "variable", Label: "Model", Value: "product_model", Description: "Full product model / ID", Example: "Odyssey G5"},
|
|
},
|
|
}
|
|
}
|
|
|
|
// TitleRoleInstruction is the shared Title-section body (CategoryEnhanceUserTemplate
|
|
// and BuildCategoryEnhanceOverlay). Drives enhance JSON "name"; never brand-only.
|
|
const TitleRoleInstruction = `Role: title. Build JSON "name": short retail title from Title formula + Attrs — never brand-only. Include product type and full model when evidence exists; follow any Title formula constraints that follow; use Attrs.`
|
|
|
|
// TitleTemplateIsBrandOnly reports stubs that only encode brand (the migrated A1
|
|
// default) so enhance collapses to brand-only names.
|
|
func TitleTemplateIsBrandOnly(template any) bool {
|
|
els, ok := titleFormulaElements(template)
|
|
if !ok || len(els) == 0 {
|
|
return false
|
|
}
|
|
vars := 0
|
|
brandOnly := true
|
|
for _, el := range els {
|
|
switch strings.ToLower(strings.TrimSpace(el.Type)) {
|
|
case "variable":
|
|
vars++
|
|
v := strings.ToLower(strings.TrimSpace(el.Value))
|
|
v = strings.ReplaceAll(v, "-", "_")
|
|
if v != "brand" && v != "znamka" {
|
|
brandOnly = false
|
|
}
|
|
case "text":
|
|
// Free-form naming-rule blobs are not brand-only stubs.
|
|
if strings.TrimSpace(el.Value) != "" {
|
|
brandOnly = false
|
|
}
|
|
}
|
|
}
|
|
return vars > 0 && brandOnly
|
|
}
|
|
|
|
// TitleTemplateNeedsRepair is true when template is missing, unparseable, or
|
|
// brand-only (insufficient vs old name/title rules).
|
|
func TitleTemplateNeedsRepair(template any) bool {
|
|
els, ok := titleFormulaElements(template)
|
|
if !ok || len(els) == 0 {
|
|
return true
|
|
}
|
|
return TitleTemplateIsBrandOnly(template)
|
|
}
|
|
|
|
// DeriveTitleFormulaFromLegacyNameRules turns PHP/A1 <name> formula text
|
|
// (e.g. "tip izdelka …", "znamka …", "poln model …") into a structured
|
|
// title_template. Falls back to DefaultRetailTitleFormula when parsing yields
|
|
// fewer than two variable slots.
|
|
func DeriveTitleFormulaFromLegacyNameRules(titleRules string) TitleFormula {
|
|
slots := extractLegacyNameSlots(titleRules)
|
|
out := TitleFormula{Separator: " ", Elements: make([]TitleFormulaElement, 0, len(slots))}
|
|
seen := map[string]struct{}{}
|
|
for _, slot := range slots {
|
|
el, ok := mapLegacyNameSlot(slot)
|
|
if !ok {
|
|
continue
|
|
}
|
|
key := el.Type + ":" + strings.ToLower(el.Value)
|
|
if _, dup := seen[key]; dup {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
el.ID = fmt.Sprintf("%d-%s-%s", len(out.Elements), el.Type, sanitizeFormulaID(el.Value))
|
|
out.Elements = append(out.Elements, el)
|
|
}
|
|
if countFormulaVariables(out) < 2 {
|
|
return ensureRetailTitleCoverage(out)
|
|
}
|
|
return ensureRetailTitleCoverage(out)
|
|
}
|
|
|
|
// DeriveTitleTemplateJSON is the JSON encoding used when repairing categories.title_template.
|
|
func DeriveTitleTemplateJSON(titleRules string) string {
|
|
f := DeriveTitleFormulaFromLegacyNameRules(titleRules)
|
|
b, err := json.Marshal(f)
|
|
if err != nil {
|
|
def := DefaultRetailTitleFormula()
|
|
b, _ = json.Marshal(def)
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func titleFormulaElements(template any) ([]TitleFormulaElement, bool) {
|
|
if template == nil {
|
|
return nil, false
|
|
}
|
|
switch t := template.(type) {
|
|
case TitleFormula:
|
|
return t.Elements, len(t.Elements) > 0
|
|
case *TitleFormula:
|
|
if t == nil {
|
|
return nil, false
|
|
}
|
|
return t.Elements, len(t.Elements) > 0
|
|
case []byte:
|
|
if len(t) == 0 {
|
|
return nil, false
|
|
}
|
|
var f TitleFormula
|
|
if err := json.Unmarshal(t, &f); err != nil {
|
|
return nil, false
|
|
}
|
|
return f.Elements, len(f.Elements) > 0
|
|
case string:
|
|
s := strings.TrimSpace(t)
|
|
if s == "" || s == "null" || s == "{}" {
|
|
return nil, false
|
|
}
|
|
var f TitleFormula
|
|
if err := json.Unmarshal([]byte(s), &f); err != nil {
|
|
return nil, false
|
|
}
|
|
return f.Elements, len(f.Elements) > 0
|
|
case map[string]any:
|
|
b, err := json.Marshal(t)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
var f TitleFormula
|
|
if err := json.Unmarshal(b, &f); err != nil {
|
|
return nil, false
|
|
}
|
|
return f.Elements, len(f.Elements) > 0
|
|
default:
|
|
b, err := json.Marshal(t)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
var f TitleFormula
|
|
if err := json.Unmarshal(b, &f); err != nil {
|
|
return nil, false
|
|
}
|
|
return f.Elements, len(f.Elements) > 0
|
|
}
|
|
}
|
|
|
|
func extractLegacyNameSlots(rules string) []string {
|
|
rules = strings.TrimSpace(rules)
|
|
if rules == "" {
|
|
return nil
|
|
}
|
|
body := rules
|
|
if m := reNameFormulaTail.FindStringSubmatch(rules); len(m) == 2 {
|
|
body = m[1]
|
|
}
|
|
raw := reQuotedNameSlot.FindAllStringSubmatch(body, -1)
|
|
out := make([]string, 0, len(raw))
|
|
for _, m := range raw {
|
|
s := strings.TrimSpace(m[1])
|
|
s = strings.Trim(s, `",. `)
|
|
if s == "" || s == "," {
|
|
continue
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func mapLegacyNameSlot(slot string) (TitleFormulaElement, bool) {
|
|
s := strings.TrimSpace(slot)
|
|
if s == "" {
|
|
return TitleFormulaElement{}, false
|
|
}
|
|
low := strings.ToLower(s)
|
|
switch {
|
|
case strings.Contains(low, "znamka"):
|
|
return TitleFormulaElement{Type: "variable", Label: "Brand", Value: "brand", Description: s, Example: "Samsung"}, true
|
|
case strings.Contains(low, "poln model"), strings.Contains(low, "model izdelka"),
|
|
strings.HasPrefix(low, "model "), low == "model", strings.Contains(low, "model uppercase"):
|
|
return TitleFormulaElement{Type: "variable", Label: "Model", Value: "product_model", Description: s, Example: "EHT6020"}, true
|
|
case strings.Contains(low, "tip izdelka"):
|
|
return TitleFormulaElement{Type: "variable", Label: "Product type", Value: "product_type", Description: s, Example: "Bluetooth speaker"}, true
|
|
case strings.HasPrefix(low, "barva"):
|
|
return TitleFormulaElement{Type: "variable", Label: "Color", Value: "barva", Description: s}, true
|
|
case strings.Contains(low, "dimenzij"):
|
|
return TitleFormulaElement{Type: "variable", Label: "Dimensions", Value: "dimensions", Description: s}, true
|
|
case strings.Contains(low, "diagonala"):
|
|
return TitleFormulaElement{Type: "variable", Label: "Diagonal", Value: "diagonala_zaslona", Description: s}, true
|
|
case strings.Contains(low, "pomnilnik"):
|
|
return TitleFormulaElement{Type: "variable", Label: "Memory", Value: "kapaciteta_ram_pomnilnika", Description: s}, true
|
|
case strings.Contains(low, "procesor"):
|
|
return TitleFormulaElement{Type: "variable", Label: "CPU", Value: "procesor", Description: s}, true
|
|
case strings.Contains(low, "grafi"):
|
|
return TitleFormulaElement{Type: "variable", Label: "GPU", Value: "graficna_kartica", Description: s}, true
|
|
case strings.Contains(low, "kapaciteta"):
|
|
return TitleFormulaElement{Type: "variable", Label: "Capacity", Value: "kapaciteta", Description: s}, true
|
|
case strings.Contains(low, "skupina po te") || strings.Contains(low, "teži") || strings.Contains(low, "tezi"):
|
|
return TitleFormulaElement{Type: "variable", Label: "Weight group", Value: "weight_group", Description: s}, true
|
|
case strings.Contains(low, "priklju"):
|
|
return TitleFormulaElement{Type: "variable", Label: "Connector", Value: "connector", Description: s}, true
|
|
case strings.Contains(low, "osvež") || strings.Contains(low, "osvez") || strings.Contains(low, "hz"):
|
|
return TitleFormulaElement{Type: "variable", Label: "Refresh rate", Value: "refresh_rate", Description: s}, true
|
|
case strings.Contains(low, "lowercase") || strings.Contains(low, "sentence case") || strings.Contains(low, "uppercase"):
|
|
// Category-specific type word ("cvrtnik lowercase", "monitor lowercase").
|
|
return TitleFormulaElement{Type: "variable", Label: "Product type", Value: "product_type", Description: s}, true
|
|
default:
|
|
// Keep unrecognized instructional slots as text so order/intent survive.
|
|
if len([]rune(s)) > 80 {
|
|
s = string([]rune(s)[:80])
|
|
}
|
|
return TitleFormulaElement{Type: "text", Label: "Naming detail", Value: s, Description: "Legacy name-formula slot"}, true
|
|
}
|
|
}
|
|
|
|
func countFormulaVariables(f TitleFormula) int {
|
|
n := 0
|
|
for _, el := range f.Elements {
|
|
if el.Type == "variable" {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func ensureRetailTitleCoverage(f TitleFormula) TitleFormula {
|
|
has := map[string]bool{}
|
|
for _, el := range f.Elements {
|
|
if el.Type != "variable" {
|
|
continue
|
|
}
|
|
k := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(el.Value), "-", "_"))
|
|
has[k] = true
|
|
}
|
|
def := DefaultRetailTitleFormula()
|
|
for _, el := range def.Elements {
|
|
k := strings.ToLower(el.Value)
|
|
if has[k] {
|
|
continue
|
|
}
|
|
// Only inject core retail slots when missing.
|
|
if k == "brand" || k == "product_model" || (k == "product_type" && countFormulaVariables(f) < 2) {
|
|
el.ID = fmt.Sprintf("%d-%s-%s", len(f.Elements), el.Type, el.Value)
|
|
f.Elements = append(f.Elements, el)
|
|
has[k] = true
|
|
}
|
|
}
|
|
if countFormulaVariables(f) < 2 {
|
|
return def
|
|
}
|
|
if f.Separator == "" {
|
|
f.Separator = " "
|
|
}
|
|
return f
|
|
}
|
|
|
|
func sanitizeFormulaID(v string) string {
|
|
v = strings.ToLower(strings.TrimSpace(v))
|
|
v = strings.ReplaceAll(v, " ", "_")
|
|
v = strings.ReplaceAll(v, "-", "_")
|
|
if v == "" {
|
|
return "slot"
|
|
}
|
|
if len(v) > 40 {
|
|
v = v[:40]
|
|
}
|
|
return v
|
|
}
|