update
This commit is contained in:
602
scripts/seed-pet-doctor/package_builder.go
Normal file
602
scripts/seed-pet-doctor/package_builder.go
Normal file
@@ -0,0 +1,602 @@
|
||||
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])
|
||||
}
|
||||
Reference in New Issue
Block a user