package service import ( "encoding/json" "fmt" "strings" "github.com/sundynix/pets-be/internal/ai" "github.com/sundynix/pets-be/internal/model" ) // 养宠助手安全护栏:不诊断、只做观察与就医前信息整理、始终给就医提示 const aiSafetyPrompt = `你是「肉垫计划」的养宠助手,服务新手猫狗主人。要求: 1. 只提供日常照护建议、观察要点和就医前信息整理,绝不做医学诊断或开具体药物剂量。 2. 任何异常都要说明"何时需要尽快就医"(如持续超过24小时、便血、频繁呕吐、精神明显变差等)。 3. 基于用户提供的宠物档案和记录作答,不要泛泛而谈。 4. 语气亲切、简洁,用中文,避免长篇大论。` // petBrief 组装宠物档案 + 近期记录的上下文文本 func (s *Service) petBrief(petID string) string { var pet model.Pet if err := s.db.First(&pet, petID).Error; err != nil { return "" } var records []model.HealthRecord s.db.Where("pet_id = ?", petID).Order("occurred_at desc").Limit(8).Find(&records) var b strings.Builder fmt.Fprintf(&b, "宠物档案:%s(%s,%s,%s,当前体重%s)。\n", pet.Name, pet.Type, pet.Gender, pet.Stage, pet.Weight) if len(records) > 0 { b.WriteString("近期记录:") items := make([]string, 0, len(records)) for _, r := range records { items = append(items, r.Title) } b.WriteString(strings.Join(items, ";")) b.WriteString("。") } return b.String() } // llmChat 真实模型聊天回复 func (s *Service) llmChat(petID *string, text string) (string, error) { system := aiSafetyPrompt if petID != nil { if brief := s.petBrief(*petID); brief != "" { system += "\n\n" + brief } } return s.ai.Complete(system, []ai.Message{{Role: "user", Content: text}}, ai.Options{Temperature: -1}) } // SymptomInput 异常观察入参 type SymptomInput struct { Symptoms []string Duration string Spirit string } // SymptomResult 风险评估结果(对应前端 risk 弹层) type SymptomResult struct { RiskLevel string `json:"risk_level"` // 低/中/高 Causes string `json:"causes"` Suggestion string `json:"suggestion"` SeekCare string `json:"seek_care"` } // AssessSymptom 异常风险评估:启用模型则结构化输出,否则规则化 func (s *Service) AssessSymptom(userID, petID string, in SymptomInput) (*SymptomResult, error) { if _, err := s.ownedPet(userID, petID); err != nil { return nil, err } // 规则化兜底 fallback := &SymptomResult{ RiskLevel: "中", Causes: "可能与换粮、应激或消化不适有关。这不是诊断结论。", Suggestion: "继续观察精神、食欲和排便;暂停新食物;记录呕吐或腹泻次数。", SeekCare: "如果持续超过 24 小时,或伴随便血、精神明显变差、频繁呕吐,建议尽快就医。", } if s.ai == nil || !s.ai.Enabled() { return fallback, nil } system := aiSafetyPrompt + "\n\n" + s.petBrief(petID) + `\n请根据以下异常信息评估,严格返回 JSON:{"risk_level":"低|中|高","causes":"可能原因(非诊断)","suggestion":"观察与照护建议","seek_care":"何时需要就医"}` user := fmt.Sprintf("异常表现:%s;持续时间:%s;精神状态:%s。", strings.Join(in.Symptoms, "、"), in.Duration, in.Spirit) out, err := s.ai.Complete(system, []ai.Message{{Role: "user", Content: user}}, ai.Options{JSON: true, Temperature: -1}) if err != nil { return fallback, nil // 模型故障不阻断,回退 } var res SymptomResult if err := json.Unmarshal([]byte(extractJSON(out)), &res); err != nil || res.RiskLevel == "" { return fallback, nil } return &res, nil } // aiPlanExtract 用模型提取计划信息 + 生成任务;失败返回 ok=false 交由规则化处理 type aiPlanExtracted struct { Stage string `json:"stage"` Risk string `json:"risk"` Priority string `json:"priority"` Reminder string `json:"reminder"` Tasks []struct { DayLabel string `json:"day_label"` Title string `json:"title"` Description string `json:"description"` } `json:"tasks"` } func (s *Service) aiPlanExtract(pet *model.Pet, input string) (*aiPlanExtracted, bool) { if s.ai == nil || !s.ai.Enabled() { return nil, false } system := aiSafetyPrompt + "\n\n" + s.petBrief(pet.ID) + `\n请把用户描述提炼为可执行的养宠计划,严格返回 JSON:` + `{"stage":"阶段","risk":"近期风险","priority":"观察重点","reminder":"重点提醒",` + `"tasks":[{"day_label":"如 Day 1-2","title":"任务标题","description":"要点"}]},tasks 3-5 条。` out, err := s.ai.Complete(system, []ai.Message{{Role: "user", Content: input}}, ai.Options{JSON: true, Temperature: -1}) if err != nil { return nil, false } var ex aiPlanExtracted if err := json.Unmarshal([]byte(extractJSON(out)), &ex); err != nil || len(ex.Tasks) == 0 { return nil, false } return &ex, true } // extractJSON 容错:从可能含前后缀的文本中截取第一个 { 到最后一个 } func extractJSON(s string) string { i := strings.IndexByte(s, '{') j := strings.LastIndexByte(s, '}') if i >= 0 && j > i { return s[i : j+1] } return s }