mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-23 05:35:30 +00:00
fix(proactive): stop anchoring openers on one stock lead-in
Both prompt languages told the model to open the way a friend "happens to mention" something, and the Chinese one even handed it "对了,上次那个……" as the example, so nearly every card came back with the same two characters in front - a script rather than a person. The deterministic fallback did the same for reminders, the one kind that always reaches today's agenda. - plan.yaml: name the stock lead-in as a second forbidden failure mode next to the notification tone, list the angles to rotate through (the date or the clash, what moved since last time, the deliverable, a direct question, the user's current focus), require that no two openers in a batch share their opening words, and restate it as a hard rule - plan.py: three opener variants per kind in FALLBACK_OPENERS, picked by a sha1 of the topic id, so the no-LLM path varies card to card while one topic keeps the same opener across rounds; every reminder variant still carries the due date, which a reminder may not hide
This commit is contained in:
parent
73a106dc53
commit
b991c54955
2 changed files with 75 additions and 17 deletions
|
|
@ -9,7 +9,8 @@ gets a deterministic fallback card so the agenda step never sees an empty input.
|
|||
Card contract: ``scenario_type`` (resume_task | answer_pending |
|
||||
explore_interest | prepare_upcoming | propose_delegation), ``opener`` (a casual,
|
||||
friend-like conversation opener that already names the minimal next action -
|
||||
never a "based on your records" notification tone), ``next_action``,
|
||||
never a "based on your records" notification tone, and never a stock lead-in
|
||||
worn into a template), ``next_action``,
|
||||
``preconditions`` and ``delivery`` (in_conversation | notification |
|
||||
agenda_item). ``linked_memory`` is derived from the topic's paths/evidence by
|
||||
code, never by the LLM.
|
||||
|
|
@ -20,6 +21,7 @@ pins every reminder card it receives.
|
|||
"""
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
|
||||
|
|
@ -44,6 +46,33 @@ MAX_NEXT_ACTION_CHARS = 200
|
|||
MAX_PRECONDITIONS = 5
|
||||
MAX_PRECONDITION_CHARS = 80
|
||||
|
||||
# Deterministic openers, several per kind, so a batch of fallback cards does not
|
||||
# open every line the same way - one repeated lead-in reads as a script. ``{when}``
|
||||
# is the parenthesised due date (empty when unknown) and only reminder variants
|
||||
# carry it, because a reminder may not hide when it is due.
|
||||
FALLBACK_OPENERS = {
|
||||
"reminder": (
|
||||
"「{title}」{when}就快到了,要不要现在把准备事项先过一遍?",
|
||||
"时间快到了:「{title}」{when},先确认下会不会和别的安排撞上?",
|
||||
"有件事排在前面了:「{title}」{when},现在动手准备最从容。",
|
||||
),
|
||||
"new_idea": (
|
||||
"有件活儿我觉得可以替你办:「{title}」,要不要我先出个方案你看看?",
|
||||
"「{title}」这件事其实可以直接交给我,要不要我先出一版草案给你审?",
|
||||
"这活儿不必你自己来:「{title}」,我先跑个初稿你看看值不值?",
|
||||
),
|
||||
"follow_up": (
|
||||
"上次聊到「{title}」,好像还没收尾——要不要趁现在花几分钟往前推一步?",
|
||||
"「{title}」还挂着,要不要我先去查一下最新进展,再回来跟你说下一步?",
|
||||
"那件「{title}」好像只差临门一脚了,现在顺手推一下?",
|
||||
),
|
||||
"interest_extend": (
|
||||
"感觉你最近可能会想看看「{title}」,有空的话可以先从相关材料扫一眼。",
|
||||
"「{title}」这块还能再往深挖一层,要不要我挑两处给你展开讲讲?",
|
||||
"顺着「{title}」又翻到些延伸内容,值得的话我带你过一遍?",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def linked_memory(candidate: dict) -> list[str]:
|
||||
"""Workspace paths related to a topic, derived from paths + evidence."""
|
||||
|
|
@ -60,23 +89,22 @@ def linked_memory(candidate: dict) -> list[str]:
|
|||
|
||||
|
||||
def fallback_card(candidate: dict) -> dict:
|
||||
"""Deterministic card used when the LLM is absent or its output is unusable."""
|
||||
"""Deterministic card used when the LLM is absent or its output is unusable.
|
||||
|
||||
Its opener is picked per topic out of ``FALLBACK_OPENERS``, so the
|
||||
no-LLM path honours the same "no repeated lead-in" rule the prompt states.
|
||||
"""
|
||||
kind = str(candidate.get("kind") or DEFAULT_KIND)
|
||||
title = str(candidate.get("title") or "")
|
||||
scenario = DEFAULT_SCENARIO_BY_KIND.get(kind, DEFAULT_SCENARIO_BY_KIND[DEFAULT_KIND])
|
||||
opener = _fallback_opener(kind, candidate, title)
|
||||
if kind == "reminder":
|
||||
due = str(candidate.get("due_at") or "").strip()
|
||||
when = f"({due})" if due else ""
|
||||
opener = f"对了,「{title}」{when}就快到了,要不要现在把准备事项先过一遍?"
|
||||
next_action = "确认时间、所需材料与是否和其它安排冲突,需要改期就现在说"
|
||||
elif kind == "new_idea":
|
||||
opener = f"有件活儿我觉得可以替你办:「{title}」,要不要我先出个方案你看看?"
|
||||
next_action = "确认值不值得委托;值得的话由 agent 先产出一版草案供你审阅"
|
||||
elif kind == "follow_up":
|
||||
opener = f"上次聊到「{title}」,好像还没收尾——要不要趁现在花几分钟往前推一步?"
|
||||
next_action = "先回顾相关记录,确认卡点,然后给出下一步动作"
|
||||
else:
|
||||
opener = f"感觉你最近可能会想看看「{title}」,有空的话可以先从相关材料扫一眼。"
|
||||
next_action = "快速浏览相关材料,判断值不值得深入"
|
||||
memory = linked_memory(candidate)
|
||||
if memory:
|
||||
|
|
@ -90,6 +118,20 @@ def fallback_card(candidate: dict) -> dict:
|
|||
}
|
||||
|
||||
|
||||
def _fallback_opener(kind: str, candidate: dict, title: str) -> str:
|
||||
"""Pick one opener variant for this kind, seeded by the topic identity.
|
||||
|
||||
The seed is the topic id, so one topic keeps one opener across rounds
|
||||
instead of flip-flopping, while different topics in the same round land on
|
||||
different variants and the agenda does not read as one repeated sentence.
|
||||
"""
|
||||
variants = FALLBACK_OPENERS.get(kind) or FALLBACK_OPENERS[DEFAULT_KIND]
|
||||
seed = str(candidate.get("id") or title)
|
||||
picked = variants[int(hashlib.sha1(seed.encode("utf-8")).hexdigest(), 16) % len(variants)]
|
||||
due = str(candidate.get("due_at") or "").strip()
|
||||
return picked.format(title=title, when=f"({due})" if due else "")
|
||||
|
||||
|
||||
def _clean_card(raw: dict, candidate: dict) -> dict:
|
||||
"""Validate one LLM card; per-field fallback keeps the card always usable."""
|
||||
base = fallback_card(candidate)
|
||||
|
|
|
|||
|
|
@ -23,11 +23,19 @@ plan_system_prompt: |
|
|||
resolve when the topic mentions one);
|
||||
- `new_idea` -> propose_delegation, naming the deliverable the agent would
|
||||
produce - an offer, never a report of work already done.
|
||||
- `opener`: one casual spoken sentence, like a friend happening to mention
|
||||
it ("by the way..."). STRICTLY forbidden: notification tone such as
|
||||
"based on your previous records", "the system reminds you", "as you
|
||||
mentioned on <date>". The opener must already point at the minimal
|
||||
executable next action, so it invites doing, not just knowing.
|
||||
- `opener`: one casual spoken sentence a friend would actually say. Two
|
||||
failure modes are STRICTLY forbidden:
|
||||
1. Notification tone: "based on your previous records", "the system
|
||||
reminds you", "as you mentioned on <date>".
|
||||
2. A stock lead-in worn into a template. Never open with "by the way..."
|
||||
or any other fixed filler ("just a reminder...", "quick note..."):
|
||||
the same first words every day read as a script, not as a person.
|
||||
Start from the substance instead and change the angle card by card - the
|
||||
date or the clash, what moved since last time, the deliverable the agent
|
||||
would produce, a direct question, or whatever the user is working on right
|
||||
now. Inside one batch no two openers may share their opening words.
|
||||
The opener must already point at the minimal executable next action, so it
|
||||
invites doing, not just knowing.
|
||||
A reminder opener still sounds like a person, but it may not hide the date:
|
||||
the user has to hear when it is due.
|
||||
- `next_action`: the smallest concrete step the user can start right now.
|
||||
|
|
@ -42,6 +50,7 @@ plan_system_prompt: |
|
|||
- Exactly one card per input topic, no extras.
|
||||
- Ground everything in the given reason/evidence; never invent facts.
|
||||
- Proposals stay proposals: never word a card as if the work were done.
|
||||
- Openers vary: one lead-in formula per batch is a defect, not a style.
|
||||
- Quote YAML strings containing punctuation such as `:`.
|
||||
- Write `opener` and `next_action` in the language the user's own material is
|
||||
written in, so the assistant never switches tongues mid-conversation.
|
||||
|
|
@ -53,7 +62,7 @@ plan_system_prompt: |
|
|||
cards:
|
||||
- topic_id: <echoed id>
|
||||
scenario_type: resume_task|answer_pending|explore_interest|prepare_upcoming|propose_delegation
|
||||
opener: <one casual sentence pointing at the next action>
|
||||
opener: <one casual sentence, substance first, pointing at the next action>
|
||||
next_action: <smallest executable step>
|
||||
preconditions: [<condition>, ...]
|
||||
delivery: in_conversation|notification|agenda_item
|
||||
|
|
@ -82,8 +91,14 @@ plan_system_prompt_zh: |
|
|||
点明要先协调哪一件事;
|
||||
- `new_idea` -> propose_delegation,说清 agent 会交付什么——是邀约,
|
||||
绝不能写成已经做完的汇报。
|
||||
- `opener`:一句口语化的开场,像朋友随口提起("对了,上次那个……")。
|
||||
严禁通知腔:"根据您之前的记录"、"系统提醒您"、"您在某天提到过"等。
|
||||
- `opener`:一句朋友真会说出口的口语化开场。以下两种写法都严禁:
|
||||
1. 通知腔:"根据您之前的记录"、"系统提醒您"、"您在某天提到过"等;
|
||||
2. 把某个口头禅当模板反复套用。不要一律以"对了……"开头,也不要换成
|
||||
"顺便说一下"、"提醒您一下"之类别的固定引子——每天都是同一句开头,
|
||||
就成了脚本,不像人。
|
||||
直接从内容本身切入,并且逐卡换角度:先说时间点或撞期、先说上次之后有
|
||||
什么变化、先说 agent 会交付什么、直接抛一个问题,或者从用户眼下正在做
|
||||
的事接过去。同一批卡片里,任意两张开场的开头用词都不得相同。
|
||||
开场必须顺势给出最小可执行的下一步,让人愿意动手,而不只是知道。
|
||||
reminder 的开场同样要像人话,但不能藏时间:用户必须听到"什么时候到期"。
|
||||
- `next_action`:用户现在就能开始的最小具体动作。
|
||||
|
|
@ -98,6 +113,7 @@ plan_system_prompt_zh: |
|
|||
- 每个输入主题恰好一张卡,不得多给。
|
||||
- 所有内容必须基于给定的 reason/evidence,禁止编造事实。
|
||||
- 提议就是提议:任何卡片都不得写成工作已经完成。
|
||||
- 开场白必须各不相同:整批共用一个引子是缺陷,不是风格。
|
||||
- 包含 `:` 等标点的 YAML 字符串请加引号。
|
||||
- `opener` 与 `next_action` 使用用户素材本身所用的语言书写,避免助手在对话
|
||||
中途切换语言。
|
||||
|
|
@ -109,7 +125,7 @@ plan_system_prompt_zh: |
|
|||
cards:
|
||||
- topic_id: <回显的 id>
|
||||
scenario_type: resume_task|answer_pending|explore_interest|prepare_upcoming|propose_delegation
|
||||
opener: <一句口语化开场,顺势给出下一步动作>
|
||||
opener: <一句口语化开场,直接从内容切入,顺势给出下一步动作>
|
||||
next_action: <最小可执行步骤>
|
||||
preconditions: [<条件>, ...]
|
||||
delivery: in_conversation|notification|agenda_item
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue