package processing import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "log" "strings" "time" ) // enhanceLogHeadTail caps each end of prompt/response bodies in worker logs. const enhanceLogHeadTail = 500 func emptyLogDash(s string) string { s = strings.TrimSpace(s) if s == "" { return "-" } return s } // truncateLogEnds returns a PII-safe preview: total rune length plus first/last // slices when the body is large (avoids megabyte log lines). func truncateLogEnds(s string, head, tail int) string { s = strings.TrimSpace(s) if s == "" { return "len=0" } runes := []rune(s) n := len(runes) if head < 0 { head = 0 } if tail < 0 { tail = 0 } if n <= head+tail+32 { return fmt.Sprintf("len=%d body=%q", n, s) } return fmt.Sprintf("len=%d head=%q tail=%q", n, string(runes[:head]), string(runes[n-tail:])) } // formulaTemplateSummary summarizes a category title/description formula for logs // (section/element types + JSON byte length + short content hash — not a full dump). func formulaTemplateSummary(template any, kind string) string { if template == nil { return "none" } raw, err := json.Marshal(template) if err != nil || len(raw) == 0 || string(raw) == "null" || string(raw) == "{}" { return "none" } sum := sha256.Sum256(raw) hash8 := hex.EncodeToString(sum[:4]) switch kind { case "title": els, sep, ok := parseTitleFormula(template) if !ok { return fmt.Sprintf("unparsed len=%d hash=%s", len(raw), hash8) } types := make([]string, 0, len(els)) for _, el := range els { types = append(types, el.Type) } return fmt.Sprintf("elements=%s sep=%q len=%d hash=%s", strings.Join(types, "+"), sep, len(raw), hash8) case "description": sections, ok := parseDescriptionFormulaSections(template) if !ok { return fmt.Sprintf("unparsed len=%d hash=%s", len(raw), hash8) } types := make([]string, 0, len(sections)) for _, sec := range sections { if t := strings.TrimSpace(sec.Type); t != "" { types = append(types, t) } } return fmt.Sprintf("sections=%s len=%d hash=%s", strings.Join(types, "+"), len(raw), hash8) default: return fmt.Sprintf("len=%d hash=%s", len(raw), hash8) } } func finishReasonFromCompletion(comp Completion) string { m, ok := comp.Raw.(map[string]any) if !ok || m == nil { return "" } fr, _ := m["finish_reason"].(string) return strings.TrimSpace(fr) } func enhanceFormulaOverride(in ProductInput) bool { return FormatDescriptionFormulaConstraint(in.DescriptionTemplate) != "" || FormatTitleFormulaConstraint(in.TitleTemplate) != "" } // logEnhancePrompt emits one structured line before the LLM call (truncated prompts). func logEnhancePrompt(in ProductInput, categoryUID, categoryName, system, user string) { log.Printf("processing: ai_enhance prompt job=%s company=%s raw=%s category_uid=%q category_name=%q title_formula=%s desc_formula=%s formula_override=%t system=%s user=%s", emptyLogDash(in.JobID), emptyLogDash(in.CompanyID), emptyLogDash(in.RawProductID), strings.TrimSpace(categoryUID), strings.TrimSpace(categoryName), formulaTemplateSummary(in.TitleTemplate, "title"), formulaTemplateSummary(in.DescriptionTemplate, "description"), enhanceFormulaOverride(in), truncateLogEnds(system, enhanceLogHeadTail, enhanceLogHeadTail), truncateLogEnds(user, enhanceLogHeadTail, enhanceLogHeadTail), ) } // logEnhanceOutcome emits one structured line for the enhance result (truncated response). func logEnhanceOutcome(in ProductInput, categoryUID, categoryName, outcome, reason string, comp Completion, elapsed time.Duration, forceReason string) { log.Printf("processing: ai_enhance outcome=%s reason=%s job=%s company=%s raw=%s category_uid=%q category_name=%q title_formula=%s desc_formula=%s formula_override=%t finish_reason=%s prompt_tokens=%d completion_tokens=%d total_tokens=%d elapsed=%s response=%s force_reason=%s", emptyLogDash(outcome), emptyLogDash(reason), emptyLogDash(in.JobID), emptyLogDash(in.CompanyID), emptyLogDash(in.RawProductID), strings.TrimSpace(categoryUID), strings.TrimSpace(categoryName), formulaTemplateSummary(in.TitleTemplate, "title"), formulaTemplateSummary(in.DescriptionTemplate, "description"), enhanceFormulaOverride(in), emptyLogDash(finishReasonFromCompletion(comp)), comp.PromptTokens, comp.OutputTokens, comp.TotalTokens, elapsed.Round(time.Millisecond), truncateLogEnds(comp.Text, enhanceLogHeadTail, enhanceLogHeadTail), emptyLogDash(forceReason), ) }