feat: 不用斜杠,单发 goal 也触发 /goal 目标
触发词从仅 /goal 放宽到 /goal 或裸 goal(大小写不敏感, 单数 only,goals/mygoal 不算):goal 收xxx、goal,目标是 收xxx、发goal 收xxx 均同款触发,新建与主线转换入口共用 同一解析。讨论句式仍不触发:goal 的用法、goal 是什么、 讨论goal的用法、/goal的用法 均返回 None。 单测补 4 项:裸 goal 触发、裸 goal 讨论不触发、followup 裸 goal 转换、新建裸 goal。
This commit is contained in:
1 parent
f3bd65c171
commit
f1aab5415c
3 files changed
+93
-32
No files matched your search
+6
-3
@@ -535,10 +535,13 @@ goal 模式的继续:server 把任务转成新 goal 语义(goal=新消息、
|
||||
## 主线 /goal 命令(发给主线一句话就自动建 goal)
|
||||
|
||||
不用先知道主线是进行中还是已终态,也不用勾选项:给主线发
|
||||
一条含字面 `/goal` 的消息即可,例如“请在这条主线开启一个
|
||||
持久 /goal,目标是收xxx”,或直接写“/goal 收xxx”。解析在
|
||||
一条含触发词的消息即可,例如“请在这条主线开启一个
|
||||
持久 /goal,目标是收xxx”,或直接写“/goal 收xxx”。斜杠
|
||||
不用:单发 `goal` 也同款触发,例如“goal 收xxx”、
|
||||
“goal,目标是收xxx”。解析在
|
||||
`cloud/goal_command.py`(server 与 worker 共用同一份,防两端
|
||||
判断不一致);只是在讨论里提到“/goal 的用法”不算命令。
|
||||
判断不一致);只是在讨论里提到“/goal 的用法”“goal 是什么”
|
||||
这种不算命令。
|
||||
新建目标也同款:`POST /tasks` 的 prompt 里直接写 `/goal
|
||||
收xxx`(或上面那句整话)即新建 goal 任务,prompt 与 goal
|
||||
都存解析出的目标,不用勾 goal 选项;空目标、plan 冲突、
|
||||
|
||||
+57
-29
@@ -7,15 +7,19 @@
|
||||
触发转换,调用方不用再知道任务当前是进行中还是已终态。
|
||||
|
||||
解析规则(刻意收紧,防误触发):
|
||||
- 文本里必须出现字面 "/goal"(全角 / 也认,大小写不敏感)。
|
||||
- 触发词是 "/goal" 或裸 "goal"(斜杠不用,全角 / 也认,
|
||||
大小写不敏感,单数 only——"goals" 不算)。
|
||||
- 光出现还不够,必须是命令形状,二者居一:
|
||||
1. 文本以 /goal 开头(最显式的命令);
|
||||
2. /goal 后面紧跟分隔符(空格、,、:、、)或“目标”标记,
|
||||
例如“/goal 收xxx”“/goal,目标是收xxx”;
|
||||
3. /goal 前面有开启类动词(开启/建立/创建/添加/设为/设置/
|
||||
开一个),例如“请开启一个持久/goal收xxx”。
|
||||
只是在讨论里提到“/goal 的用法”这种不算命令。
|
||||
- 目标取 /goal 之后的部分:先去分隔符,再去“目标是/目标为/
|
||||
1. 文本以触发词开头(最显式的命令),例如“goal 收xxx”;
|
||||
2. 触发词后面紧跟分隔符(空格、,、:、、)或“目标”标记,
|
||||
例如“/goal 收xxx”“goal,目标是收xxx”;
|
||||
3. 触发词前面有开启/发送类动词(开启/建立/创建/添加/设为/
|
||||
设置/开一个/发/发送),例如“请开启一个持久/goal收xxx”、
|
||||
“发goal 收xxx”。
|
||||
只是在讨论里提到“/goal 的用法”“goal 的用法”“goal 是什么”
|
||||
这种不算命令:触发词后面紧跟“的/是/吗/什么/怎么/如何/
|
||||
模式/用法/意思/功能”时一律不当命令。
|
||||
- 目标取触发词之后的部分:先去分隔符,再去“目标是/目标为/
|
||||
目标:/目标:”或“持久目标是/为/:/:”这类前缀,最后去尾句号。
|
||||
- 返回值:不是命令返回 None;是命令但没给目标返回 ""(调用方
|
||||
据此回 400“目标不能为空”,而不是当成普通消息悄悄排队);
|
||||
@@ -28,12 +32,17 @@
|
||||
import re
|
||||
|
||||
_GOAL_RE = re.compile(r"/goal(?![A-Za-z])", re.IGNORECASE)
|
||||
_BARE_GOAL_RE = re.compile(r"(?<![A-Za-z])goal(?![A-Za-z])", re.IGNORECASE)
|
||||
_SEPARATORS = " \t\r\n::,,、"
|
||||
_TARGET_MARKERS = (
|
||||
"持久目标是", "持久目标为", "持久目标:", "持久目标:",
|
||||
"目标是", "目标为", "目标:", "目标:",
|
||||
)
|
||||
_OPEN_VERBS = ("开启", "建立", "创建", "添加", "设为", "设置", "开一个")
|
||||
_OPEN_VERBS = ("开启", "建立", "创建", "添加", "设为", "设置", "开一个",
|
||||
"发", "发送")
|
||||
# 触发词后紧跟这些词头时是在讨论 goal 本身,不是下命令。
|
||||
_DISCUSSION_PREFIXES = ("的", "是", "吗", "什么", "怎么", "如何",
|
||||
"模式", "用法", "意思", "功能")
|
||||
|
||||
|
||||
def _strip_target(rest):
|
||||
@@ -49,6 +58,20 @@ def _strip_target(rest):
|
||||
return rest
|
||||
|
||||
|
||||
def _rest_is_command(rest):
|
||||
"""触发词后的部分是不是命令形状(讨论词头除外)。"""
|
||||
if not rest:
|
||||
# 单独一句“/goal”/“goal”:是命令,但没目标,调用方回 400。
|
||||
return True
|
||||
if rest[0] in _SEPARATORS:
|
||||
return True
|
||||
if rest.startswith("目标"):
|
||||
return True
|
||||
if rest.startswith(_DISCUSSION_PREFIXES):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def parse_goal_command(text):
|
||||
"""解析主线消息里的 /goal 命令,返回目标或 None/""(见模块说明)。"""
|
||||
if not isinstance(text, str):
|
||||
@@ -56,24 +79,29 @@ def parse_goal_command(text):
|
||||
s = text.strip().replace("/", "/")
|
||||
if not s:
|
||||
return None
|
||||
# 先认带斜杠的:是命令直接返回;不是命令(如讨论里提到)
|
||||
# 也不急着否掉,后面裸 goal 的位置还可能是一条真命令。
|
||||
match = _GOAL_RE.search(s)
|
||||
if match is None:
|
||||
return None
|
||||
before = s[:match.start()]
|
||||
rest = s[match.end():]
|
||||
stripped = s.lstrip()
|
||||
is_command = False
|
||||
if stripped.lower().startswith("/goal"):
|
||||
is_command = True
|
||||
elif rest and rest[0] in _SEPARATORS:
|
||||
is_command = True
|
||||
elif rest.startswith("目标"):
|
||||
is_command = True
|
||||
elif any(verb in before for verb in _OPEN_VERBS):
|
||||
is_command = True
|
||||
elif not rest:
|
||||
# 单独一句“/goal”:是命令,但没目标,调用方回 400。
|
||||
is_command = True
|
||||
if not is_command:
|
||||
return None
|
||||
return _strip_target(rest)
|
||||
if match is not None:
|
||||
before = s[:match.start()]
|
||||
rest = s[match.end():]
|
||||
# 斜杠本身已是显式标记,句首与否不再单算:命令与否
|
||||
# 只看前面有没有动词、后面是不是讨论词头——这样
|
||||
# “/goal的用法”也不会被误当成命令。
|
||||
if (any(verb in before for verb in _OPEN_VERBS)
|
||||
or _rest_is_command(rest)):
|
||||
return _strip_target(rest)
|
||||
# 再认不带斜杠的 goal:位置必须在句首,或前面是分隔符/
|
||||
# 开启·发送类动词,否则“讨论goal的用法”这类也会误触发。
|
||||
for bare in _BARE_GOAL_RE.finditer(s):
|
||||
if bare.start() > 0 and s[bare.start() - 1] == "/":
|
||||
continue # 斜杠那处已在上面判过
|
||||
before = s[:bare.start()]
|
||||
rest = s[bare.end():]
|
||||
at_start = not before.strip()
|
||||
before_ok = at_start or (
|
||||
before.rstrip() and before.rstrip()[-1] in _SEPARATORS) or (
|
||||
any(verb in before for verb in _OPEN_VERBS))
|
||||
if before_ok and _rest_is_command(rest):
|
||||
return _strip_target(rest)
|
||||
return None
|
||||
@@ -56,6 +56,22 @@ class ParserTestCase(unittest.TestCase):
|
||||
self.assertIsNone(parse_goal_command("请帮我完成目标收xxx"))
|
||||
self.assertIsNone(parse_goal_command("hello world"))
|
||||
|
||||
def test_bare_goal_without_slash(self):
|
||||
# 不用斜杠:用户单发 goal 也触发同款目标。
|
||||
self.assertEqual(parse_goal_command("goal 收xxx"), "收xxx")
|
||||
self.assertEqual(parse_goal_command("goal,目标是收xxx"), "收xxx")
|
||||
self.assertEqual(parse_goal_command("goal目标是收xxx"), "收xxx")
|
||||
self.assertEqual(parse_goal_command("goal收xxx"), "收xxx")
|
||||
self.assertEqual(parse_goal_command("发goal 收xxx"), "收xxx")
|
||||
self.assertEqual(parse_goal_command("goal"), "")
|
||||
|
||||
def test_bare_goal_discussion_still_not_a_command(self):
|
||||
self.assertIsNone(parse_goal_command("goal的用法"))
|
||||
self.assertIsNone(parse_goal_command("goal是什么意思"))
|
||||
self.assertIsNone(parse_goal_command("讨论goal的用法"))
|
||||
self.assertIsNone(parse_goal_command("请问goal是什么意思"))
|
||||
self.assertIsNone(parse_goal_command("/goal的用法"))
|
||||
|
||||
|
||||
class ServerTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@@ -248,6 +264,20 @@ class ServerTestCase(unittest.TestCase):
|
||||
task = self._create(prompt="讨论/goal的用法")
|
||||
self.assertEqual(task["goal_mode"], 0)
|
||||
|
||||
def test_followup_bare_goal_converts(self):
|
||||
task = self._create()
|
||||
status, body = self._req(
|
||||
"POST", "/tasks/%s/followups" % task["id"],
|
||||
{"body": "goal 收xxx"}, self.owner_key)
|
||||
self.assertEqual(status, 200, body)
|
||||
self.assertEqual(body["delivery"], "goal_converted")
|
||||
self.assertEqual(body["goal"], "收xxx")
|
||||
|
||||
def test_create_task_bare_goal(self):
|
||||
task = self._create(prompt="goal 收xxx")
|
||||
self.assertEqual(task["goal_mode"], 1)
|
||||
self.assertEqual(task["goal"], "收xxx")
|
||||
|
||||
def test_continue_goal_command(self):
|
||||
task = self._create()
|
||||
self._set_status(task["id"], "completed")
|
||||
|
||||
Reference in new issue
Block a user