package main import ( "encoding/json" "fmt" "os" "sort" "strings" "git.iwork-ai.com/xdc/iqudo-top1/internal/scriptkit" "github.com/xuri/excelize/v2" ) // Data sources for the script package. const petScriptSource = "症状-疾病-话术.json" const petQuestionSource = "症状确认话术.json" const petSmallCategorySource = "症状小类-大类.json" const petSmallAskScriptSource = "症状小类询问话术.json" const petPreGuideSource = "症状确认前引导话术.json" const petClassificationSource = "症状分类.json" const petKnowledgeSource = "症状-疾病-方案.xlsx" type petScriptEntry struct { Title string `json:"title"` Template string `json:"template"` } type petClassification struct { Symptoms []struct { Type string `json:"type"` Subs []string `json:"subs"` } `json:"symptoms"` Diagnosis []struct { Type string `json:"type"` Subs []struct { SubType string `json:"subType"` } `json:"subs"` } `json:"diagnosis"` } type petCombination struct { Name string `json:"name"` Weight int `json:"weight"` Products []struct { SKUCode string `json:"sku_code"` ProductName string `json:"product_name"` } `json:"products"` Script string `json:"script"` } type petScriptDisease struct { Combinations []petCombination `json:"combinations"` } type petKnowledgeRow struct { Symptom string Disease string SKUCode string ProductName string } // petQuestionBank is the customer-facing copy bank for the script package. // Every question lives in the JSON data file, not in Go code. type petQuestionBank struct { OpeningGreeting string `json:"opening_greeting"` DiagnosisFallback string `json:"diagnosis_fallback"` RecommendFallback string `json:"recommend_fallback"` SymptomScreeningTemplate string `json:"symptom_screening_template"` SymptomConfirmOptions struct { Yes string `json:"yes"` No string `json:"no"` } `json:"symptom_confirm_options"` DiseaseConfirmOptions struct { Yes string `json:"yes"` No string `json:"no"` } `json:"disease_confirm_options"` Symptoms map[string]petSymptomQuestions `json:"symptoms"` DiseaseConfirmTemplate string `json:"disease_confirm_template"` DiseaseConfirmedCopyTemplate string `json:"disease_confirmed_copy_template"` DiseasePlanIntro string `json:"disease_plan_intro"` DiseaseCareNote string `json:"disease_care_note"` Diseases map[string]petDiseasePhrase `json:"diseases"` } type petSymptomQuestions struct { Confirm string `json:"confirm"` Short string `json:"short"` Choices []petChoiceQuestion `json:"choices"` } type petChoiceQuestion struct { Title string `json:"title"` Question string `json:"question"` Multiple bool `json:"multiple"` Options []petChoiceOption `json:"options"` } type petChoiceOption struct { Key string `json:"key"` Label string `json:"label"` } type petDiseasePhrase struct { Phrase string `json:"phrase"` } // scriptKeyGenerator produces stable ASCII keys for dimension values and // scripts. Chinese display names live in the Name field. type scriptKeyGenerator struct { counters map[string]int } func (g *scriptKeyGenerator) key(prefix string) string { g.counters[prefix]++ return fmt.Sprintf("%s_%03d", prefix, g.counters[prefix]) } // buildScriptPackage builds the 问诊问药 script package from the docs files. func buildScriptPackage(scriptFile, knowledgeFile, smallCategoryFile, smallAskScriptFile, preGuideFile, classificationFile, questionFile string) (scriptkit.PackageInput, error) { bank, err := readQuestionBank(questionFile) if err != nil { return scriptkit.PackageInput{}, err } classification, err := readClassification(classificationFile) if err != nil { return scriptkit.PackageInput{}, err } smallToBig, err := readStringMap(smallCategoryFile) if err != nil { return scriptkit.PackageInput{}, err } smallAsk, err := readPetScriptMap(smallAskScriptFile) if err != nil { return scriptkit.PackageInput{}, err } preGuide, err := readPreGuide(preGuideFile) if err != nil { return scriptkit.PackageInput{}, err } combinations, combinationSymptoms, err := readCombinations(scriptFile) if err != nil { return scriptkit.PackageInput{}, err } rows, err := readPetKnowledge(knowledgeFile) if err != nil { return scriptkit.PackageInput{}, err } keys := &scriptKeyGenerator{counters: map[string]int{}} input := scriptkit.PackageInput{ Name: "宠物问诊问药话术包", StartStageKey: "opening", Dimensions: []scriptkit.DimensionInput{}, Stages: []scriptkit.StageInput{}, Linkages: []scriptkit.LinkageInput{}, Adapters: []scriptkit.AdapterInput{}, } // 症状大类来自小类映射、诊断分组、组合话术和 SKU 表的并集。 bigSet := map[string]bool{} for _, big := range smallToBig { if big != "" { bigSet[big] = true } } for _, diagnosis := range classification.Diagnosis { if diagnosis.Type != "" { bigSet[diagnosis.Type] = true } } for big := range combinationSymptoms { bigSet[big] = true } for _, row := range rows { if row.Symptom != "" { bigSet[row.Symptom] = true } } bigNames := make([]string, 0, len(bigSet)) for name := range bigSet { bigNames = append(bigNames, name) } sort.Strings(bigNames) bigKey := map[string]string{} bigDimension := scriptkit.DimensionInput{DimKey: "symptom", Name: "症状", SortOrder: 1, Values: []scriptkit.ValueInput{}} for _, name := range bigNames { key := keys.key("symptom") bigKey[name] = key bigDimension.Values = append(bigDimension.Values, scriptkit.ValueInput{ValueKey: key, Name: name, InitialWeight: 0, SortOrder: len(bigDimension.Values)}) } input.Dimensions = append(input.Dimensions, bigDimension) subSet := map[string]bool{} for sub := range smallToBig { if sub != "" { subSet[sub] = true } } for _, symptom := range classification.Symptoms { for _, sub := range symptom.Subs { if sub != "" { subSet[sub] = true } } } subNames := make([]string, 0, len(subSet)) for name := range subSet { subNames = append(subNames, name) } sort.Strings(subNames) subKey := map[string]string{} subDimension := scriptkit.DimensionInput{DimKey: "symptom_sub", Name: "症状小类", SortOrder: 2, Values: []scriptkit.ValueInput{}} for _, name := range subNames { key := keys.key("symptom_sub") subKey[name] = key subDimension.Values = append(subDimension.Values, scriptkit.ValueInput{ValueKey: key, Name: name, InitialWeight: 0, SortOrder: len(subDimension.Values)}) } input.Dimensions = append(input.Dimensions, subDimension) diseaseNames := make([]string, 0) diseaseKey := map[string]string{} for _, diagnosis := range classification.Diagnosis { for _, sub := range diagnosis.Subs { if sub.SubType == "" { continue } diseaseNames = append(diseaseNames, sub.SubType) } } sort.Strings(diseaseNames) diseaseDimension := scriptkit.DimensionInput{DimKey: "disease", Name: "疾病分型", SortOrder: 3, Values: []scriptkit.ValueInput{}} for _, name := range diseaseNames { key := keys.key("disease") diseaseKey[name] = key diseaseDimension.Values = append(diseaseDimension.Values, scriptkit.ValueInput{ValueKey: key, Name: name, InitialWeight: 0, SortOrder: len(diseaseDimension.Values)}) } input.Dimensions = append(input.Dimensions, diseaseDimension) // 每个症状都必须配置确认题和选择题;每个疾病都必须配置口语化说法。 var missingSymptoms, missingDiseases []string for _, name := range bigNames { questions, ok := bank.Symptoms[name] if !ok || strings.TrimSpace(questions.Confirm) == "" || len(questions.Choices) == 0 { missingSymptoms = append(missingSymptoms, name) } } for _, name := range diseaseNames { if _, ok := bank.Diseases[name]; !ok { missingDiseases = append(missingDiseases, name) } } if len(missingSymptoms) > 0 { return scriptkit.PackageInput{}, fmt.Errorf("话术库 %s 缺少症状确认题/选择题: %v", questionFile, missingSymptoms) } if len(missingDiseases) > 0 { return scriptkit.PackageInput{}, fmt.Errorf("话术库 %s 缺少疾病口语化说法: %v", questionFile, missingDiseases) } // Linkages: 小类 -> 大类, 大类 -> 疾病. 激活阈值 5:只有已确认的症状 // (权重达到 5 以上)才会把疾病带入拟诊阶段;模糊提示(权重 3)只触发 // 症状确认题,不会直接带出疾病。 linkages := make([]scriptkit.LinkageInput, 0) for sub, big := range smallToBig { if subKey[sub] == "" || bigKey[big] == "" { continue } linkages = append(linkages, scriptkit.LinkageInput{FromDimensionValueKey: subKey[sub], ToDimensionValueKey: bigKey[big], RelationType: "contributes_to", Contribution: 8, ActivationThreshold: 5, SortOrder: len(linkages)}) } for _, diagnosis := range classification.Diagnosis { for _, sub := range diagnosis.Subs { if bigKey[diagnosis.Type] == "" || diseaseKey[sub.SubType] == "" { continue } linkages = append(linkages, scriptkit.LinkageInput{FromDimensionValueKey: bigKey[diagnosis.Type], ToDimensionValueKey: diseaseKey[sub.SubType], RelationType: "contributes_to", Contribution: 5, ActivationThreshold: 5, SortOrder: len(linkages)}) } } sort.SliceStable(linkages, func(i, j int) bool { return linkages[i].SortOrder < linkages[j].SortOrder }) input.Linkages = linkages // Adapters: symptom tags and order SKUs initialize symptom weights. // SKU 只提示相关症状(权重 3,触发症状确认题),不会直接命中疾病—— // 疾病必须由已确认的症状联动带出。 adapters := make([]scriptkit.AdapterInput, 0) for _, name := range bigNames { adapters = append(adapters, scriptkit.AdapterInput{SourceField: "input.input_symptom_tags", MatchValue: name, TargetDimensionValueKey: bigKey[name], Weight: 8, SortOrder: len(adapters)}) } for _, name := range subNames { adapters = append(adapters, scriptkit.AdapterInput{SourceField: "input.input_symptom_tags", MatchValue: name, TargetDimensionValueKey: subKey[name], Weight: 8, SortOrder: len(adapters)}) } for _, row := range rows { if strings.TrimSpace(row.SKUCode) == "" { continue } if bigKey[row.Symptom] != "" { adapters = append(adapters, scriptkit.AdapterInput{SourceField: "derived.matched_skus", MatchValue: row.SKUCode, TargetDimensionValueKey: bigKey[row.Symptom], Weight: 3, SortOrder: len(adapters)}) } } sort.SliceStable(adapters, func(i, j int) bool { return adapters[i].SortOrder < adapters[j].SortOrder }) input.Adapters = adapters // Stage 1: 开场. 开场只做问候和宠物档案补充,不显示任何维度(症状只在症状节点显示)。 opening := scriptkit.StageInput{ StageKey: "opening", Name: "开场", Purpose: "确认顾客与宠物信息,并补充宠物档案", PrimaryDimensionKey: "", SortOrder: 1, Scripts: []scriptkit.ScriptInput{ {ScriptKey: "opening_greeting", Name: "开场话术", ScriptType: "template", Content: bank.OpeningGreeting, SortOrder: 10}, }, } input.Stages = append(input.Stages, opening) // Stage 2: 症状确认。候选症状超过 3 个时先做一道多选筛选题, // 筛完再对选中的症状按由浅入深的顺序做选择题。 symptomStage := scriptkit.StageInput{StageKey: "symptom", Name: "症状确认", Purpose: "确认宠物当前的主要症状", PrimaryDimensionKey: "symptom", SortOrder: 2, Scripts: []scriptkit.ScriptInput{}} for index, guide := range preGuide { symptomStage.Scripts = append(symptomStage.Scripts, scriptkit.ScriptInput{ScriptKey: keys.key("symptom_guide"), Name: "引导话术" + guide.Title, ScriptType: "fallback", Content: guide.Template, SortOrder: 100 + index}) } screenOptions := make([]scriptkit.OptionInput, 0, len(bigNames)) for _, name := range bigNames { short := bank.Symptoms[name].Short if short == "" { short = name } screenOptions = append(screenOptions, scriptkit.OptionInput{OptionKey: bigKey[name], Label: short, TargetDimensionValueKey: bigKey[name], Effect: "set", EffectValue: 10, SortOrder: len(screenOptions) + 1}) } symptomStage.Scripts = append(symptomStage.Scripts, scriptkit.ScriptInput{ ScriptKey: keys.key("symptom_screen"), Name: "症状筛选", ScriptType: "screen", Content: bank.SymptomScreeningTemplate, Multiple: true, SortOrder: 50, Options: screenOptions, }) for _, name := range bigNames { questions := bank.Symptoms[name] confirm := scriptkit.ScriptInput{ ScriptKey: keys.key("symptom_confirm"), Name: "确认" + name + "症状", ScriptType: "confirm", Content: questions.Confirm, DimensionValueKey: bigKey[name], ConfirmThreshold: 3, ShowThreshold: 5, SortOrder: 1000, Options: []scriptkit.OptionInput{ {OptionKey: "yes", Label: bank.SymptomConfirmOptions.Yes, TargetDimensionValueKey: bigKey[name], Effect: "set", EffectValue: 8, SortOrder: 1}, {OptionKey: "no", Label: bank.SymptomConfirmOptions.No, TargetDimensionValueKey: bigKey[name], Effect: "zero", EffectValue: 0, SortOrder: 2}, }, } symptomStage.Scripts = append(symptomStage.Scripts, confirm) for choiceIndex, choice := range questions.Choices { options := make([]scriptkit.OptionInput, 0, len(choice.Options)) for optionIndex, option := range choice.Options { options = append(options, scriptkit.OptionInput{OptionKey: option.Key, Label: option.Label, Effect: "set", EffectValue: 0, SortOrder: optionIndex + 1}) } scriptKey := keys.key("symptom_choice") symptomStage.Scripts = append(symptomStage.Scripts, scriptkit.ScriptInput{ ScriptKey: scriptKey, Name: name + choice.Title, ScriptType: "choice", Content: choice.Question, DimensionValueKey: bigKey[name], ConfirmThreshold: 3, ShowThreshold: 5, CollectFieldKey: scriptKey, Multiple: choice.Multiple, SortOrder: 1100 + choiceIndex, Options: options, }) } } for _, name := range subNames { for index, ask := range smallAsk[name] { symptomStage.Scripts = append(symptomStage.Scripts, scriptkit.ScriptInput{ScriptKey: keys.key("symptom_sub_ask"), Name: name + ask.Title, ScriptType: "info", Content: ask.Template, DimensionValueKey: subKey[name], ConfirmThreshold: 3, ShowThreshold: 5, SortOrder: 5000 + index}) } } input.Stages = append(input.Stages, symptomStage) // Stage 3: 拟诊确认. 候选疾病(由已确认症状带出,权重5)先显示确认题; // 客户确认后(权重10)才显示该疾病的确认话术,可同时确认多个疾病。 diagnosisStage := scriptkit.StageInput{StageKey: "diagnosis", Name: "拟诊确认", Purpose: "结合症状确认可能的疾病方向(可确认多个),确认后再进入推荐", PrimaryDimensionKey: "disease", SortOrder: 3, Scripts: []scriptkit.ScriptInput{ {ScriptKey: "diagnosis_fallback", Name: "拟诊兜底话术", ScriptType: "fallback", Content: bank.DiagnosisFallback, SortOrder: 100}, }} for _, name := range diseaseNames { phrase := bank.Diseases[name].Phrase confirm := scriptkit.ScriptInput{ ScriptKey: keys.key("disease_confirm"), Name: "确认" + name + "方向", ScriptType: "confirm", Content: strings.ReplaceAll(bank.DiseaseConfirmTemplate, "{{disease_phrase}}", phrase), DimensionValueKey: diseaseKey[name], ConfirmThreshold: 5, ShowThreshold: 10, SortOrder: 1000, Options: []scriptkit.OptionInput{ {OptionKey: "yes", Label: bank.DiseaseConfirmOptions.Yes, TargetDimensionValueKey: diseaseKey[name], Effect: "set", EffectValue: 10, SortOrder: 1}, {OptionKey: "no", Label: bank.DiseaseConfirmOptions.No, TargetDimensionValueKey: diseaseKey[name], Effect: "zero", EffectValue: 0, SortOrder: 2}, }, } diagnosisStage.Scripts = append(diagnosisStage.Scripts, confirm) diagnosisStage.Scripts = append(diagnosisStage.Scripts, scriptkit.ScriptInput{ ScriptKey: keys.key("disease_copy"), Name: name + "疾病确认话术", ScriptType: "message", Content: strings.ReplaceAll(bank.DiseaseConfirmedCopyTemplate, "{{disease_phrase}}", phrase), DimensionValueKey: diseaseKey[name], ConfirmThreshold: 5, ShowThreshold: 10, SortOrder: 1100, }) diagnosisStage.Scripts = append(diagnosisStage.Scripts, scriptkit.ScriptInput{ ScriptKey: keys.key("disease_copy"), Name: name + "方案引入话术", ScriptType: "message", Content: bank.DiseasePlanIntro, DimensionValueKey: diseaseKey[name], ConfirmThreshold: 5, ShowThreshold: 10, SortOrder: 1101, }) } input.Stages = append(input.Stages, diagnosisStage) // Stage 4: 药品推荐与注意事项. 只有客户确认过的疾病(权重10)才会推荐。 recommendStage := scriptkit.StageInput{StageKey: "recommend", Name: "药品推荐与注意事项", Purpose: "按已确认的疾病方向推荐方案、商品和注意事项", PrimaryDimensionKey: "disease", SortOrder: 4, Scripts: []scriptkit.ScriptInput{ {ScriptKey: "recommend_fallback", Name: "推荐兜底话术", ScriptType: "fallback", Content: bank.RecommendFallback, SortOrder: 100}, }} for _, name := range diseaseNames { order := 1000 combos := combinations[name] sort.SliceStable(combos, func(i, j int) bool { return combos[i].Weight > combos[j].Weight }) for _, combination := range combos { products := make([]scriptkit.ProductInput, 0, len(combination.Products)) for _, product := range combination.Products { if strings.TrimSpace(product.SKUCode) == "" { continue } products = append(products, scriptkit.ProductInput{SKUCode: product.SKUCode, ProductName: product.ProductName}) } recommendStage.Scripts = append(recommendStage.Scripts, scriptkit.ScriptInput{ ScriptKey: keys.key("recommend_combination"), Name: name + combination.Name, ScriptType: "message", Content: strings.ReplaceAll(combination.Script, "{{purchased_products}}", "{{input.product_names}}"), DimensionValueKey: diseaseKey[name], ConfirmThreshold: 5, ShowThreshold: 10, Products: products, SortOrder: order, }) order++ } recommendStage.Scripts = append(recommendStage.Scripts, scriptkit.ScriptInput{ ScriptKey: keys.key("recommend_note"), Name: name + "注意事项", ScriptType: "message", Content: bank.DiseaseCareNote, DimensionValueKey: diseaseKey[name], ConfirmThreshold: 5, ShowThreshold: 10, SortOrder: order, }) } input.Stages = append(input.Stages, recommendStage) // Verify generated keys are valid before returning. if err := scriptkit.ValidatePackageInput(input); err != nil { return scriptkit.PackageInput{}, fmt.Errorf("generated package is invalid: %w", err) } return input, nil } func readQuestionBank(filename string) (petQuestionBank, error) { raw, err := os.ReadFile(filename) if err != nil { return petQuestionBank{}, fmt.Errorf("read pet question bank %s: %w", filename, err) } var bank petQuestionBank if err := json.Unmarshal(raw, &bank); err != nil { return petQuestionBank{}, fmt.Errorf("parse pet question bank %s: %w", filename, err) } if bank.SymptomConfirmOptions.Yes == "" { bank.SymptomConfirmOptions.Yes = "有" } if bank.SymptomConfirmOptions.No == "" { bank.SymptomConfirmOptions.No = "没有" } if bank.DiseaseConfirmOptions.Yes == "" { bank.DiseaseConfirmOptions.Yes = "对,按这个判断" } if bank.DiseaseConfirmOptions.No == "" { bank.DiseaseConfirmOptions.No = "感觉不太像" } if bank.SymptomScreeningTemplate == "" { return petQuestionBank{}, fmt.Errorf("pet question bank %s is missing symptom_screening_template", filename) } if bank.DiseaseConfirmTemplate == "" || bank.DiseaseConfirmedCopyTemplate == "" { return petQuestionBank{}, fmt.Errorf("pet question bank %s is missing disease templates", filename) } return bank, nil } func readClassification(filename string) (petClassification, error) { raw, err := os.ReadFile(filename) if err != nil { return petClassification{}, fmt.Errorf("read pet classification %s: %w", filename, err) } var wrapper struct { ConsultPayload string `json:"consultPayload"` } if err := json.Unmarshal(raw, &wrapper); err != nil { return petClassification{}, fmt.Errorf("parse pet classification wrapper %s: %w", filename, err) } var result petClassification if err := json.Unmarshal([]byte(wrapper.ConsultPayload), &result); err != nil { return petClassification{}, fmt.Errorf("parse pet classification %s: %w", filename, err) } return result, nil } func readStringMap(filename string) (map[string]string, error) { raw, err := os.ReadFile(filename) if err != nil { return nil, fmt.Errorf("read pet string map %s: %w", filename, err) } var result map[string]string if err := json.Unmarshal(raw, &result); err != nil { return nil, fmt.Errorf("parse pet string map %s: %w", filename, err) } return result, nil } func readPetScriptMap(filename string) (map[string][]petScriptEntry, error) { raw, err := os.ReadFile(filename) if err != nil { return nil, fmt.Errorf("read pet script map %s: %w", filename, err) } var source map[string][]petScriptEntry if err := json.Unmarshal(raw, &source); err != nil { return nil, fmt.Errorf("parse pet script map %s: %w", filename, err) } return source, nil } func readPreGuide(filename string) ([]petScriptEntry, error) { raw, err := os.ReadFile(filename) if err != nil { return nil, fmt.Errorf("read pet pre-symptom guide %s: %w", filename, err) } var source struct { PreSymptomScripts []petScriptEntry `json:"pre_symptom_scripts"` } if err := json.Unmarshal(raw, &source); err != nil { return nil, fmt.Errorf("parse pet pre-symptom guide %s: %w", filename, err) } return source.PreSymptomScripts, nil } // readCombinations loads 疾病 -> 组合方案话术 and the symptom keys of the source. func readCombinations(filename string) (map[string][]petCombination, map[string]bool, error) { raw, err := os.ReadFile(filename) if err != nil { return nil, nil, fmt.Errorf("read pet script combinations %s: %w", filename, err) } var source map[string]map[string]petScriptDisease if err := json.Unmarshal(raw, &source); err != nil { return nil, nil, fmt.Errorf("parse pet script combinations %s: %w", filename, err) } result := map[string][]petCombination{} symptoms := map[string]bool{} for symptom, diseases := range source { symptoms[symptom] = true for disease, definition := range diseases { result[disease] = append(result[disease], definition.Combinations...) } } return result, symptoms, nil } func readPetKnowledge(filename string) ([]petKnowledgeRow, error) { book, err := excelize.OpenFile(filename) if err != nil { return nil, fmt.Errorf("open pet knowledge %s: %w", filename, err) } defer book.Close() sheets := book.GetSheetList() if len(sheets) == 0 { return nil, fmt.Errorf("pet knowledge %s has no worksheet", filename) } iterator, err := book.Rows(sheets[0]) if err != nil { return nil, fmt.Errorf("read pet knowledge worksheet: %w", err) } defer iterator.Close() columns := map[string]int{} result := make([]petKnowledgeRow, 0) rowNumber := 0 for iterator.Next() { rowNumber++ values, err := iterator.Columns() if err != nil { return nil, fmt.Errorf("read pet knowledge row %d: %w", rowNumber, err) } if rowNumber == 1 { for index, value := range values { columns[strings.TrimSpace(value)] = index } for _, required := range []string{"症状", "疾病", "方案", "SKU_CODE", "商品标题"} { if _, ok := columns[required]; !ok { return nil, fmt.Errorf("pet knowledge is missing column %q", required) } } continue } row := petKnowledgeRow{ Symptom: excelValue(values, columns["症状"]), Disease: excelValue(values, columns["疾病"]), SKUCode: excelValue(values, columns["SKU_CODE"]), ProductName: excelValue(values, columns["商品标题"]), } if row.Symptom == "" && row.Disease == "" { continue } if row.Symptom == "" || row.Disease == "" { return nil, fmt.Errorf("pet knowledge row %d must contain 症状 and 疾病", rowNumber) } result = append(result, row) } if err := iterator.Error(); err != nil { return nil, fmt.Errorf("iterate pet knowledge: %w", err) } if len(result) == 0 { return nil, fmt.Errorf("pet knowledge contains no data") } return result, nil } func excelValue(values []string, index int) string { if index >= len(values) { return "" } return strings.TrimSpace(values[index]) }