212 lines
8.6 KiB
Go
212 lines
8.6 KiB
Go
package backthink
|
||
|
||
import (
|
||
"regexp"
|
||
"strings"
|
||
"unicode"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/urgency"
|
||
)
|
||
|
||
// 自主思考被引导(context.go 的 guidancePrompt)以如下紧凑、
|
||
// 机器可解析的决策收尾,好让服务不必再从触发的重要度
|
||
// 单独推断是否推送(design/20 §2 P2):
|
||
//
|
||
// [我决定:push] (或 no_push)
|
||
// [渠道:web|im|kook|telegram|...|auto]
|
||
// [紧急:routine|warning|critical] (可省略;见 parseProactiveUrgency)
|
||
// <温暖的正文内容>
|
||
//
|
||
// 这些标记按纯文本解析,因此模型稍有不同地
|
||
// 格式化它们时仍能得出可用的决策;格式错乱/含糊的决策
|
||
// 标记失败安全为「不推送」。
|
||
var (
|
||
// decideMarker 匹配 push/no_push 决策词,容忍两种宽度的
|
||
// 冒号与可选空格。它用于恢复决策,所以对
|
||
// 带括号的形式保持严格:裸的 "我决定:push" 被当作散文,
|
||
// 而不是决策(失败安全)。
|
||
decideMarker = regexp.MustCompile(`(?i)\[\s*我决定\s*[::]\s*(no_push|push)\s*\]`)
|
||
// channelMarker 匹配决策行上的目标渠道词。
|
||
channelMarker = regexp.MustCompile(`(?i)\[\s*渠道\s*[::]\s*([a-z0-9_\-]+)\s*\]`)
|
||
// urgencyMarker 匹配她自报的紧急度档位词。它是**结构化**通道:只认固定枚举,
|
||
// 绝不从散文里解读意图(项目红线);解析严格(必须带括号)=失败安全。
|
||
urgencyMarker = regexp.MustCompile(`(?i)\[\s*紧急\s*[::]\s*(routine|warning|critical)\s*\]`)
|
||
// decidePrefixes 是「尝试了决策但无法解析」的标记形状,
|
||
// 这类轮次被视为含糊(失败安全)。它
|
||
// 刻意只含带括号形式,因此仅仅在散文中出现
|
||
// "我决定:"(中文里很常见)的消息不会被误读为格式错乱的标记。
|
||
decidePrefixes = []string{"[我决定"}
|
||
|
||
// stripDecideRe / stripChannelRe 把决策/渠道标记识别为
|
||
// 需要从用户可见正文中删除的词,按它们预期的带括号形式
|
||
// 与全角括号形式处理,因此格式良好的决策永不泄漏。
|
||
stripDecideRe = regexp.MustCompile(`(?i)[【\[]\s*我决定\s*[::]\s*(no_push|push)\s*[】\]]`)
|
||
stripChannelRe = regexp.MustCompile(`(?i)[【\[]\s*渠道\s*[::]\s*[a-z0-9_\-]+\s*[】\]]`)
|
||
// stripUrgencyRe 删掉带括号的紧急度标记,档位值不限枚举:她写成枚举外的词时
|
||
// 解析侧不上抬(保守),面向用户的正文里也绝不能留着这段内部标记。
|
||
stripUrgencyRe = regexp.MustCompile(`(?i)[【\[]\s*紧急\s*[::]\s*[^】\]\s]+\s*[】\]]`)
|
||
bareDecideRe = regexp.MustCompile(`(?i)我决定\s*[::]\s*(no_push|push)`)
|
||
bareChannelRe = regexp.MustCompile(`(?i)渠道\s*[::]\s*[a-z0-9_\-]+`)
|
||
// bareUrgencyRe 只认枚举档位:散文里「紧急:」这类词很常见,
|
||
// 不该因为恰好紧跟一个英文词就被删掉。
|
||
bareUrgencyRe = regexp.MustCompile(`(?i)紧急\s*[::]\s*(routine|warning|critical)`)
|
||
)
|
||
|
||
// nonePhrases 是没有标记时表示「无可汇报」的结论。
|
||
// 正文在比较前先归一化(折叠大小写、去掉标点/空格),
|
||
// 因此 "无"、"无。" 与 "Nothing to report." 都收敛到
|
||
// 同一个词。
|
||
var nonePhrases = []string{
|
||
"无", "没什么", "没有", "无物可报", "暂无", "没有值得说的",
|
||
"nothing", "none", "nothingtoreport", "nothingworthreporting", "nothingnew",
|
||
}
|
||
|
||
// parseProactive 从思考结论中恢复推送决策、目标渠道提示与
|
||
// 干净的正文消息。它是纯函数:不读状态、不做 I/O,
|
||
// 因此可安全并发调用、测试成本低。
|
||
//
|
||
// 语义(design/20 §2 P2):
|
||
// - 格式良好的 [我决定:push|no_push] 标记具有权威性。
|
||
// - 格式错乱/含糊的决策标记 → 不推送(失败安全)。
|
||
// - 无标记:正文读作 "无"/"nothing to report" → 不推送;
|
||
// 否则正文被当作主动消息(push=true),与
|
||
// P2 之前的默认一致。
|
||
// - 渠道提示是 [渠道:...] 词,转小写;为空/auto 则保持
|
||
// 默认的活跃客户端优先、IM 降级路由。
|
||
// - 她自报的紧急度由 parseProactiveUrgency 单独恢复(固定枚举的
|
||
// 结构化标记,绝不从散文解读)。
|
||
// - 决策为推送但正文为空或仍读作「无」 → 不推送
|
||
// (失败安全),因此单独一个标记绝不会推出空消息。
|
||
func parseProactive(conclusion string) (message, channelHint string, push bool) {
|
||
text := strings.TrimSpace(conclusion)
|
||
|
||
if val, ok := matchDecide(text); ok {
|
||
push = val == "push"
|
||
} else if hasAmbiguousDecide(text) {
|
||
push = false
|
||
} else {
|
||
push = !looksNone(text)
|
||
}
|
||
|
||
channelHint = matchChannel(text)
|
||
message = stripInternalMarkers(text)
|
||
|
||
if push && (message == "" || looksNone(message)) {
|
||
push = false
|
||
}
|
||
return message, channelHint, push
|
||
}
|
||
|
||
// parseProactiveUrgency 从结论的结构化尾段恢复她自报的紧急度 —— 这是通用紧急度通道
|
||
// 的一种生产者形式(「她自己」):主动思考轮可以读外围插件数据、调工具,因此她要能
|
||
// 声明「我认为这条紧急、要突破安静时段发出去」。
|
||
//
|
||
// 它只认固定枚举 [紧急:routine|warning|critical],绝不从散文/自由文本里解读意图
|
||
// (项目红线:判断要结构化)。没写、格式错乱或写了枚举外的档位一律得到零值
|
||
// LevelRoutine(即不上抬)—— 保守默认优于猜测,误判只可能少打扰、不会多打扰。
|
||
func parseProactiveUrgency(conclusion string) urgency.Level {
|
||
m := urgencyMarker.FindStringSubmatch(conclusion)
|
||
if m == nil {
|
||
return urgency.LevelRoutine
|
||
}
|
||
switch strings.ToLower(m[1]) {
|
||
case "critical":
|
||
return urgency.LevelCritical
|
||
case "warning":
|
||
return urgency.LevelWarning
|
||
default:
|
||
return urgency.LevelRoutine
|
||
}
|
||
}
|
||
|
||
// matchDecide 返回匹配到的决策词(已转小写)以及
|
||
// 是否存在格式良好的决策标记。
|
||
func matchDecide(text string) (string, bool) {
|
||
m := decideMarker.FindStringSubmatch(text)
|
||
if m == nil {
|
||
return "", false
|
||
}
|
||
return strings.ToLower(m[1]), true
|
||
}
|
||
|
||
// hasAmbiguousDecide 报告是否尝试了决策标记但
|
||
// 无法解析,调用方把它当作「不推送」(失败安全)。
|
||
func hasAmbiguousDecide(text string) bool {
|
||
if decideMarker.MatchString(text) {
|
||
return false
|
||
}
|
||
for _, p := range decidePrefixes {
|
||
if strings.Contains(text, p) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// matchChannel 返回已转小写的目标渠道词;结论未携带
|
||
// (或携带格式错乱的)渠道标记时返回 ""。格式错乱的渠道
|
||
// 标记降级为默认路由,而不是让整轮失败。
|
||
func matchChannel(text string) string {
|
||
m := channelMarker.FindStringSubmatch(text)
|
||
if m == nil {
|
||
return ""
|
||
}
|
||
return strings.ToLower(m[1])
|
||
}
|
||
|
||
// stripInternalMarkers 移除结构化的决策/渠道/紧急度标记,因此
|
||
// 主动消息正文绝不会把它们带给用户。它处理预期的
|
||
// 带括号形式([我决定:push][渠道:kook][紧急:critical])以及模型可能发出的漂移形式——
|
||
// 裸的 "我决定:push" / "渠道:kook" / "紧急:critical"(无括号)与全角 【我决定:push】——然后
|
||
// 丢弃因此变空的行并压缩剩余部分。它既在
|
||
// parseProactive 内部应用(使消息与记录干净),又作为
|
||
// buildProactiveMessage 里的最后一道守护,因此无论标记如何
|
||
// 被格式化,都无法存活到出站正文里。只有我们的日志/记录可以携带
|
||
// 该标记;面向用户的消息必须是纯温暖内容。
|
||
func stripInternalMarkers(text string) string {
|
||
lines := strings.Split(text, "\n")
|
||
out := make([]string, 0, len(lines))
|
||
for _, ln := range lines {
|
||
ln = stripDecideRe.ReplaceAllString(ln, "")
|
||
ln = stripChannelRe.ReplaceAllString(ln, "")
|
||
ln = stripUrgencyRe.ReplaceAllString(ln, "")
|
||
ln = bareDecideRe.ReplaceAllString(ln, "")
|
||
ln = bareChannelRe.ReplaceAllString(ln, "")
|
||
ln = bareUrgencyRe.ReplaceAllString(ln, "")
|
||
ln = strings.TrimSpace(ln)
|
||
if ln == "" {
|
||
continue
|
||
}
|
||
out = append(out, ln)
|
||
}
|
||
return strings.Join(out, "\n")
|
||
}
|
||
|
||
// looksNone 报告正文是否表示「没什么值得说的」。它既用于
|
||
// 驱动无标记时的默认,也用于对矛盾的 push 标记失败安全。
|
||
func looksNone(text string) bool {
|
||
t := normalizeNone(text)
|
||
if t == "" {
|
||
return false
|
||
}
|
||
for _, p := range nonePhrases {
|
||
if t == p {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// normalizeNone 把正文转小写并去掉空格与标点,好让
|
||
// nonePhrases 在 "无"、"无。"、"Nothing to report." 之类之间干净比较。
|
||
func normalizeNone(text string) string {
|
||
var b strings.Builder
|
||
for _, r := range strings.ToLower(strings.TrimSpace(text)) {
|
||
if unicode.IsSpace(r) || unicode.IsPunct(r) {
|
||
continue
|
||
}
|
||
b.WriteRune(r)
|
||
}
|
||
return b.String()
|
||
}
|