Files

171 lines
5.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}