dsh-router-laya 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +176 -0
- package/NOTICE +19 -0
- package/README.md +108 -0
- package/bin/setup.mjs +297 -0
- package/client.js +436 -0
- package/cordis.patch.yml +9 -0
- package/index.js +870 -0
- package/install.mjs +38 -0
- package/package.json +49 -0
- package/service/finetuned_judge.py +324 -0
- package/service/intent_parser.py +1012 -0
- package/service/laya/__init__.py +51 -0
- package/service/laya/agent.py +447 -0
- package/service/laya/common.py +280 -0
- package/service/laya/email.py +90 -0
- package/service/laya/lang.py +324 -0
- package/service/laya/presets.py +187 -0
- package/service/laya/pyproject.toml +38 -0
- package/service/laya/router.py +447 -0
- package/service/laya_router.py +320 -0
- package/service/requirements.lock.txt +27 -0
- package/service/start_router.ps1 +102 -0
- package/service/start_router.sh +144 -0
- package/weights/fetch.mjs +219 -0
- package/weights/manifest.json +36 -0
|
@@ -0,0 +1,1012 @@
|
|
|
1
|
+
"""Phase 0 意图解析器:词典锚点 + 否定作用域 + 约束代数(零训练,零延迟)。
|
|
2
|
+
|
|
3
|
+
设计来源:docs/intent-detection-review.md 三模型综合定稿。
|
|
4
|
+
|
|
5
|
+
三层结构:
|
|
6
|
+
1. 锚点匹配:在文本中找档位词(语义族,不是穷举)
|
|
7
|
+
2. 否定作用域:检查锚点左侧窗口有无否定词
|
|
8
|
+
3. 约束代数:输出 {op, tier, span} 结构化约束
|
|
9
|
+
|
|
10
|
+
输出是**证据**,不是决策。最终档位由 finetuned_judge.compute_tier +
|
|
11
|
+
本模块的约束过滤共同决定。
|
|
12
|
+
|
|
13
|
+
from intent_parser import parse_intent, apply_constraint
|
|
14
|
+
|
|
15
|
+
intent = parse_intent("不要用最高思考,简单做")
|
|
16
|
+
# → {'op': 'exclude', 'tier': 'max', 'span': '不要用最高', 'polarity': 'negate'}
|
|
17
|
+
|
|
18
|
+
final = apply_constraint('high', intent)
|
|
19
|
+
# → 'high'(exclude(max) 不影响 high)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from typing import Dict, List, Optional
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── 锚点词表 ──────────────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
# 每档一个语义族,覆盖常见表达。匹配靠这些词触发,不靠穷举所有说法。
|
|
28
|
+
# 新表达如果 embedding 能落到这些锚点附近,Phase 1 的模型就能泛化。
|
|
29
|
+
|
|
30
|
+
ANCHORS = {
|
|
31
|
+
"max": [
|
|
32
|
+
# ── 原 max 词(v1 保留)──
|
|
33
|
+
"最高思考", "最高强度", "最大思考", "最强思考", "最高effort", "最大effort",
|
|
34
|
+
"拉满", "火力全开", "开到最大", "尽最大努力", "最深度", "最强模型",
|
|
35
|
+
"最拉的", "最顶级", "顶配", "满血",
|
|
36
|
+
"max effort", "max thinking", "think hard", "think deep", "think max",
|
|
37
|
+
"maximum effort", "maximum thinking", "go all out", "full power",
|
|
38
|
+
"深度思考到底", "想透彻", "想到底",
|
|
39
|
+
# ── M2 质量强化族(裁决1:从 high 迁入 max)──
|
|
40
|
+
"认真分析", "认真推敲", "认真推演", "认真想", "认真做", "认真点",
|
|
41
|
+
"认真思考", "认真推理", "认真核对", "认真检查", "认真评估",
|
|
42
|
+
"仔细分析", "仔细推演", "仔细推敲", "仔细想", "仔细检查", "仔细核对",
|
|
43
|
+
"深度思考", "深度分析", "深度推理", "深度模式", "深度调研", "深度剖析",
|
|
44
|
+
"深入思考", "深入分析", "深入调研", "深入剖析", "深入",
|
|
45
|
+
"彻底", "透彻", "严谨", "缜密", "慎重", "深思熟虑", "周密",
|
|
46
|
+
# 想清楚/想明白/推敲:GRAY→按金标 majority 收在 high(过程增量),勿放 max
|
|
47
|
+
# 推演仍归 max(金标「推演一遍」等为 force_max)
|
|
48
|
+
"想透", "推演",
|
|
49
|
+
# think harder → force_high(裁决 Q1 增量词,勿放 max)
|
|
50
|
+
"think deeply", "think carefully", "think hard",
|
|
51
|
+
"deep dive", "go deep", "thoroughly", "thorough",
|
|
52
|
+
# ── 质量/穷尽补漏(§7 金标漏检)──
|
|
53
|
+
# 裸词 careful 会 FP("careful about the details");只留完整形态。
|
|
54
|
+
# 裸「每个假设/把每个假设」会 FP(列出来给客户看);用验证类形态锚定。
|
|
55
|
+
"careful analysis", "a careful analysis",
|
|
56
|
+
"验证一遍", "把每个假设都验证", "都验证一遍",
|
|
57
|
+
"check each assumption", "verify every", "verify each",
|
|
58
|
+
# 泛化:核一遍/每条推导(probe 改写,非金标原句)
|
|
59
|
+
"核一遍", "每条推导", "都核一遍",
|
|
60
|
+
# exclude 目标档位名(否定后 → exclude,非 none)
|
|
61
|
+
"top 档", "top档", "top 档思考",
|
|
62
|
+
"很重的思考", "重的思考模式", "heavy thinking", "heavy thinking mode",
|
|
63
|
+
"deliberate", "be deliberate", "rigorous",
|
|
64
|
+
"deep thinking", "deep reasoning", "deepest thinking",
|
|
65
|
+
"think it through", "think it over", "reason carefully",
|
|
66
|
+
"think this through properly", "think long and hard",
|
|
67
|
+
"airtight", "deep-dive thinking", "deep-dive",
|
|
68
|
+
"deeply", "in depth", "seriously",
|
|
69
|
+
# ── 补充缺口词(三方诊断 P1)──
|
|
70
|
+
"最深思", "最深思考", "深思考", "深想", "极限思考", "全功率", "全速",
|
|
71
|
+
"最猛", "顶格", "顶满", "满档", "满配", "拧到最大", "开到最满",
|
|
72
|
+
"ultrathink", "deepest", "top-tier", "top tier", "brainpower",
|
|
73
|
+
"crank up", "crank it up", "max out", "max it out",
|
|
74
|
+
"ultra think", "ultra-thinking",
|
|
75
|
+
"好好想", "好好分析", "好好做", "好好看", "用心思考", "用心分析",
|
|
76
|
+
"费心思", "多费点脑子", "多费点心思", "细致", "细心", "谨慎",
|
|
77
|
+
"别草率", "别抢答", "别急着答", "慢一点想", "不用急",
|
|
78
|
+
# ── 穷尽/竭尽 ──
|
|
79
|
+
"竭尽全力", "竭尽所能", "不遗余力", "毫无保留", "别偷懒", "别敷衍", "别应付",
|
|
80
|
+
"动真格", "把脑子全用上", "全部马力", "用尽全力",
|
|
81
|
+
"don't hold back", "exhaustive", "pull out all the stops",
|
|
82
|
+
# 中文裸词:需要附近有动作/限定词才触发
|
|
83
|
+
"最高", "最大",
|
|
84
|
+
# 英文裸词
|
|
85
|
+
"max",
|
|
86
|
+
# ── P1 续2:force_max→none 缺口 ──
|
|
87
|
+
"把推理预算", "烧穿显卡", "动用你全部", "全部的推理能力",
|
|
88
|
+
"开满思考", "最好的答案", "最优解", "全部马力",
|
|
89
|
+
"maximum thought", "absolute max", "burn the entire",
|
|
90
|
+
"entire thinking budget", "thinking budget",
|
|
91
|
+
"to the maximum", "up to the maximum", "at maximum depth",
|
|
92
|
+
"maximum depth", "to the top", "crank it to",
|
|
93
|
+
"highest thinking", "highest effort", "highest reasoning",
|
|
94
|
+
"highest thinking level", "highest effort level",
|
|
95
|
+
"highest reasoning effort", "think as hard as",
|
|
96
|
+
"reasoning to maximum", "thinking intensity",
|
|
97
|
+
"run this at the highest", "apply your highest",
|
|
98
|
+
"reasoning effort you support", "at the highest effort",
|
|
99
|
+
"skimp on the reasoning", "skimp",
|
|
100
|
+
"strongest thinking", "strongest reasoning", "top reasoning",
|
|
101
|
+
"the strongest thinking level", "strongest thinking level",
|
|
102
|
+
# 思考预算仅在升档动词同现时算 max(弱锚,见 _WEAK_MAX_ANCHORS)
|
|
103
|
+
"思考预算", "推理预算",
|
|
104
|
+
],
|
|
105
|
+
"high": [
|
|
106
|
+
# ── 显式 high 档位名 ──
|
|
107
|
+
"高思考", "高effort", "high effort", "high thinking",
|
|
108
|
+
"high mode", "high tier", "high-effort",
|
|
109
|
+
"高强度", "高强度思考", "高强度推理", "高推理", "高档", "高一档",
|
|
110
|
+
"high reasoning", "high reasoning effort", "elevated thinking",
|
|
111
|
+
# ── 比较级/增量(Q3决策:调高→force_high,不是max)──
|
|
112
|
+
"调高", "提高", "拧高", "升档", "加档", "加码", "加大",
|
|
113
|
+
"多想想", "多想一步", "多想几层", "多想几步", "多想一点", "多想几遍",
|
|
114
|
+
"多花点时间想", "多花点时间", "多花点脑力", "多留点时间",
|
|
115
|
+
"再深一点", "更用力", "更认真", "更深一层", "深想一层",
|
|
116
|
+
"提升推理", "加大推理", "提高思考",
|
|
117
|
+
"一步一步", "逐项", "层层递进", "一步步",
|
|
118
|
+
"高一点", "高一些", "再高一点", "调高一点", "思考调高",
|
|
119
|
+
"抬一档", "再抬", "档再抬",
|
|
120
|
+
"想清楚", "想明白", "推敲", "反复推敲",
|
|
121
|
+
"较深", "较深的思考", "深一点的思考",
|
|
122
|
+
"多花点思考", "多花点思考时间",
|
|
123
|
+
"think harder", "think more", "think deeper", "think a bit more",
|
|
124
|
+
"heavier reasoning", "more deeply", "step by step",
|
|
125
|
+
"raise the effort", "bump it up", "turn it up",
|
|
126
|
+
# 裸 raise it 会 FP("raise it to the board");只留带宾语形态。
|
|
127
|
+
"bump the effort", "up a notch", "one notch up",
|
|
128
|
+
"想那么深",
|
|
129
|
+
# ── P1 续:force_high→none 缺口 ──
|
|
130
|
+
"serious thought", "deeper reasoning", "stronger reasoning",
|
|
131
|
+
"more effort", "extra effort", "harder reasoning",
|
|
132
|
+
# strongest/top 族归 max(与 highest* 一致),勿放 high
|
|
133
|
+
"stronger analysis", "deeper analysis", "more thorough",
|
|
134
|
+
"layer by layer", "edge cases", "failure modes",
|
|
135
|
+
"reasoning chain", "reason it out", "weigh the alternatives",
|
|
136
|
+
"considered answer", "first instinct", "real thought",
|
|
137
|
+
"dig into", "go over the logic", "verify the logic",
|
|
138
|
+
"not be hasty", "genuine analysis", "escalate the thinking",
|
|
139
|
+
"raise the reasoning", "turn the reasoning level",
|
|
140
|
+
"reasoning budget", "thinking level up", "thinking tier",
|
|
141
|
+
"one notch", "or two", "fuller answer",
|
|
142
|
+
# as planned → inherit 词表(勿放 high)
|
|
143
|
+
"费点心思", "严肃推理", "开到高", "最强的一档",
|
|
144
|
+
"排一遍", "推理链", "逻辑链", "一层一层", "跳着说",
|
|
145
|
+
"跟上你的思路", "每一步", "走完整", "别跳步",
|
|
146
|
+
"事实核对", "反例", "长期影响", "格外谨慎",
|
|
147
|
+
"bump the thinking", "reasoning level", "think through carefully",
|
|
148
|
+
"take your time", "go over", "walk me through",
|
|
149
|
+
"layer by layer", "in depth", "root cause",
|
|
150
|
+
"high",
|
|
151
|
+
# ── P1 续2:force_high→none 缺口 ──
|
|
152
|
+
"think this one through carefully", "this one through carefully",
|
|
153
|
+
"dig into this properly", "before answering", "deeper analysis here",
|
|
154
|
+
"genuine analysis", "failure properly", "surface symptom",
|
|
155
|
+
"analysis layer", "哪个最靠谱", "可能性都排一遍",
|
|
156
|
+
"这题需要仔细", "一步跳步", "推理清楚之后", "跟着你的思路",
|
|
157
|
+
"值得仔细", "哪种方案", "更稳",
|
|
158
|
+
# highest* 族归 max(M1),勿放 high
|
|
159
|
+
],
|
|
160
|
+
"low": [
|
|
161
|
+
# ── 显式 low 档位 ──
|
|
162
|
+
"低思考", "简单想", "快速想", "随便想", "别想太多", "不用想太多",
|
|
163
|
+
"省着点", "省钱", "经济模式", "节约模式", "低成本", "低强度", "低档",
|
|
164
|
+
"最低强度", "最低档", "最省", "经济档", "省点思考", "省算力", "省成本", "省电",
|
|
165
|
+
"low effort", "low thinking", "quick thinking",
|
|
166
|
+
"think less", "think quick", "don't overthink", "dont overthink",
|
|
167
|
+
"别过度", "别太复杂", "简单模式", "快速模式", "简单点",
|
|
168
|
+
"快点回答", "简单回答", "简洁回答", "直接回答", "直接说",
|
|
169
|
+
"秒回", "秒答", "快答", "速回", "快问快答", "快准狠",
|
|
170
|
+
"随便答", "随便说说", "随口", "随手", "浅想", "浅思考",
|
|
171
|
+
"轻描淡写", "凭直觉", "糙一点", "瞎说", "走个过场",
|
|
172
|
+
"别啰嗦", "别费劲", "别动脑子", "别深究", "别上强度", "能简则简",
|
|
173
|
+
# ── 长度/输出要求(Q2决策:→force_low + 记 axis=style)──
|
|
174
|
+
"一行字", "一句话", "一个字", "简短", "简洁", "精炼", "精简",
|
|
175
|
+
"越短越好", "别太长", "只要结论", "直接给结论",
|
|
176
|
+
"别废话", "少废话", "开门见山", "别铺垫",
|
|
177
|
+
"结论放第一句", "分析就免了", "不用解释", "跳过推理",
|
|
178
|
+
"一个词", "十个字以内",
|
|
179
|
+
"one-liner", "one sentence", "keep it short", "tl;dr",
|
|
180
|
+
"brief", "concise", "just the answer", "no preamble",
|
|
181
|
+
"quick answer", "short answer", "one line", "one word",
|
|
182
|
+
"keep it light", "budget mode", "shallow",
|
|
183
|
+
"cut to the chase", "verdict first", "skim", "gist",
|
|
184
|
+
# ── 糊弄/随意 ──
|
|
185
|
+
"糊弄", "随便聊聊", "浅浅", "粗暴",
|
|
186
|
+
# ── P1 续:force_low→none 缺口 ──
|
|
187
|
+
"大概", "大概齐", "差不多", "差不多得了", "别较真", "别太较真",
|
|
188
|
+
"磨叽", "抓紧答", "随性", "别绷着", "赶紧说", "应付一下",
|
|
189
|
+
"烧钱", "便宜跑", "大手大脚", "够用就好", "省算力", "简单过",
|
|
190
|
+
"抠搜", "别浪费算力", "压到最低", "别超支", "yes 或 no",
|
|
191
|
+
"只留结果", "砍掉", "最低", "最省", "最快",
|
|
192
|
+
"minimal", "thinking minimal", "off the cuff", "off the top",
|
|
193
|
+
"quick and dirty", "quick reply", "quick one", "burn brain",
|
|
194
|
+
"as short as", "three words", "headline only", "nothing else",
|
|
195
|
+
"zero walkthrough", "body text", "no body", "token spend",
|
|
196
|
+
"thrifty", "quick pass", "keep it breezy", "breezy",
|
|
197
|
+
"plain answer", "no frills", "fastest", "spitball",
|
|
198
|
+
"gimme the short", "short version", "surface level",
|
|
199
|
+
"thinking light", "low profile", "keep thinking",
|
|
200
|
+
"low",
|
|
201
|
+
# ── P1 续2:force_low→none 缺口 ──
|
|
202
|
+
"浅尝辄止", "直觉回答", "省则省", "能省则省", "效率第一",
|
|
203
|
+
"一分钟内", "超时就算了", "nothing fancy", "no fluff",
|
|
204
|
+
"cut the fluff", "give me the result", "just answer",
|
|
205
|
+
"no body text", "rigor needed", "no rigor", "don't burn",
|
|
206
|
+
"brain cycles", "normal推理", "正常推理",
|
|
207
|
+
"lowest effort", "lowest effort please", "lowest thinking",
|
|
208
|
+
"save the compute", "answer briefly", "briefly",
|
|
209
|
+
"keep it rough", "first pass is fine", "cheap mode",
|
|
210
|
+
"single line", "keep it to a single line",
|
|
211
|
+
"no deep analysis needed", "avoid over-deliberating",
|
|
212
|
+
"省着点花", "把预算省着", "省着点",
|
|
213
|
+
"think too hard about it", "think too much about it",
|
|
214
|
+
"don't think too hard", "dont think too hard",
|
|
215
|
+
# ── §7 中文省钱/英文直给 ──
|
|
216
|
+
"经济实惠", "省着用", "大风刮来",
|
|
217
|
+
"straight and fast", "give it to me straight",
|
|
218
|
+
"cheap and fast", "fast and cheap",
|
|
219
|
+
# ── 泛化改写(probe_a_overfit:勿只收金标字面)──
|
|
220
|
+
"划算", "悠着点", "费很贵", "quick version", "give me the quick",
|
|
221
|
+
# ── 残留漏检补:low 风格词(勿收 max 金标词)──
|
|
222
|
+
"light touch", "bare minimum", "bottom line only",
|
|
223
|
+
"three words", "三个词", "大白话", "别整复杂", "别想复杂",
|
|
224
|
+
"just wing it", "answer casually", "don't sweat", "dont sweat",
|
|
225
|
+
"don't think too much", "dont think too much", "简单答下",
|
|
226
|
+
# 「安排上」已删:裸词把「给我安排上一个深度调研课题」误打成 force_low;
|
|
227
|
+
# 金标「省 token 模式,安排上」仍由「省 token」锚定。
|
|
228
|
+
"别费那", "省 token", "省token",
|
|
229
|
+
"skip the rest", "no deep thought",
|
|
230
|
+
],
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# 裸词(最高/最大/max/high/low)需要附近有动作词或限定词才触发
|
|
234
|
+
# 否则"最大上下文""max函数""high availability"等会被误触发
|
|
235
|
+
_BARE_WORDS = {"最高", "最大", "max", "high", "low"}
|
|
236
|
+
# 弱 max 锚:须有升档动词,否则丢弃(防「思考预算省着点花」→max)
|
|
237
|
+
_WEAK_MAX_WORDS = {"思考预算", "推理预算", "把推理预算"}
|
|
238
|
+
_UPSHIFT_VERBS = [
|
|
239
|
+
"拉满", "开满", "开到", "烧穿", "加大", "顶格", "拧到", "全花", "花掉",
|
|
240
|
+
"全部", "最大", "最高", "全力", "竭尽", "用尽", "开到最大",
|
|
241
|
+
"crank", "maximum", "max out", "go full", "burn", "all out",
|
|
242
|
+
]
|
|
243
|
+
# 动作词:中文 + 英文(P1 修复:原来只有中文,英文裸词必被过滤)
|
|
244
|
+
_ACTION_WORDS = [
|
|
245
|
+
"用", "开", "走", "给", "来", "做", "搞", "跑", "想", "思考",
|
|
246
|
+
"拉", "开到", "调到", "设为", "设成", "调成", "提到", "要", "上", "加",
|
|
247
|
+
"选择", "切换", "档", "模式", "就免", "免了", "处理",
|
|
248
|
+
# 注意:不包含"上"("上下文"会误触发)—— 但上面加了"上"是因为"要上"是常见搭配
|
|
249
|
+
# 英文动作词(P1 补)
|
|
250
|
+
"use", "go", "run", "set", "give", "turn", "crank", "keep", "make",
|
|
251
|
+
"think", "reason", "answer", "put", "switch", "skip", "never",
|
|
252
|
+
]
|
|
253
|
+
# 英文档位名词(和裸词组合才触发,如 "max thinking" / "high effort")
|
|
254
|
+
_ACTION_EN_RE = re.compile(
|
|
255
|
+
r"\b(?:use|go|run|set|give|turn|crank|keep|make|think|reason|answer|put|switch|skip|never)\b"
|
|
256
|
+
r"|\b(?:thinking|reasoning|effort|mode|tier|depth|brainpower|level|setting)\b",
|
|
257
|
+
re.IGNORECASE,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# 否定词:出现在锚点词左侧窗口内 → 该锚点的含义翻转
|
|
261
|
+
NEGATIONS = [
|
|
262
|
+
"不要", "别", "不用", "无需", "不必", "不需", "非", "莫", "勿",
|
|
263
|
+
"不使用", "别用", "不用要", "不采用", "不走", "跳过", "免了", "算了",
|
|
264
|
+
"关掉", "关闭", "去掉", "禁用", "禁止", "别上", "别给",
|
|
265
|
+
"don't", "dont", "no", "never", "not", "without", "skip", "avoid",
|
|
266
|
+
"turn off", "disable", "don't use", "do not", "is off",
|
|
267
|
+
"否决", "排除", "搁置", "缓一缓", "先不", "暂不",
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
# 后置否定只认"打发/作罢"类:锚点右侧的通用否定(别/no)常属于下一小句
|
|
271
|
+
# 的词汇化下调("随便说说,别深究"),不能翻转前一锚点。
|
|
272
|
+
_DISMISSIVE_RIGHT = [
|
|
273
|
+
"免了", "算了", "不用了", "不需要了", "就免了", "这次免了",
|
|
274
|
+
"no need", "not this time", "skip it", "off the table",
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
# 限定词:出现在锚点词左侧 → 强化该锚点("只用最高" = force(max))
|
|
278
|
+
RESTRICTIONS = ["只用", "仅用", "只要", "只给", "只开", "只走", "就要", "必须用", "得用", "来个", "给我"]
|
|
279
|
+
|
|
280
|
+
# 否定作用域窗口(字符数):锚点词前面多少个字内算否定作用域
|
|
281
|
+
# P2:从 12 提到 24——英文 "never go full max" / "don't set thinking to low" 的否定词在更左侧
|
|
282
|
+
SCOPE_WINDOW = 24
|
|
283
|
+
|
|
284
|
+
# 编译正则(锚点词按长度降序,优先匹配长的)
|
|
285
|
+
_ANCHOR_RES = {
|
|
286
|
+
tier: sorted([(len(a), a) for a in words], key=lambda x: -x[0])
|
|
287
|
+
for tier, words in ANCHORS.items()
|
|
288
|
+
}
|
|
289
|
+
_NEG_RES = [re.compile(re.escape(w), re.IGNORECASE) for w in NEGATIONS]
|
|
290
|
+
_RESTR_RES = [re.compile(re.escape(w), re.IGNORECASE) for w in RESTRICTIONS]
|
|
291
|
+
|
|
292
|
+
# 纯 ASCII 锚点要求 ASCII 词边界:
|
|
293
|
+
# 不能用 \b——Python 里汉字也是 \w,导致 "别用max" 的 max 前无边界被过滤。
|
|
294
|
+
# 用 (?<![A-Za-z0-9])…(?![A-Za-z0-9]):highest 不会切出 high,别用max 能匹配 max。
|
|
295
|
+
_ASCII_ANCHOR_RES = {
|
|
296
|
+
tier: [
|
|
297
|
+
(ln, w, re.compile(
|
|
298
|
+
r"(?<![A-Za-z0-9])" + re.escape(w) + r"(?![A-Za-z0-9])",
|
|
299
|
+
re.IGNORECASE,
|
|
300
|
+
))
|
|
301
|
+
if w.isascii() else (ln, w, None)
|
|
302
|
+
for ln, w in words
|
|
303
|
+
]
|
|
304
|
+
for tier, words in _ANCHOR_RES.items()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def find_anchors(text: str) -> List[Dict]:
|
|
309
|
+
"""在文本中找所有锚点词,返回 [{tier, word, start, end}],按出现顺序。
|
|
310
|
+
|
|
311
|
+
英文(纯 ASCII)锚点要求词边界;中文子串匹配(最长优先由排序保证)。
|
|
312
|
+
"""
|
|
313
|
+
hits = []
|
|
314
|
+
lower = text.lower()
|
|
315
|
+
for tier, word_list in _ASCII_ANCHOR_RES.items():
|
|
316
|
+
for _, word, en_re in word_list:
|
|
317
|
+
search_word = word.lower()
|
|
318
|
+
if en_re is not None:
|
|
319
|
+
for m in en_re.finditer(text):
|
|
320
|
+
hits.append({
|
|
321
|
+
"tier": tier,
|
|
322
|
+
"word": m.group(0),
|
|
323
|
+
"start": m.start(),
|
|
324
|
+
"end": m.end(),
|
|
325
|
+
})
|
|
326
|
+
continue
|
|
327
|
+
start = 0
|
|
328
|
+
while True:
|
|
329
|
+
idx = lower.find(search_word, start)
|
|
330
|
+
if idx == -1:
|
|
331
|
+
break
|
|
332
|
+
hits.append({
|
|
333
|
+
"tier": tier,
|
|
334
|
+
"word": text[idx:idx + len(word)],
|
|
335
|
+
"start": idx,
|
|
336
|
+
"end": idx + len(word),
|
|
337
|
+
})
|
|
338
|
+
start = idx + 1
|
|
339
|
+
# 同位置取更长;部分重叠一律 **leftmost 优先**(不得用更晚起点的更长锚替换)
|
|
340
|
+
# 修:「最高强度思考」被 high 族「高强度思考」@start+1 夺锚 → force_high
|
|
341
|
+
hits.sort(key=lambda h: (h["start"], -(h["end"] - h["start"])))
|
|
342
|
+
filtered = []
|
|
343
|
+
for h in hits:
|
|
344
|
+
if filtered and h["start"] < filtered[-1]["end"]:
|
|
345
|
+
# 与上一锚重叠:保留 leftmost(同 start 已按更长优先排序)
|
|
346
|
+
continue
|
|
347
|
+
filtered.append(h)
|
|
348
|
+
return filtered
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
_DISMISSIVE_RIGHT_RES = [re.compile(re.escape(w), re.IGNORECASE) for w in _DISMISSIVE_RIGHT]
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def detect_negation(text: str, anchor_start: int, anchor_end: Optional[int] = None) -> Optional[str]:
|
|
355
|
+
"""检查锚点词左侧(或右侧)SCOPE_WINDOW 个字符内是否有否定词。
|
|
356
|
+
|
|
357
|
+
左侧:通用否定词窗口。
|
|
358
|
+
右侧:只认打发类(免了/算了)——通用右窗否定多半属于下一小句。
|
|
359
|
+
返回匹配到的否定词或 None。
|
|
360
|
+
"""
|
|
361
|
+
# 左侧窗口
|
|
362
|
+
window = text[max(0, anchor_start - SCOPE_WINDOW):anchor_start]
|
|
363
|
+
for neg_re in _NEG_RES:
|
|
364
|
+
m = neg_re.search(window)
|
|
365
|
+
if m:
|
|
366
|
+
if m.end() <= len(window):
|
|
367
|
+
return m.group()
|
|
368
|
+
# 右侧窗口(后置打发否定:"深度思考这次免了" / "max档就免了")
|
|
369
|
+
if anchor_end is not None:
|
|
370
|
+
right = text[anchor_end:anchor_end + SCOPE_WINDOW]
|
|
371
|
+
for neg_re in _DISMISSIVE_RIGHT_RES:
|
|
372
|
+
m = neg_re.search(right)
|
|
373
|
+
if m:
|
|
374
|
+
return m.group()
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# 否定二分法(裁决5 / P2):
|
|
379
|
+
# 否定「程度/努力」短语 → force_low("不用深度思考" = 省着点)
|
|
380
|
+
# 否定「档位名词」 → exclude("不要用最高" = 排除 max 档)
|
|
381
|
+
_DEGREE_WORDS = re.compile(
|
|
382
|
+
r"深度|认真|仔细|费劲|费心|费力|思考|推理| effort| thinking| reasoning|deep|hard|careful",
|
|
383
|
+
re.IGNORECASE,
|
|
384
|
+
)
|
|
385
|
+
_TIER_NOUNS = re.compile(
|
|
386
|
+
r"最高|最高档|最高思考|max|ultrathink|拉满|火力全开|high|low|高档|低档|一档"
|
|
387
|
+
r"|高思考|高effort|high effort|high thinking|think\s*hard|think\s*deep"
|
|
388
|
+
r"|深度思考模式|深度模式|low effort|low thinking|最低档|最低思考",
|
|
389
|
+
re.IGNORECASE,
|
|
390
|
+
)
|
|
391
|
+
# 锚点本身即档位名(被否定对象是 mode/tier 标识,不是程度短语)
|
|
392
|
+
# 尽量与 ANCHORS 中的模式名对齐;单独手写仅作兜底
|
|
393
|
+
_ANCHOR_IS_TIER = re.compile(
|
|
394
|
+
r"最高|最大|拉满|火力全开|满血|顶格|顶满|ultrathink|max|think\s*hard|think\s*deep"
|
|
395
|
+
r"|高思考|高effort|high|low|高档|低档|最低档|经济档|高强度|最强思考|最猛|顶格"
|
|
396
|
+
r"|深度思考|深度模式|深度推理|深思考|深入思考|开满|开到最大"
|
|
397
|
+
r"|full max|highest|lowest|thinking level|effort level|reasoning effort"
|
|
398
|
+
r"|minimal thinking|lazy mode|economy mode|to the top|to the max"
|
|
399
|
+
r"|up to the maximum|at the highest"
|
|
400
|
+
r"|top\s*档|很重的思考|重的思考模式|heavy thinking",
|
|
401
|
+
re.IGNORECASE,
|
|
402
|
+
)
|
|
403
|
+
# 「别省/别偷懒/别敷衍」= 升档诉求(转换表,不是 exclude 也不是 force_low)
|
|
404
|
+
_UPSHIFT_NEG = re.compile(
|
|
405
|
+
r"别省|不要省|不用省|别偷懒|别敷衍|别糊弄|别应付|别摸鱼"
|
|
406
|
+
r"|don't skimp|do not skimp|don't hold back|no skimping|别省.*预算|省着点花",
|
|
407
|
+
re.IGNORECASE,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# P4 替代档:exclude 线索 + 显式中间/下调替代 → force(替代)
|
|
411
|
+
# 「简单做/随便答」仍走 exclude(冻结回归例),不在此列
|
|
412
|
+
# 词形收窄:裸「默认」会 FP(「接口的默认值先不改」);只认「默认档」等替代档表达。
|
|
413
|
+
# 「常规即可」是金标 force_high 正例,须保留小句内常规+补语形态。
|
|
414
|
+
_H_SUB_HIGH = re.compile(
|
|
415
|
+
r"正常(?:答|想|来|回答|处理)"
|
|
416
|
+
r"|常规(?:即可|就行|答|想|来|回答|处理)"
|
|
417
|
+
r"|默认档|常规档|标准档|普通档"
|
|
418
|
+
r"|保持不变|照常"
|
|
419
|
+
r"|\bas usual\b|\bnormally\b|keep it normal|keep it standard"
|
|
420
|
+
r"|(?:default|standard) (?:mode|tier|setting|answer|reply|one)",
|
|
421
|
+
re.IGNORECASE,
|
|
422
|
+
)
|
|
423
|
+
_H_SUB_LOW = re.compile(
|
|
424
|
+
r"一句话|一行字|简短|短答|直接答|直给"
|
|
425
|
+
r"|\bone sentence\b|\bone-liner\b|keep it short|just answer",
|
|
426
|
+
re.IGNORECASE,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def negation_to_intent(neg_word: str, anchor_tier: str, text: str, anchor_start: int, anchor_end: int) -> Dict:
|
|
431
|
+
"""否定二分法:否定程度→force_low,否定档位→exclude;别省/别偷懒→force_max。
|
|
432
|
+
|
|
433
|
+
优先级(裁决 spec §2 R2 / C-3 + 用户拍板 C1):
|
|
434
|
+
0. 别省/别偷懒/别敷衍 → force_max(升档,不是 exclude/low)
|
|
435
|
+
1. 被否定锚点是档位/模式名 → exclude
|
|
436
|
+
2. 否定低档行为 → force_low
|
|
437
|
+
3. 仅程度短语 → force_low
|
|
438
|
+
4. 默认 → exclude
|
|
439
|
+
"""
|
|
440
|
+
anchor_word = text[anchor_start:anchor_end]
|
|
441
|
+
left = text[max(0, anchor_start - SCOPE_WINDOW):anchor_start]
|
|
442
|
+
right = text[anchor_end:anchor_end + SCOPE_WINDOW]
|
|
443
|
+
phrase = left + anchor_word + right
|
|
444
|
+
span = text[max(0, anchor_start - SCOPE_WINDOW):anchor_end]
|
|
445
|
+
|
|
446
|
+
def _exclude():
|
|
447
|
+
return {
|
|
448
|
+
"op": "exclude", "tier": anchor_tier,
|
|
449
|
+
"span": span, "polarity": "negate", "neg_word": neg_word,
|
|
450
|
+
"stage": "anchor", "void_reason": None,
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
def _force_low():
|
|
454
|
+
return {
|
|
455
|
+
"op": "force", "tier": "low",
|
|
456
|
+
"span": span, "polarity": "negate", "neg_word": neg_word,
|
|
457
|
+
"anchor": anchor_word, "axis": "style",
|
|
458
|
+
"stage": "anchor", "void_reason": None,
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
def _force_max():
|
|
462
|
+
return {
|
|
463
|
+
"op": "force", "tier": "max",
|
|
464
|
+
"span": span, "polarity": "negate", "neg_word": neg_word,
|
|
465
|
+
"anchor": anchor_word, "conversion": "upshift",
|
|
466
|
+
"stage": "anchor", "void_reason": None,
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
# 0) 升档转换(用户拍板 C1):别省那点预算 / don't skimp / 别偷懒
|
|
470
|
+
if _UPSHIFT_NEG.search(phrase):
|
|
471
|
+
return _force_max()
|
|
472
|
+
|
|
473
|
+
# 1) 档位/模式名被否定 → exclude
|
|
474
|
+
if _ANCHOR_IS_TIER.search(anchor_word) or _TIER_NOUNS.search(phrase):
|
|
475
|
+
return _exclude()
|
|
476
|
+
|
|
477
|
+
# 2) 否定低档行为 → force_low
|
|
478
|
+
if anchor_tier == "low":
|
|
479
|
+
return _force_low()
|
|
480
|
+
|
|
481
|
+
# 3) 否定程度词 → force_low
|
|
482
|
+
if _DEGREE_WORDS.search(phrase):
|
|
483
|
+
return _force_low()
|
|
484
|
+
|
|
485
|
+
# 4) 默认 → exclude
|
|
486
|
+
return _exclude()
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def detect_restriction(text: str, anchor_start: int) -> Optional[str]:
|
|
490
|
+
"""检查锚点词左侧是否有限定词(只用/仅用等)。"""
|
|
491
|
+
window = text[max(0, anchor_start - SCOPE_WINDOW):anchor_start]
|
|
492
|
+
for restr_re in _RESTR_RES:
|
|
493
|
+
m = restr_re.search(window)
|
|
494
|
+
if m:
|
|
495
|
+
if m.end() <= len(window):
|
|
496
|
+
return m.group()
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# ── 语境闸门(v2 新增):命中即作废,优先级最高 ──────────────────────────────────────────────────
|
|
501
|
+
# v1 只在 spec 里写了这些规则,代码没实现——这是 Phase 0 基线只有 39.3% 的主要原因之一。
|
|
502
|
+
|
|
503
|
+
# 条件词:档位词落在条件从句(前/后/同句)→ none(含 "use max if …" 后置条件)
|
|
504
|
+
_COND_WORDS = re.compile(
|
|
505
|
+
r"(?:如果|要是|假如|若|万一|实在不行|的话|必要时|除非|需要时|遇到[^,。]{0,12}就|当[^,。]{0,16}时[^,。]{0,8}才|当[^,。]{0,16}时)"
|
|
506
|
+
r"|(?:\bif\b|\bunless\b|in case |whenever |as needed )",
|
|
507
|
+
re.IGNORECASE,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# 疑问标记:档位词在疑问句中 → none(祈使式疑问除外,Q4)
|
|
511
|
+
# 改为**同小句**判定,废除 ±5 字符邻接(「什么时候该用高思考」间距 6 曾漏拦)
|
|
512
|
+
_QUESTION_WORDS = re.compile(
|
|
513
|
+
r"[??]"
|
|
514
|
+
r"|(?:要不要|是不是|能不能|可不可以|该不该|怎么会|为什么|会不会|值不值|有什么区别|怎么判断|怎么选"
|
|
515
|
+
r"|有没有|是否有|用不用|需不需要"
|
|
516
|
+
r"|什么时候|何时|什么是|什么意思|为何|怎样|哪个更|哪种更|怎么决定|如何决定|如何选|怎么用|是啥)"
|
|
517
|
+
r"|\b(?:whether|should|does|can you|could you|how do|how does|why do|why does|which one|when should|when is|what does|what is|how is|what does .* change|is there|are there)\b"
|
|
518
|
+
r"|^\s*(?:is|are|do|does|can|could|should|would|what|which|when|why|how)\b",
|
|
519
|
+
re.IGNORECASE | re.MULTILINE,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
_CLAUSE_SPLIT_RE = re.compile(r"[,。!?、;;,.!?]|(?:\s{2,})")
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _clause_span(text: str, pos: int) -> tuple:
|
|
526
|
+
"""返回 pos 所在小句的 [start, end)。"""
|
|
527
|
+
starts = [0] + [m.end() for m in _CLAUSE_SPLIT_RE.finditer(text)]
|
|
528
|
+
ends = [m.start() for m in _CLAUSE_SPLIT_RE.finditer(text)] + [len(text)]
|
|
529
|
+
for s, e in zip(starts, ends):
|
|
530
|
+
if s <= pos < e or (s <= pos <= e and s < e):
|
|
531
|
+
if s <= pos <= e:
|
|
532
|
+
return s, e
|
|
533
|
+
return 0, len(text)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _same_clause(text: str, p1: int, p2: int) -> bool:
|
|
537
|
+
s1, e1 = _clause_span(text, p1)
|
|
538
|
+
s2, e2 = _clause_span(text, p2)
|
|
539
|
+
return not (e1 <= s2 or e2 <= s1)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _clause_spans(text: str) -> List[tuple]:
|
|
543
|
+
"""按小句分隔符切出全部 [start, end)(不含分隔符本身)。"""
|
|
544
|
+
spans = []
|
|
545
|
+
last = 0
|
|
546
|
+
for m in _CLAUSE_SPLIT_RE.finditer(text):
|
|
547
|
+
if m.start() > last:
|
|
548
|
+
spans.append((last, m.start()))
|
|
549
|
+
last = m.end()
|
|
550
|
+
if last < len(text):
|
|
551
|
+
spans.append((last, len(text)))
|
|
552
|
+
return spans or [(0, len(text))]
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _adj_clause_region(text: str, anchor_start: int) -> str:
|
|
556
|
+
"""exclude 锚点所在小句 + 左右邻接小句(P4 替代档作用域)。"""
|
|
557
|
+
spans = _clause_spans(text)
|
|
558
|
+
idx = 0
|
|
559
|
+
for i, (s, e) in enumerate(spans):
|
|
560
|
+
if s <= anchor_start < e:
|
|
561
|
+
idx = i
|
|
562
|
+
break
|
|
563
|
+
else:
|
|
564
|
+
idx = len(spans) - 1
|
|
565
|
+
lo = spans[max(0, idx - 1)][0]
|
|
566
|
+
hi = spans[min(len(spans) - 1, idx + 1)][1]
|
|
567
|
+
return text[lo:hi]
|
|
568
|
+
|
|
569
|
+
# 祈使式疑问(Q4 决策:不落入疑问句拦截)——"能不能认真点"语义是"请认真点"
|
|
570
|
+
_IMPERATIVE_QUESTION = re.compile(
|
|
571
|
+
r"能不能.{0,6}(?:认真|仔细|深度|好好|想|分析)"
|
|
572
|
+
r"|(?:can you|could you|please|would you).{0,10}(?:think|analy[sz]e|carefully|deeply|hard)",
|
|
573
|
+
re.IGNORECASE,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# 引用/转述标记
|
|
577
|
+
_QUOTE_WORDS = re.compile(
|
|
578
|
+
r"同事说|听说|文档里?写|文档里?说|上一?轮|刚才|据说|他说|她?让我|你(?:之前|刚才)说"
|
|
579
|
+
r"|(?:says |said |told me |told |someone (?:said|told)|in the docs|config says|earlier|previous turn)",
|
|
580
|
+
re.IGNORECASE,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# 元讨论标记(讨论路由系统本身)
|
|
584
|
+
_META_WORDS = re.compile(
|
|
585
|
+
r"Laya|路由器|档位机制|怎么判断|规则怎么写|词典|阈值|开关控制|开关|值不值|花多少"
|
|
586
|
+
r"|是什么意思|怎么用|最佳实践|这条分支|用户要求|actually change"
|
|
587
|
+
r"|add a flag|flag to control|whether [^,]{0,40} is on"
|
|
588
|
+
r"|(?:how does|how do you decide|rule of thumb|effort routing)",
|
|
589
|
+
re.IGNORECASE,
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
# 技术名词黑名单(形容词/代码用法,裁决6)
|
|
593
|
+
# 双侧检查:锚点前后的「min/max/用法」都算技术语境
|
|
594
|
+
_TECH_NOUNS = re.compile(
|
|
595
|
+
r"^\s*(?:上下文|长度|可用性|延迟|堆|函数|变量名|索引|像素|并发|链路|窗口|最大值|最小值|用法"
|
|
596
|
+
r"|\bmin\b|\bmax\b"
|
|
597
|
+
r"|availability|latency|heap|function|index|throughput|max\(\)|Math\.max|max-heap"
|
|
598
|
+
r"|deep learning|深度学习|deep copy|深度优先|high-performance|高性能"
|
|
599
|
+
r"|low\s*/\s*high|high\s*/\s*low)"
|
|
600
|
+
r"|(?:availability|latency|heap|throughput|max\(\)|Math\.max|max-heap"
|
|
601
|
+
r"|deep learning|深度学习|deep copy|深度优先|high-performance|高性能"
|
|
602
|
+
r"|low\s*/\s*high|high\s*/\s*low)",
|
|
603
|
+
re.IGNORECASE,
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
# 代码环境标记(此前定义未接线 —— P0 接入 _context_gate)
|
|
607
|
+
_CODE_MARKERS = re.compile(
|
|
608
|
+
r"`[^`]+`|```[\s\S]*?```|\.[a-z]{1,5}\b|--[a-z-]+|=>|::"
|
|
609
|
+
r"|(?:eval|DEBUG|flag|parameter|variable|field|attribute)\b",
|
|
610
|
+
re.IGNORECASE,
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _context_gate(text: str) -> Optional[str]:
|
|
615
|
+
"""语境闸门:只有当**档位词本身**处于被讨论/被否定的语境时才作废。
|
|
616
|
+
|
|
617
|
+
关键修正:不能因为句子里有疑问词/条件词就作废——
|
|
618
|
+
"请认真思考这个问题" 虽然有"问题"但档位词"认真思考"是指令不是被讨论对象。
|
|
619
|
+
"""
|
|
620
|
+
# 找到所有锚点词的位置
|
|
621
|
+
anchor_positions = []
|
|
622
|
+
for tier, words in ANCHORS.items():
|
|
623
|
+
for w in words:
|
|
624
|
+
idx = text.lower().find(w.lower())
|
|
625
|
+
if idx >= 0:
|
|
626
|
+
anchor_positions.append((tier, w, idx, idx + len(w)))
|
|
627
|
+
if not anchor_positions:
|
|
628
|
+
return None # 无档位词,不需要闸门
|
|
629
|
+
|
|
630
|
+
# 祈使式疑问豁免(Q4决策)
|
|
631
|
+
is_imperative_q = bool(_IMPERATIVE_QUESTION.search(text))
|
|
632
|
+
|
|
633
|
+
# ── 疑问句:疑问词与档位锚点**同小句**即作废(废除 ±5 字符) ──
|
|
634
|
+
if not is_imperative_q:
|
|
635
|
+
qm = _QUESTION_WORDS.search(text)
|
|
636
|
+
if qm:
|
|
637
|
+
for tier, w, a_start, a_end in anchor_positions:
|
|
638
|
+
if _same_clause(text, qm.start(), a_start) or _same_clause(text, qm.start(), a_end - 1):
|
|
639
|
+
return "question_adjacent"
|
|
640
|
+
|
|
641
|
+
# ── 条件句:条件与锚点同句(前/后皆可,含 "use max if …") ──
|
|
642
|
+
cond = _COND_WORDS.search(text)
|
|
643
|
+
if cond:
|
|
644
|
+
for tier, w, a_start, a_end in anchor_positions:
|
|
645
|
+
# 前置条件(原逻辑)
|
|
646
|
+
if cond.start() < a_start:
|
|
647
|
+
seg = text[cond.end():a_start]
|
|
648
|
+
if "。" not in seg and "." not in seg and "!" not in seg:
|
|
649
|
+
if _same_clause(text, cond.start(), a_start) or re.fullmatch(r"[\s,;:,、]*|[^。.!!]{0,40}", seg):
|
|
650
|
+
if re.fullmatch(r"[^。.!!]*", seg) and len(seg) < 80:
|
|
651
|
+
return "conditional"
|
|
652
|
+
if _same_clause(text, cond.start(), a_start):
|
|
653
|
+
return "conditional"
|
|
654
|
+
# 后置条件:锚点在前、if/如果 紧随其后同小句("use max if the algorithm…")
|
|
655
|
+
elif cond.start() >= a_end:
|
|
656
|
+
seg = text[a_end:cond.start()]
|
|
657
|
+
if _same_clause(text, a_end - 1, cond.start()) and len(seg) < 40:
|
|
658
|
+
return "conditional"
|
|
659
|
+
|
|
660
|
+
# ── 元讨论:元词与锚点同小句 ──
|
|
661
|
+
meta = _META_WORDS.search(text)
|
|
662
|
+
if meta:
|
|
663
|
+
for tier, w, a_start, a_end in anchor_positions:
|
|
664
|
+
if _same_clause(text, meta.start(), a_start) or abs(meta.start() - a_start) <= 10:
|
|
665
|
+
return "meta_discussion"
|
|
666
|
+
|
|
667
|
+
# ── 引用:引用标记在锚点前且同小句 ──
|
|
668
|
+
quote = _QUOTE_WORDS.search(text)
|
|
669
|
+
if quote:
|
|
670
|
+
for tier, w, a_start, a_end in anchor_positions:
|
|
671
|
+
if quote.start() < a_start and _same_clause(text, quote.start(), a_start):
|
|
672
|
+
seg = text[quote.end():a_start]
|
|
673
|
+
if "。" not in seg and "." not in seg:
|
|
674
|
+
return "quotation"
|
|
675
|
+
|
|
676
|
+
# ── 代码环境:反引号/代码块,或 _CODE_MARKERS 命中锚点邻域 ──
|
|
677
|
+
for tier, w, a_start, a_end in anchor_positions:
|
|
678
|
+
if re.search(rf"`[^`]*{re.escape(w)}[^`]*`", text, re.IGNORECASE) or \
|
|
679
|
+
re.search(rf"```[\s\S]*?{re.escape(w)}[\s\S]*?```", text, re.IGNORECASE):
|
|
680
|
+
return "code_context"
|
|
681
|
+
# 接线 _CODE_MARKERS:锚点左右 8 字内有代码记号
|
|
682
|
+
region = text[max(0, a_start - 8):min(len(text), a_end + 8)]
|
|
683
|
+
if _CODE_MARKERS.search(region):
|
|
684
|
+
# 排除普通句号域名误伤:`.` 后必须跟 1-5 字母才算扩展名
|
|
685
|
+
cm = _CODE_MARKERS.search(region)
|
|
686
|
+
if cm and not (cm.group() == "." and not re.search(r"\.[a-z]{1,5}\b", region, re.I)):
|
|
687
|
+
if cm.group() not in (".",):
|
|
688
|
+
return "code_context"
|
|
689
|
+
|
|
690
|
+
# ── 技术名词:锚点**前或后**紧跟技术名词(含 min/max/latency/low-high 用法) ──
|
|
691
|
+
for tier, w, a_start, a_end in anchor_positions:
|
|
692
|
+
after = text[a_end:a_end + 12]
|
|
693
|
+
before = text[max(0, a_start - 12):a_start]
|
|
694
|
+
# 允许 low-latency / low latency / low/high 等连写与间隔
|
|
695
|
+
if _TECH_NOUNS.match(after) or _TECH_NOUNS.match(after.lstrip("-–—/ \t")):
|
|
696
|
+
return "adjective_usage"
|
|
697
|
+
if re.search(r"low\s*/\s*high|high\s*/\s*low", before + w + after, re.I):
|
|
698
|
+
if w.lower() in ("low", "high", "最低", "最高", "低档", "高档"):
|
|
699
|
+
return "adjective_usage"
|
|
700
|
+
# 「min和max的用法」:max 后紧跟 的用法 / 前有 min
|
|
701
|
+
if re.search(r"的用法$|用法", after) or re.search(r"\bmin\b$", before, re.I):
|
|
702
|
+
return "adjective_usage"
|
|
703
|
+
if re.search(r"(?:min|max)\s*(?:和|与|及|,|/)?\s*$", before, re.I) and w.lower() in ("max", "min", "最高", "最大"):
|
|
704
|
+
# 「min和max」中 max 前是 min
|
|
705
|
+
if re.search(r"\bmin\b", before, re.I) or "min" in before.lower():
|
|
706
|
+
return "adjective_usage"
|
|
707
|
+
|
|
708
|
+
return None
|
|
709
|
+
# 场景一:上一轮失败/卡顿后用户说"继续",Laya 会判成 low(没内容),但应该保持原档位。
|
|
710
|
+
# 场景二:上一轮给出了方案,用户回一句"好的/同意/没问题/按你的来"就是要开始执行。此时任务难度
|
|
711
|
+
# 没有任何变化,只是从「讨论」变成「执行」,重判同样会因为消息无内容而掉到 low。
|
|
712
|
+
# 匹配规则:文本先按标点切词,**每个词**都必须在词表里才认继承(见 _is_all_inherit_tokens)。
|
|
713
|
+
# 这不是保守过头:带内容的句子一定切不完——"继续优化这个函数" 整体不是词,
|
|
714
|
+
# "start the server" 里的 the/server 不在词表——所以那些句子继续交给 Laya。
|
|
715
|
+
# 好处是逗号连写的批准语也能认:"yes, go ahead"、"好的,开始吧"。
|
|
716
|
+
INHERIT_WORDS = [
|
|
717
|
+
# 继续 / 下一步(中)
|
|
718
|
+
"继续", "接着", "然后呢", "下一步", "继续吧", "继续来", "继续做",
|
|
719
|
+
"然后", "接下来", "再继续", "接着来",
|
|
720
|
+
"继续推进", "继续搞", "继续弄", "继续整", "继续说", "继续干",
|
|
721
|
+
"接着整", "接着弄", "接着说", "接着推进", "接着做",
|
|
722
|
+
"往下说", "往下走", "往下讲", "往下弄", "往下写", "往下推进", "再往下",
|
|
723
|
+
"走起", "开干", "干起来", "下一步呢",
|
|
724
|
+
# 批准执行(中):方案已给出,用户点头
|
|
725
|
+
"好的", "好吧", "好", "行", "可以", "同意", "赞成", "没问题", "没毛病",
|
|
726
|
+
"按你的来", "按你说的来", "按你说的办", "按你说的", "按这个来", "按这个办",
|
|
727
|
+
"听你的", "就这么办", "这么办", "就这么定", "就按这个",
|
|
728
|
+
"开始吧", "开始", "执行吧", "干吧", "搞吧", "来吧", "上吧", "动手吧",
|
|
729
|
+
"收到", "照办", "照做", "明白", "明白了", "了解", "了解了", "懂了", "知晓了",
|
|
730
|
+
"好嘞", "好哦", "好呀", "好哒", "好滴", "嗯好", "嗯呢", "嗯呐", "嗯嗯",
|
|
731
|
+
"成", "成吧", "中", "中啊", "中嘞", "妥了", "妥妥的", "准了",
|
|
732
|
+
"行吧", "行嘞", "行行行", "可以可以", "批准了", "去办吧",
|
|
733
|
+
# 继续(英)
|
|
734
|
+
"go on", "continue", "keep going", "carry on", "next", "then what", "go ahead",
|
|
735
|
+
"keep at it", "keep it going", "keep rolling", "keep moving", "keep on going",
|
|
736
|
+
"continue on", "continue please", "onward", "on you go",
|
|
737
|
+
# 批准执行(英)
|
|
738
|
+
"ok", "okay", "yes", "yeah", "yep", "yup", "sure", "sure thing", "yes please",
|
|
739
|
+
"no problem", "no prob", "no promblem", "no worries", "np",
|
|
740
|
+
"agreed", "agree", "approved", "sounds good", "lgtm", "looks good",
|
|
741
|
+
"do it", "go for it", "let's go", "lets go", "proceed", "start",
|
|
742
|
+
"alright", "fine", "cool", "roger", "got it", "understood", "noted",
|
|
743
|
+
"copy that", "that works", "works for me", "that's good", "all good",
|
|
744
|
+
"ship it", "make it so", "let's roll", "green light", "roger that",
|
|
745
|
+
"righto", "mmhm", "sure sure",
|
|
746
|
+
# ── P1 续:inherit→none 缺口 ──
|
|
747
|
+
"保持", "继续保持", "开工", "执行", "推进", "下去", "走", "定",
|
|
748
|
+
"就这样", "就按这个走", "按这个走", "按这个推进", "得嘞", "说下去",
|
|
749
|
+
"继续哈", "没问题的", "可以的", "可以哈", "嗯行", "好嘞,走",
|
|
750
|
+
"就按这个", "按你说的做", "照你说的做", "同意执行", "那就上",
|
|
751
|
+
"那就按你说的来", "按你的思路走", "按刚才说的办", "按这个方案推进",
|
|
752
|
+
"sounds good", "as planned", "please continue", "with that",
|
|
753
|
+
"go ahead with", "from where you left", "left off",
|
|
754
|
+
"do that", "fine by me", "sounds right", "approved",
|
|
755
|
+
"you're good", "good to go", "go right ahead", "make it happen",
|
|
756
|
+
"ship it then", "my go-ahead", "sounds fine", "that's fine",
|
|
757
|
+
"affirmative", "go on then", "go ahead then", "what's next",
|
|
758
|
+
"go on ahead", "let's do it", "go ahead and", "do it then",
|
|
759
|
+
"嗯,可以", "嗯,好的", "好,开工", "行,那就",
|
|
760
|
+
# ── P0:金标继承缺口 ──
|
|
761
|
+
"没意见", "没意见,执行", "照这个办", "就这么干", "没问题,就这么干",
|
|
762
|
+
"接着往下", "就照这个", "照这个", "可以呀", "ok继续",
|
|
763
|
+
"proceed as planned", "carry on then", "ship it", "roger that",
|
|
764
|
+
"that's good", "yes do it", "yes, do it",
|
|
765
|
+
# ── 恢复 P1 续2(勿丢)──
|
|
766
|
+
"嗯", "就按你说的做", "就按你说的", "刚才说的办", "方案权衡听你的",
|
|
767
|
+
"方案权衡听你的,继续", "听你的,继续", "请开始", "批准", "批准执行",
|
|
768
|
+
"approved go", "approved go ahead", "go now", "please go",
|
|
769
|
+
"you're good to go", "you have my go-ahead", "have my go-ahead",
|
|
770
|
+
"sounds good go ahead", "sounds good carry on", "good to go",
|
|
771
|
+
"continue as planned", "continue from where you left off",
|
|
772
|
+
"from where you left off", "where you left off",
|
|
773
|
+
"就按刚才", "按刚才说的", "嗯执行吧", "好的请开始", "执行吧",
|
|
774
|
+
# ── §7/inherit 残留 ──
|
|
775
|
+
"按你的方案来", "按方案来", "好,那就这样", "那就这样", "好那就这样",
|
|
776
|
+
"按你的方案", "你的方案来",
|
|
777
|
+
# ── M3 新增短语回归 ──
|
|
778
|
+
"按原计划执行", "按原计划办",
|
|
779
|
+
]
|
|
780
|
+
|
|
781
|
+
# 分词:英文按单词,中文逐字(P1 修复:原来中文整串一个token,DP切不开)
|
|
782
|
+
_TOKEN_RE = re.compile(r"[0-9a-z']+|[\u4e00-\u9fff]") # P1: 中文单字
|
|
783
|
+
_INHERIT_TOKENS = [tuple(_TOKEN_RE.findall(w.lower())) for w in INHERIT_WORDS]
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
_FILLER_CHARS = set('的了吧呢啊哈哦呀嘞呐滴哒嘛咯呗喽诶欸唉哟嘿了吗着过很太挺请就先呀嘛呗')
|
|
787
|
+
_FILLER_EN = {'um', 'uh', 'hm', 'hmm', 'well', 'just', 'then', 'so', 'and', 'or', 'a', 'an', 'the', 'to', 'it', 'that', 'this', 'my', 'from', 'where', 'you'}
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _is_all_inherit_tokens(tokens: List[str]) -> bool:
|
|
791
|
+
"""整串 token 能否被词表完整切分(允许跳过语气填充词)。
|
|
792
|
+
|
|
793
|
+
用 DP 而不是贪心取长词:贪心在 "sure thing" 这类前缀重合的地方会选错。
|
|
794
|
+
"""
|
|
795
|
+
n = len(tokens)
|
|
796
|
+
reach = [False] * (n + 1)
|
|
797
|
+
reach[0] = True
|
|
798
|
+
for i in range(n):
|
|
799
|
+
if reach[i]:
|
|
800
|
+
t = tokens[i]
|
|
801
|
+
if (len(t) == 1 and t in _FILLER_CHARS) or t in _FILLER_EN:
|
|
802
|
+
if not reach[i + 1]:
|
|
803
|
+
reach[i + 1] = True
|
|
804
|
+
for entry in _INHERIT_TOKENS:
|
|
805
|
+
j = i + len(entry)
|
|
806
|
+
if j <= n and not reach[j] and tuple(tokens[i:j]) == entry:
|
|
807
|
+
reach[j] = True
|
|
808
|
+
return reach[n]
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def parse_intent(text: str) -> Optional[Dict]:
|
|
812
|
+
"""解析用户消息中的显式档位意图。
|
|
813
|
+
|
|
814
|
+
返回结构化约束 {op, tier, span, polarity, stage} 或 None(无意图)。
|
|
815
|
+
|
|
816
|
+
stage 字段标注命中位置(诊断用):
|
|
817
|
+
gate — 语境闸门拦截
|
|
818
|
+
inherit — 继承词命中
|
|
819
|
+
anchor — 锚点匹配成功
|
|
820
|
+
no_anchor — 词表里没找到任何锚点
|
|
821
|
+
bare_filtered — 裸词被动作词过滤器丢弃
|
|
822
|
+
|
|
823
|
+
op 语义(约束代数):
|
|
824
|
+
force(t) — 明确要 t 档("用最高"、"只用low")
|
|
825
|
+
exclude(t) — 排除 t 档("不要用最高")
|
|
826
|
+
inherit — 保持上一轮档位("继续"、"go on")
|
|
827
|
+
|
|
828
|
+
Phase 0 只实现 force、exclude、inherit。
|
|
829
|
+
"""
|
|
830
|
+
if not text:
|
|
831
|
+
return {"op": "none", "tier": None, "span": text, "polarity": "neutral",
|
|
832
|
+
"stage": "empty_text", "void_reason": "empty"}
|
|
833
|
+
|
|
834
|
+
# ── 语境闸门(v2,优先级最高)──
|
|
835
|
+
gate = _context_gate(text)
|
|
836
|
+
if gate is not None:
|
|
837
|
+
return {"op": "none", "tier": None, "span": text, "polarity": "neutral",
|
|
838
|
+
"stage": "gate", "void_reason": gate}
|
|
839
|
+
|
|
840
|
+
# ── 继承意图 ──
|
|
841
|
+
# 长度门槛:去标点+折叠空白后 ≤20 字符;纯 ASCII 且 ≤6 词可放宽
|
|
842
|
+
# ("please continue as planned" 26 字符但 4 词,全是继承词)
|
|
843
|
+
stripped = text.strip()
|
|
844
|
+
stripped_nopunct = re.sub(r"[,。!?、,.!?;::…~\-]+", " ", stripped)
|
|
845
|
+
stripped_nopunct = re.sub(r"\s+", " ", stripped_nopunct).strip()
|
|
846
|
+
_len_ok = (
|
|
847
|
+
len(stripped_nopunct) <= 20
|
|
848
|
+
or (stripped_nopunct.isascii() and len(stripped_nopunct.split()) <= 6)
|
|
849
|
+
)
|
|
850
|
+
if _len_ok:
|
|
851
|
+
tokens = _TOKEN_RE.findall(stripped_nopunct.lower())
|
|
852
|
+
if tokens and _is_all_inherit_tokens(tokens):
|
|
853
|
+
return {
|
|
854
|
+
"op": "inherit", "tier": None, "span": stripped,
|
|
855
|
+
"polarity": "neutral", "word": " ".join(tokens),
|
|
856
|
+
"stage": "inherit", "void_reason": None,
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
# ── 档位锚点匹配 ──
|
|
860
|
+
anchors = find_anchors(text)
|
|
861
|
+
if not anchors:
|
|
862
|
+
return {"op": "none", "tier": None, "span": text, "polarity": "neutral",
|
|
863
|
+
"stage": "no_anchor", "void_reason": "vocabulary_gap"}
|
|
864
|
+
|
|
865
|
+
# 裸词 + 弱锚过滤:作用于**全部**锚点(原先只看 anchors[0])
|
|
866
|
+
def _has_action(window: str) -> bool:
|
|
867
|
+
return any(act in window for act in _ACTION_WORDS) or bool(_ACTION_EN_RE.search(window))
|
|
868
|
+
|
|
869
|
+
def _has_upshift(window: str) -> bool:
|
|
870
|
+
return any(v in window for v in _UPSHIFT_VERBS)
|
|
871
|
+
|
|
872
|
+
filtered = []
|
|
873
|
+
dropped_bare = False
|
|
874
|
+
dropped_weak = False
|
|
875
|
+
for x in anchors:
|
|
876
|
+
wl = x["word"].lower()
|
|
877
|
+
window = text[max(0, x["start"] - SCOPE_WINDOW):x["end"] + SCOPE_WINDOW]
|
|
878
|
+
if wl in _BARE_WORDS:
|
|
879
|
+
# 否定语境下裸词保留:「无需 max」「别用 low」→ exclude,而非 none
|
|
880
|
+
if not _has_action(window) and detect_negation(text, x["start"], x["end"]) is None:
|
|
881
|
+
dropped_bare = True
|
|
882
|
+
continue
|
|
883
|
+
if x["word"] in _WEAK_MAX_WORDS:
|
|
884
|
+
# 「思考预算省着点花」:有省/节约且无升档动词 → 丢
|
|
885
|
+
if re.search(r"省着|节约|省钱|便宜|够用", window) and not _has_upshift(window):
|
|
886
|
+
dropped_weak = True
|
|
887
|
+
continue
|
|
888
|
+
if not _has_upshift(window) and not _has_action(window):
|
|
889
|
+
dropped_weak = True
|
|
890
|
+
continue
|
|
891
|
+
filtered.append(x)
|
|
892
|
+
|
|
893
|
+
if not filtered:
|
|
894
|
+
if dropped_bare:
|
|
895
|
+
return {"op": "none", "tier": None, "span": text, "polarity": "neutral",
|
|
896
|
+
"stage": "bare_filtered", "void_reason": "no_action_context"}
|
|
897
|
+
if dropped_weak:
|
|
898
|
+
return {"op": "none", "tier": None, "span": text, "polarity": "neutral",
|
|
899
|
+
"stage": "bare_filtered", "void_reason": "weak_anchor_filtered"}
|
|
900
|
+
return {"op": "none", "tier": None, "span": text, "polarity": "neutral",
|
|
901
|
+
"stage": "no_anchor", "void_reason": "vocabulary_gap"}
|
|
902
|
+
|
|
903
|
+
anchors = filtered
|
|
904
|
+
a = anchors[0]
|
|
905
|
+
span_start = max(0, a["start"] - SCOPE_WINDOW)
|
|
906
|
+
span = text[span_start:a["end"]]
|
|
907
|
+
|
|
908
|
+
# 检查否定(含后置否定 + 否定二分法)
|
|
909
|
+
neg_word = detect_negation(text, a["start"], a["end"])
|
|
910
|
+
if neg_word:
|
|
911
|
+
intent = negation_to_intent(neg_word, a["tier"], text, a["start"], a["end"])
|
|
912
|
+
# P4:exclude + 显式替代档 → force(替代);「简单做/随便答」仍 exclude
|
|
913
|
+
# 替代词只在 exclude 锚点所在/邻接小句生效(防跨句「默认值」误替代)
|
|
914
|
+
if intent.get("op") == "exclude":
|
|
915
|
+
_sub_region = _adj_clause_region(text, a["start"])
|
|
916
|
+
if _H_SUB_HIGH.search(_sub_region):
|
|
917
|
+
return {
|
|
918
|
+
"op": "force", "tier": "high",
|
|
919
|
+
"span": intent.get("span", span),
|
|
920
|
+
"polarity": "assert",
|
|
921
|
+
"anchor": a["word"],
|
|
922
|
+
"conversion": "substitute",
|
|
923
|
+
"exclude_slot": intent.get("tier"),
|
|
924
|
+
"stage": "anchor", "void_reason": None,
|
|
925
|
+
}
|
|
926
|
+
if _H_SUB_LOW.search(_sub_region) and a["tier"] != "low":
|
|
927
|
+
return {
|
|
928
|
+
"op": "force", "tier": "low",
|
|
929
|
+
"span": intent.get("span", span),
|
|
930
|
+
"polarity": "assert",
|
|
931
|
+
"axis": "style",
|
|
932
|
+
"anchor": a["word"],
|
|
933
|
+
"conversion": "substitute",
|
|
934
|
+
"exclude_slot": intent.get("tier"),
|
|
935
|
+
"stage": "anchor", "void_reason": None,
|
|
936
|
+
}
|
|
937
|
+
return intent
|
|
938
|
+
|
|
939
|
+
# 检查限定(只用/仅用 → force)
|
|
940
|
+
restr_word = detect_restriction(text, a["start"])
|
|
941
|
+
if restr_word:
|
|
942
|
+
return {
|
|
943
|
+
"op": "force",
|
|
944
|
+
"tier": a["tier"],
|
|
945
|
+
"span": span,
|
|
946
|
+
"polarity": "assert",
|
|
947
|
+
"restriction": restr_word,
|
|
948
|
+
"anchor": a["word"],
|
|
949
|
+
"stage": "anchor",
|
|
950
|
+
"void_reason": None,
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
# 无否定无限定 → force(默认肯定)
|
|
954
|
+
return {
|
|
955
|
+
"op": "force",
|
|
956
|
+
"tier": a["tier"],
|
|
957
|
+
"span": span,
|
|
958
|
+
"polarity": "assert",
|
|
959
|
+
"anchor": a["word"],
|
|
960
|
+
"stage": "anchor",
|
|
961
|
+
"void_reason": None,
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def apply_constraint(base_tier: str, intent: Optional[Dict], prev_tier: Optional[str] = None) -> str:
|
|
966
|
+
"""将意图约束应用到内容判断的 base_tier 上,返回最终档位。
|
|
967
|
+
|
|
968
|
+
约束代数:
|
|
969
|
+
force(t) → 直接返回 t(用户显式指定,最高优先)
|
|
970
|
+
exclude(t) → 如果 base_tier == t,降一档;否则不变
|
|
971
|
+
inherit → 返回 prev_tier(保持上一轮档位);无上一轮则回退 base_tier
|
|
972
|
+
none → 返回 base_tier(无意图;显式契约,不靠「未知 op」落网)
|
|
973
|
+
None → 返回 base_tier(无意图,内容判断说了算)
|
|
974
|
+
|
|
975
|
+
降档规则(exclude 的处理):
|
|
976
|
+
exclude(max) 且 base=max → high(降一档,不降到底)
|
|
977
|
+
exclude(high) 且 base=high → low
|
|
978
|
+
exclude(low) 且 base=low → high(反向:排除 low = 至少 high)
|
|
979
|
+
"""
|
|
980
|
+
if intent is None:
|
|
981
|
+
return base_tier
|
|
982
|
+
|
|
983
|
+
tier_order = ["low", "high", "max"]
|
|
984
|
+
|
|
985
|
+
# P0 后 parse_intent 对 empty/gate/no_anchor 也返回 dict op=none;内容判断说了算。
|
|
986
|
+
if intent["op"] == "none":
|
|
987
|
+
return base_tier
|
|
988
|
+
|
|
989
|
+
if intent["op"] == "force":
|
|
990
|
+
# 用户显式指定,一票否决
|
|
991
|
+
return intent["tier"]
|
|
992
|
+
|
|
993
|
+
if intent["op"] == "inherit":
|
|
994
|
+
# 保持上一轮档位;没有上一轮则用 Laya 的判断
|
|
995
|
+
if prev_tier is not None:
|
|
996
|
+
return prev_tier
|
|
997
|
+
return base_tier
|
|
998
|
+
|
|
999
|
+
if intent["op"] == "exclude":
|
|
1000
|
+
target = intent["tier"]
|
|
1001
|
+
if base_tier == target:
|
|
1002
|
+
if target == "max":
|
|
1003
|
+
return "high" # 排除 max → 降一档
|
|
1004
|
+
if target == "high":
|
|
1005
|
+
return "low" # 排除 high → 降一档
|
|
1006
|
+
if target == "low":
|
|
1007
|
+
return "high" # 排除 low → 升一档("别用low"= 至少 high)
|
|
1008
|
+
# base_tier 不是被排除的档 → 不影响
|
|
1009
|
+
return base_tier
|
|
1010
|
+
|
|
1011
|
+
# 未知 op(Phase 1 扩展)
|
|
1012
|
+
return base_tier
|