package squad import ( "fmt" "strings" "time" ) // StepEvaluator 负责单步执行产物评估与自适应反馈调整 type StepEvaluator struct{} // NewStepEvaluator 创建单步评估器 func NewStepEvaluator() *StepEvaluator { return &StepEvaluator{} } // EvaluateStepOutput 评估单步产物与黑板事实,推导基准假设与动态重排指令 func (e *StepEvaluator) EvaluateStepOutput( wsDir string, step StepSpec, outputContent string, bb *BlackboardState, ) (*EvaluationResult, error) { now := time.Now() res := &EvaluationResult{ StepID: step.ID, Action: StepActionContinue, Reason: "步骤按计划正常推进", NewAssumptions: make([]AssumptionItem, 0), DynamicStepsToAdd: make([]StepSpec, 0), DynamicStepsToAlter: make([]StepSpec, 0), StepsToSkip: make([]string, 0), } contentLower := strings.ToLower(outputContent) // 1. 基于黑板缺口与产物推导“基准假设 (Baseline Assumptions)” var gapTexts []string if bb != nil { for _, g := range bb.Gaps { gapTexts = append(gapTexts, strings.ToLower(g.Item)) } } combinedLower := contentLower + " " + strings.Join(gapTexts, " ") // 规则 1.1: 灭菌方式缺口推断 if strings.Contains(combinedLower, "未明确灭菌") || strings.Contains(combinedLower, "未提供灭菌") || strings.Contains(combinedLower, "未确定灭菌") || strings.Contains(combinedLower, "缺少灭菌方式") || strings.Contains(combinedLower, "未提供产品灭菌方式") { if !hasAssumptionTopic(bb, "sterilization_method") { res.NewAssumptions = append(res.NewAssumptions, AssumptionItem{ ID: fmt.Sprintf("assump_sterilization_%d", now.UnixNano()), Topic: "sterilization_method", Statement: "基准假设灭菌方式采用环氧乙烷(EO)灭菌,解析期设定为14天", Rationale: "客户未明确灭菌方式,依据二类无源无菌高分子介入器械行业通用工程实践设定", Source: "inferred", }) } } // 规则 1.2: 货架有效期缺口推断 if strings.Contains(combinedLower, "未明确货架寿命") || strings.Contains(combinedLower, "有效期未定") || strings.Contains(combinedLower, "未提供有效期") { if !hasAssumptionTopic(bb, "shelf_life") { res.NewAssumptions = append(res.NewAssumptions, AssumptionItem{ ID: fmt.Sprintf("assump_shelflife_%d", now.UnixNano()), Topic: "shelf_life", Statement: "基准假设产品货架有效期设定为2年,需执行加速老化试验", Rationale: "符合常规无菌医疗器械首次注册基准设计周期", Source: "inferred", }) } } // 规则 1.3: 注册目标市场缺口推断 if strings.Contains(combinedLower, "未明确目标市场") || strings.Contains(combinedLower, "市场未定") { if !hasAssumptionTopic(bb, "target_market") { res.NewAssumptions = append(res.NewAssumptions, AssumptionItem{ ID: fmt.Sprintf("assump_market_%d", now.UnixNano()), Topic: "target_market", Statement: "基准假设首发上市目标为中国境内NMPA二类医疗器械注册", Rationale: "以国内优先注册为基准,后续拓展海外认证", Source: "inferred", }) } } // 2. 检查是否触发动态跳步 (Skip Step) if strings.Contains(contentLower, "无需开展重复公开资料检索") || strings.Contains(contentLower, "无需重复开展市场竞品检索") || strings.Contains(contentLower, "竞品数据已全面锁定") || strings.Contains(contentLower, "跳过市场调研") { if res.Action == StepActionContinue { res.Action = StepActionSkipStep } res.StepsToSkip = append(res.StepsToSkip, "step_02_market", "step_02", "market") res.Reason = "前序分析已充分锁定竞品数据,避免重复冗余检索" } // 3. 检查是否触发动态加步 (Add Step) // 射频/微波 EMC 与热安全风险 if (strings.Contains(contentLower, "射频") || strings.Contains(contentLower, "微波") || strings.Contains(contentLower, "消融")) && (strings.Contains(contentLower, "电磁兼容") || strings.Contains(contentLower, "emc") || strings.Contains(contentLower, "热灼伤")) { res.Action = StepActionAddStep res.DynamicStepsToAdd = append(res.DynamicStepsToAdd, StepSpec{ ID: "step_rf_emc_safety", Role: "engineer", Title: "高频电磁兼容性(EMC)与热安全防护专项工程论证", SubWorkspace: "tasks/dynamic_rf_emc", Mode: "execute", AllowDynamicAdjustment: false, Accept: []string{ "核算高频消融电磁辐射泄漏安全边际与抗干扰设计", "设计温度反馈闭环与防止意外热灼伤切断机制", }, }) res.Reason = "识别到大功率射频高频风险,动态注入EMC与热防护专项工程论证工序" } // 动物源病毒灭活专题 if (strings.Contains(contentLower, "牛心包") || strings.Contains(contentLower, "动物源")) && (strings.Contains(contentLower, "病毒灭活") || strings.Contains(contentLower, "免疫原性")) { res.Action = StepActionAddStep res.DynamicStepsToAdd = append(res.DynamicStepsToAdd, StepSpec{ ID: "step_animal_virus_inactivation", Role: "regulatory", Title: "动物源性材料病毒灭活与免疫原性合规专题论证", SubWorkspace: "tasks/dynamic_virus_inactivation", Mode: "execute", AllowDynamicAdjustment: false, Accept: []string{ "核查动物源溯源合规证明与供体检疫要求", "拟定基于ISO 22442的病毒灭活有效性验证方案", }, }) res.Reason = "识别到动物源性材料生物风险,动态追加病毒灭活合规论证工序" } return res, nil } func hasAssumptionTopic(bb *BlackboardState, topic string) bool { if bb == nil { return false } for _, a := range bb.Assumptions { if a.Topic == topic { return true } } return false }