feat: simplify SOP to immediate-effect configuration
This commit is contained in:
320
scripts/seed-pet-doctor/knowledge_import.go
Normal file
320
scripts/seed-pet-doctor/knowledge_import.go
Normal file
@@ -0,0 +1,320 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.iwork-ai.com/xdc/iqudo-top1/internal/model"
|
||||
"github.com/xuri/excelize/v2"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const petKnowledgeSource = "症状-疾病-方案.xlsx"
|
||||
const petScriptSource = "症状-疾病-话术.json"
|
||||
|
||||
func resetPetKnowledge(tx *gorm.DB, tenantID, scenarioID uint64) error {
|
||||
var ids []uint64
|
||||
if err := tx.Model(&model.KnowledgeItem{}).Where("tenant_id = ? AND scenario_id = ?", tenantID, scenarioID).Pluck("id", &ids).Error; err != nil {
|
||||
return fmt.Errorf("list old pet knowledge: %w", err)
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
if err := tx.Where("tenant_id = ? AND scenario_id = ? AND (from_knowledge_id IN ? OR to_knowledge_id IN ?)", tenantID, scenarioID, ids, ids).Delete(&model.KnowledgeRelation{}).Error; err != nil {
|
||||
return fmt.Errorf("delete old pet knowledge relations: %w", err)
|
||||
}
|
||||
if err := tx.Where("tenant_id = ? AND scenario_id = ? AND id IN ?", tenantID, scenarioID, ids).Delete(&model.KnowledgeItem{}).Error; err != nil {
|
||||
return fmt.Errorf("delete old pet knowledge items: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type petScriptDisease struct {
|
||||
Plans []struct {
|
||||
Plan string `json:"plan"`
|
||||
Products []struct {
|
||||
SKUCode string `json:"sku_code"`
|
||||
ProductName string `json:"product_name"`
|
||||
} `json:"products"`
|
||||
} `json:"plans"`
|
||||
Combinations []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"`
|
||||
} `json:"combinations"`
|
||||
}
|
||||
|
||||
func importPetScripts(tx *gorm.DB, tenantID, scenarioID uint64, filename string) error {
|
||||
raw, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read pet scripts %s: %w", filename, err)
|
||||
}
|
||||
var source map[string]map[string]petScriptDisease
|
||||
if err := json.Unmarshal(raw, &source); err != nil {
|
||||
return fmt.Errorf("parse pet scripts %s: %w", filename, err)
|
||||
}
|
||||
for symptom, diseases := range source {
|
||||
for disease, definition := range diseases {
|
||||
diseaseKey := petKnowledgeKey("disease", disease)
|
||||
if err := upsertPetScriptItems(tx, tenantID, scenarioID, symptom, disease, diseaseKey, definition); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertPetScriptItems(tx *gorm.DB, tenantID, scenarioID uint64, symptom, disease, diseaseKey string, definition petScriptDisease) error {
|
||||
symptomKey := petKnowledgeKey("symptom", symptom)
|
||||
if err := upsertPetKnowledgeItem(tx, tenantID, scenarioID, symptomKey, symptom, "symptom", []byte(`{"source":"`+petScriptSource+`"}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := upsertPetKnowledgeItem(tx, tenantID, scenarioID, diseaseKey, disease, "disease", []byte(`{"source":"`+petScriptSource+`"}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := upsertPetRelation(tx, scenarioID, symptomKey, "possible_disease", diseaseKey, 3000); err != nil {
|
||||
return err
|
||||
}
|
||||
for index, combination := range definition.Combinations {
|
||||
copyName := fmt.Sprintf("%s-%s话术", disease, combination.Name)
|
||||
copyKey := petKnowledgeKey("copy", "json_"+symptom+"_"+disease+"_"+combination.Name)
|
||||
content, _ := json.Marshal(map[string]interface{}{"source": petScriptSource, "template": strings.ReplaceAll(combination.Script, "{{purchased_products}}", "{{input.product_names}}"), "combination": combination.Name, "weight": combination.Weight})
|
||||
if err := upsertPetKnowledgeItem(tx, tenantID, scenarioID, copyKey, copyName, "copy", content); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := upsertPetRelation(tx, scenarioID, diseaseKey, "recommended_copy", copyKey, 3000+index); err != nil {
|
||||
return err
|
||||
}
|
||||
for productIndex, product := range combination.Products {
|
||||
if strings.TrimSpace(product.SKUCode) == "" {
|
||||
continue
|
||||
}
|
||||
productKey := petKnowledgeKey("product", product.SKUCode)
|
||||
content, _ := json.Marshal(map[string]interface{}{"source": petScriptSource, "sku_code": product.SKUCode, "product_name": product.ProductName})
|
||||
if err := upsertPetKnowledgeItem(tx, tenantID, scenarioID, productKey, product.ProductName, "product", content); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := upsertPetRelation(tx, scenarioID, diseaseKey, "recommended_product", productKey, 3400+index*100+productIndex); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for index, plan := range definition.Plans {
|
||||
if strings.TrimSpace(plan.Plan) == "" {
|
||||
continue
|
||||
}
|
||||
planKey := petKnowledgeKey("plan", plan.Plan)
|
||||
if err := upsertPetKnowledgeItem(tx, tenantID, scenarioID, planKey, plan.Plan, "plan", []byte(`{"source":"`+petScriptSource+`"}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := upsertPetRelation(tx, scenarioID, diseaseKey, "recommended_plan", planKey, 3200+index); err != nil {
|
||||
return err
|
||||
}
|
||||
for productIndex, product := range plan.Products {
|
||||
if strings.TrimSpace(product.SKUCode) == "" {
|
||||
continue
|
||||
}
|
||||
productKey := petKnowledgeKey("product", product.SKUCode)
|
||||
content, _ := json.Marshal(map[string]interface{}{"source": petScriptSource, "sku_code": product.SKUCode, "product_name": product.ProductName})
|
||||
if err := upsertPetKnowledgeItem(tx, tenantID, scenarioID, productKey, product.ProductName, "product", content); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := upsertPetRelation(tx, scenarioID, diseaseKey, "recommended_product", productKey, 3600+index*100+productIndex); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertPetKnowledgeItem(tx *gorm.DB, tenantID, scenarioID uint64, key, name, typeName string, content []byte) error {
|
||||
row := model.KnowledgeItem{TenantID: tenantID, ScenarioID: scenarioID, ItemKey: key, Name: name, Type: typeName, Content: datatypes.JSON(content), Status: "active", SortOrder: 3000}
|
||||
return tx.Where("tenant_id = ? AND scenario_id = ? AND item_key = ?", tenantID, scenarioID, key).Assign(row).FirstOrCreate(&row).Error
|
||||
}
|
||||
|
||||
func upsertPetRelation(tx *gorm.DB, scenarioID uint64, fromKey, relation, toKey string, order int) error {
|
||||
var from, to model.KnowledgeItem
|
||||
if err := tx.Where("scenario_id = ? AND item_key = ?", scenarioID, fromKey).First(&from).Error; err != nil {
|
||||
return fmt.Errorf("find script relation source %s: %w", fromKey, err)
|
||||
}
|
||||
if err := tx.Where("scenario_id = ? AND item_key = ?", scenarioID, toKey).First(&to).Error; err != nil {
|
||||
return fmt.Errorf("find script relation target %s: %w", toKey, err)
|
||||
}
|
||||
row := model.KnowledgeRelation{TenantID: from.TenantID, ScenarioID: scenarioID, FromKnowledgeID: from.ID, RelationType: relation, ToKnowledgeID: to.ID, Condition: datatypes.JSON([]byte(`{}`)), SortOrder: order}
|
||||
return tx.Where("scenario_id = ? AND from_knowledge_id = ? AND relation_type = ? AND to_knowledge_id = ?", scenarioID, from.ID, relation, to.ID).Assign(row).FirstOrCreate(&row).Error
|
||||
}
|
||||
|
||||
type petKnowledgeRow struct {
|
||||
Symptom string
|
||||
Disease string
|
||||
Plan string
|
||||
SKUCode string
|
||||
ProductName string
|
||||
}
|
||||
|
||||
func importPetKnowledge(tx *gorm.DB, tenantID, scenarioID uint64, filename string) error {
|
||||
rows, err := readPetKnowledge(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return upsertPetKnowledge(tx, tenantID, scenarioID, rows)
|
||||
}
|
||||
|
||||
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["疾病"]),
|
||||
Plan: excelValue(values, columns["方案"]),
|
||||
SKUCode: excelValue(values, columns["SKU_CODE"]),
|
||||
ProductName: excelValue(values, columns["商品标题"]),
|
||||
}
|
||||
if row.Symptom == "" && row.Disease == "" && row.Plan == "" {
|
||||
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])
|
||||
}
|
||||
|
||||
func upsertPetKnowledge(tx *gorm.DB, tenantID, scenarioID uint64, rows []petKnowledgeRow) error {
|
||||
type itemDefinition struct {
|
||||
name, typeName string
|
||||
content map[string]interface{}
|
||||
}
|
||||
items := map[string]itemDefinition{}
|
||||
type relationDefinition struct{ from, relation, to string }
|
||||
relations := map[relationDefinition]bool{}
|
||||
for _, row := range rows {
|
||||
symptomKey := petKnowledgeKey("symptom", row.Symptom)
|
||||
diseaseKey := petKnowledgeKey("disease", row.Disease)
|
||||
items[symptomKey] = itemDefinition{name: row.Symptom, typeName: "symptom"}
|
||||
items[diseaseKey] = itemDefinition{name: row.Disease, typeName: "disease"}
|
||||
relations[relationDefinition{symptomKey, "possible_disease", diseaseKey}] = true
|
||||
for index, template := range []string{
|
||||
"结合客户描述,目前更符合“%s”这一分型。建议先向客户说明判断依据,再介绍对应方案。",
|
||||
"针对“%s”,这边建议按下面的方案进行护理和商品搭配;如症状持续或加重,应及时就医。",
|
||||
} {
|
||||
copyName := fmt.Sprintf("%s推荐话术%d", row.Disease, index+1)
|
||||
copyKey := petKnowledgeKey("copy", copyName)
|
||||
items[copyKey] = itemDefinition{name: copyName, typeName: "copy", content: map[string]interface{}{"template": fmt.Sprintf(template, row.Disease)}}
|
||||
relations[relationDefinition{diseaseKey, "recommended_copy", copyKey}] = true
|
||||
}
|
||||
if row.Plan != "" {
|
||||
planKey := petKnowledgeKey("plan", row.Plan)
|
||||
items[planKey] = itemDefinition{name: row.Plan, typeName: "plan"}
|
||||
relations[relationDefinition{diseaseKey, "recommended_plan", planKey}] = true
|
||||
// The current SOP view expands one relation level from a symptom.
|
||||
relations[relationDefinition{symptomKey, "recommended_plan", planKey}] = true
|
||||
}
|
||||
if row.SKUCode != "" {
|
||||
productName := row.ProductName
|
||||
if productName == "" {
|
||||
productName = row.SKUCode
|
||||
}
|
||||
productKey := petKnowledgeKey("product", row.SKUCode)
|
||||
items[productKey] = itemDefinition{name: productName, typeName: "product", content: map[string]interface{}{"sku_code": row.SKUCode, "product_name": productName}}
|
||||
relations[relationDefinition{diseaseKey, "recommended_product", productKey}] = true
|
||||
}
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(items))
|
||||
for key := range items {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
ids := make(map[string]uint64, len(keys))
|
||||
for order, key := range keys {
|
||||
definition := items[key]
|
||||
payload := map[string]interface{}{"source": petKnowledgeSource}
|
||||
for name, value := range definition.content {
|
||||
payload[name] = value
|
||||
}
|
||||
content, _ := json.Marshal(payload)
|
||||
row := model.KnowledgeItem{TenantID: tenantID, ScenarioID: scenarioID, ItemKey: key, Name: definition.name, Type: definition.typeName, Content: datatypes.JSON(content), Status: "active", SortOrder: 1000 + order}
|
||||
if err := tx.Where("tenant_id = ? AND scenario_id = ? AND item_key = ?", tenantID, scenarioID, key).Assign(row).FirstOrCreate(&row).Error; err != nil {
|
||||
return fmt.Errorf("upsert imported knowledge item %s: %w", key, err)
|
||||
}
|
||||
ids[key] = row.ID
|
||||
}
|
||||
|
||||
relationList := make([]relationDefinition, 0, len(relations))
|
||||
for relation := range relations {
|
||||
relationList = append(relationList, relation)
|
||||
}
|
||||
sort.Slice(relationList, func(i, j int) bool {
|
||||
left, right := relationList[i], relationList[j]
|
||||
return left.from+left.relation+left.to < right.from+right.relation+right.to
|
||||
})
|
||||
for order, definition := range relationList {
|
||||
row := model.KnowledgeRelation{TenantID: tenantID, ScenarioID: scenarioID, FromKnowledgeID: ids[definition.from], RelationType: definition.relation, ToKnowledgeID: ids[definition.to], Condition: datatypes.JSON([]byte(`{}`)), SortOrder: 1000 + order}
|
||||
if err := tx.Where("scenario_id = ? AND from_knowledge_id = ? AND relation_type = ? AND to_knowledge_id = ?", scenarioID, row.FromKnowledgeID, row.RelationType, row.ToKnowledgeID).Assign(row).FirstOrCreate(&row).Error; err != nil {
|
||||
return fmt.Errorf("upsert imported knowledge relation: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func petKnowledgeKey(typeName, name string) string {
|
||||
digest := sha256.Sum256([]byte(typeName + "\x00" + strings.TrimSpace(name)))
|
||||
return "xlsx_" + typeName + "_" + hex.EncodeToString(digest[:6])
|
||||
}
|
||||
Reference in New Issue
Block a user