package ctxmgr import ( "fmt" "regexp" "strings" "time" ) // ConversationTaskState 会话级任务状态(DST,对话状态跟踪) // 与项目级黑板的分工:黑板是全项目多方共享的全局 SSOT; // task_state 是记录“当前这场会话的任务推进到哪一步”的会话私有工作记忆。 type ConversationTaskState struct { ActiveTask string `json:"active_task,omitempty"` // 当前对话任务类型(如 intake_consult / case_compare / doc_reject_consult) TopicAnchor string `json:"topic_anchor,omitempty"` // 主题锚点:最近一轮有效核心问句 TopicEntities []string `json:"topic_entities,omitempty"` // 锚点实体(如项目名、器械分类、国家标准编号) CollectedSlots map[string]string `json:"collected_slots,omitempty"` // 已收集参数槽位 MissingSlots []string `json:"missing_slots,omitempty"` // 待补充关键槽位 OpenQuestions []string `json:"open_questions,omitempty"` // 未决技术/商务问题 UpdatedAt time.Time `json:"updated_at"` } // NewConversationTaskState 创建初始对话状态 func NewConversationTaskState() *ConversationTaskState { return &ConversationTaskState{ CollectedSlots: make(map[string]string), MissingSlots: make([]string, 0), OpenQuestions: make([]string, 0), TopicEntities: make([]string, 0), UpdatedAt: time.Now(), } } // UpdateFromTurn 根据当轮输入、识别槽位与分支流转刷新状态机 func (ts *ConversationTaskState) UpdateFromTurn(userInput string, intentName string, filledSlots map[string]string, branch string, stage string) { if ts.CollectedSlots == nil { ts.CollectedSlots = make(map[string]string) } ts.UpdatedAt = time.Now() // 1. 更新活跃任务与主题锚点 if intentName != "" && intentName != "CASUAL_CHAT" { ts.ActiveTask = intentName } trimmedInput := strings.TrimSpace(userInput) if isCompleteAnchorCandidate(trimmedInput) { ts.TopicAnchor = trimmedInput } // 2. 合入已识别槽位 for k, v := range filledSlots { if strings.TrimSpace(v) != "" { ts.CollectedSlots[k] = strings.TrimSpace(v) } } // 3. 提取常见主题实体(器械类型、强标号等) ts.extractTopicEntities(trimmedInput) // 4. 计算当前阶段缺失槽位 ts.computeMissingSlots(stage) // 5. 若分支为极速兑现(EXPECTATION_HIT),若已完成某项挂起待办,相应清理未决问题 if branch == "EXPECTATION_HIT" && len(ts.OpenQuestions) > 0 { ts.OpenQuestions = ts.OpenQuestions[1:] } } func (ts *ConversationTaskState) extractTopicEntities(input string) { entityCandidates := []string{ "超声手术设备", "超声刀", "高频超声", "掌上超声", "内窥镜", "腹腔镜", "医用电气设备", "GB 9706.1-2020", "YY 9706.102", "YY 0505", "GB/T 42062", "CE认证", "NMPA", "FDA 510(k)", } for _, cand := range entityCandidates { if strings.Contains(input, cand) && !containsString(ts.TopicEntities, cand) { ts.TopicEntities = append(ts.TopicEntities, cand) if len(ts.TopicEntities) > 6 { ts.TopicEntities = ts.TopicEntities[len(ts.TopicEntities)-6:] } } } } func (ts *ConversationTaskState) computeMissingSlots(stage string) { required := []string{"device_type", "target_market"} if stage == "V0.1_triage" || stage == "intake" { required = append(required, "budget") } missing := make([]string, 0) for _, req := range required { if _, ok := ts.CollectedSlots[req]; !ok { missing = append(missing, req) } } ts.MissingSlots = missing } // RenderPromptSnippet 渲染为 Prompt 中的【工作记忆】段 func (ts *ConversationTaskState) RenderPromptSnippet() string { if ts == nil { return "" } var sb strings.Builder sb.WriteString("【本轮任务状态 (Working Memory)】\n") if ts.ActiveTask != "" { sb.WriteString(fmt.Sprintf("- 活跃任务: %s\n", ts.ActiveTask)) } if ts.TopicAnchor != "" { sb.WriteString(fmt.Sprintf("- 主题锚点: \"%s\"\n", ts.TopicAnchor)) } if len(ts.TopicEntities) > 0 { sb.WriteString(fmt.Sprintf("- 核心实体: %s\n", strings.Join(ts.TopicEntities, "、"))) } if len(ts.CollectedSlots) > 0 { var slotStrs []string for k, v := range ts.CollectedSlots { slotStrs = append(slotStrs, fmt.Sprintf("%s=%s", k, v)) } sb.WriteString(fmt.Sprintf("- 已收集参数: %s\n", strings.Join(slotStrs, "; "))) } if len(ts.MissingSlots) > 0 { sb.WriteString(fmt.Sprintf("- 待补充槽位: %s\n", strings.Join(ts.MissingSlots, "、"))) } if len(ts.OpenQuestions) > 0 { sb.WriteString(fmt.Sprintf("- 未决问题: %s\n", strings.Join(ts.OpenQuestions, ";"))) } return sb.String() } func isGenericAffirmative(s string) bool { s = strings.TrimSpace(s) return s == "好" || s == "好的" || s == "行" || s == "可以" || s == "1" || s == "同意" || s == "推进" } var ( ellipsisAnchorFilterRe = regexp.MustCompile(`(?:呢|也|还是|怎么样|怎样|如何)[??]*$`) pronounAnchorFilterRe = regexp.MustCompile(`^(?:那|另外|还有|同时|那关于)?\s*(?:这个|该|这|这份|那份|上次说的|之前说的|它|它们)`) ) func isCompleteAnchorCandidate(s string) bool { s = strings.TrimSpace(s) if s == "" || isGenericAffirmative(s) { return false } runes := []rune(s) // 短于 12 个字符且含有省略或代词特征,不能覆盖已有完整主题锚点 if len(runes) < 12 && (ellipsisAnchorFilterRe.MatchString(s) || pronounAnchorFilterRe.MatchString(s)) { return false } // 即使达到或超过 12 个字符,如果末尾是纯疑问助词(例如单纯追问标准),也尽量避免冲刷 if ellipsisAnchorFilterRe.MatchString(s) && len(runes) < 16 { return false } return len(runes) >= 6 } func containsString(arr []string, s string) bool { for _, item := range arr { if item == s { return true } } return false }