feat(chat): server-side interview turn plan (mode, anchor slot, snippet)
- Add plan_interview_turn: emotion_first / memoir_push / follow_user_only - Inject hard directive block at top of guided system prompt - Pass stage_switched_this_turn from ChatOrchestrator after stage detection - Log interview_turn_plan for observability; add unit tests
This commit is contained in:
@@ -15,6 +15,7 @@ from app.agents.chat.interview_state_hints import (
|
|||||||
extract_recent_questions,
|
extract_recent_questions,
|
||||||
update_recent_questions,
|
update_recent_questions,
|
||||||
)
|
)
|
||||||
|
from app.agents.chat.interview_turn_plan import plan_interview_turn
|
||||||
from app.agents.chat.personas import normalize_interview_persona
|
from app.agents.chat.personas import normalize_interview_persona
|
||||||
from app.agents.chat.prompt_context import ChatPromptContext
|
from app.agents.chat.prompt_context import ChatPromptContext
|
||||||
from app.agents.chat.prompts_conversation import (
|
from app.agents.chat.prompts_conversation import (
|
||||||
@@ -99,6 +100,7 @@ class InterviewAgent:
|
|||||||
occupation: str = "",
|
occupation: str = "",
|
||||||
profile_birth_year: int | None = None,
|
profile_birth_year: int | None = None,
|
||||||
profile_era_place: str = "",
|
profile_era_place: str = "",
|
||||||
|
stage_switched_this_turn: bool = False,
|
||||||
) -> AgentChatTurn:
|
) -> AgentChatTurn:
|
||||||
"""生成状态感知的访谈回复,不持久化(由 Orchestrator 负责)"""
|
"""生成状态感知的访谈回复,不持久化(由 Orchestrator 负责)"""
|
||||||
if not self.llm:
|
if not self.llm:
|
||||||
@@ -133,6 +135,20 @@ class InterviewAgent:
|
|||||||
max_tokens = int(settings.chat_interview_max_tokens)
|
max_tokens = int(settings.chat_interview_max_tokens)
|
||||||
max_chars = int(settings.chat_interview_max_chars_per_segment)
|
max_chars = int(settings.chat_interview_max_chars_per_segment)
|
||||||
|
|
||||||
|
turn_plan = plan_interview_turn(
|
||||||
|
current_stage=memoir_state.current_stage,
|
||||||
|
empty_slots=empty_slots,
|
||||||
|
normalized_user_message=text_for_model,
|
||||||
|
memory_evidence_text=memory_evidence_text,
|
||||||
|
stage_switched_this_turn=stage_switched_this_turn,
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"event=interview_turn_plan mode={} anchor_slot={} snippet_len={}",
|
||||||
|
turn_plan.mode,
|
||||||
|
turn_plan.anchor_slot_key or "-",
|
||||||
|
len(turn_plan.anchor_snippet or ""),
|
||||||
|
)
|
||||||
|
|
||||||
ctx = ChatPromptContext(
|
ctx = ChatPromptContext(
|
||||||
current_stage=memoir_state.current_stage,
|
current_stage=memoir_state.current_stage,
|
||||||
empty_slots=empty_slots,
|
empty_slots=empty_slots,
|
||||||
@@ -149,6 +165,7 @@ class InterviewAgent:
|
|||||||
known_facts=memoir_state.known_facts,
|
known_facts=memoir_state.known_facts,
|
||||||
persona_threads=memoir_state.persona_threads,
|
persona_threads=memoir_state.persona_threads,
|
||||||
recent_questions=recent_questions or memoir_state.recent_questions,
|
recent_questions=recent_questions or memoir_state.recent_questions,
|
||||||
|
turn_plan=turn_plan,
|
||||||
)
|
)
|
||||||
system_prompt = ctx.guided_system_prompt()
|
system_prompt = ctx.guided_system_prompt()
|
||||||
messages: List[Any] = [SystemMessage(content=system_prompt)]
|
messages: List[Any] = [SystemMessage(content=system_prompt)]
|
||||||
|
|||||||
198
api/app/agents/chat/interview_turn_plan.py
Normal file
198
api/app/agents/chat/interview_turn_plan.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
"""
|
||||||
|
访谈轮次编排(方案 A):由服务端显式给出 turn_mode / 主槽 / 挂钩摘录,
|
||||||
|
减少仅靠长 prompt 软约束时模型「随便问、不往回忆录引」的漂移。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from app.agents.chat.prompts_conversation import SLOT_NAME_MAP
|
||||||
|
from app.agents.stage_constants import STAGE_SLOT_KEYS
|
||||||
|
|
||||||
|
InterviewTurnMode = Literal["emotion_first", "memoir_push", "follow_user_only"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class InterviewTurnPlan:
|
||||||
|
"""单轮访谈的硬目标(供注入 system prompt 顶部,优先级高于一般性建议)。"""
|
||||||
|
|
||||||
|
mode: InterviewTurnMode
|
||||||
|
anchor_slot_key: str | None
|
||||||
|
anchor_slot_readable: str
|
||||||
|
anchor_snippet: str
|
||||||
|
|
||||||
|
|
||||||
|
def primary_empty_slot(stage: str, empty_slots: list[str]) -> str | None:
|
||||||
|
"""按 STAGE_SLOT_KEYS 顺序取第一个仍空的槽。"""
|
||||||
|
if not empty_slots:
|
||||||
|
return None
|
||||||
|
order = STAGE_SLOT_KEYS.get(stage, ())
|
||||||
|
for key in order:
|
||||||
|
if key in empty_slots:
|
||||||
|
return key
|
||||||
|
return empty_slots[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_scene_hint(memory_evidence_text: str) -> str:
|
||||||
|
raw = (memory_evidence_text or "").strip()
|
||||||
|
if "[场景氛围提示" in raw:
|
||||||
|
raw = raw.split("[场景氛围提示", 1)[0].strip()
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def extract_anchor_snippet(
|
||||||
|
*,
|
||||||
|
memory_evidence_text: str,
|
||||||
|
user_message: str,
|
||||||
|
max_chars: int = 180,
|
||||||
|
) -> str:
|
||||||
|
"""优先记忆摘录,其次用户原话(用于追问挂钩,非事实断言)。"""
|
||||||
|
mem = _strip_scene_hint(memory_evidence_text)
|
||||||
|
if mem and len(mem) >= 4:
|
||||||
|
return mem[:max_chars].strip()
|
||||||
|
um = (user_message or "").strip()
|
||||||
|
if len(um) >= 10:
|
||||||
|
return um[:max_chars].strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
_EMOTION_MARKERS: tuple[str, ...] = (
|
||||||
|
"哭",
|
||||||
|
"难受",
|
||||||
|
"委屈",
|
||||||
|
"害怕",
|
||||||
|
"后悔",
|
||||||
|
"恨",
|
||||||
|
"舍不得",
|
||||||
|
"崩溃",
|
||||||
|
"绝望",
|
||||||
|
"心疼",
|
||||||
|
"哽咽",
|
||||||
|
"咽不下",
|
||||||
|
"睡不着",
|
||||||
|
"想哭",
|
||||||
|
"好难",
|
||||||
|
"太难了",
|
||||||
|
"挺不住",
|
||||||
|
"扛不住",
|
||||||
|
"放不下",
|
||||||
|
"意难平",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_emotion_heavy(text: str) -> bool:
|
||||||
|
t = (text or "").strip()
|
||||||
|
if not t:
|
||||||
|
return False
|
||||||
|
if any(m in t for m in _EMOTION_MARKERS):
|
||||||
|
return True
|
||||||
|
if len(t) >= 40 and ("!" in t or "!" in t) and (".." in t or "…" in t or "唉" in t):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def plan_interview_turn(
|
||||||
|
*,
|
||||||
|
current_stage: str,
|
||||||
|
empty_slots: list[str],
|
||||||
|
normalized_user_message: str,
|
||||||
|
memory_evidence_text: str,
|
||||||
|
stage_switched_this_turn: bool,
|
||||||
|
) -> InterviewTurnPlan:
|
||||||
|
"""
|
||||||
|
粗规则(可迭代):
|
||||||
|
- 情绪浓:先共情,不强推叙述槽搜集问。
|
||||||
|
- 刚切换人生阶段:跟着用户节奏,不做「新阶段问卷首开」。
|
||||||
|
- 当前阶段无空槽:深度跟进,不重启盘点。
|
||||||
|
- 默认:memoir_push,锁一个主槽 + 挂钩摘录。
|
||||||
|
"""
|
||||||
|
snippet = extract_anchor_snippet(
|
||||||
|
memory_evidence_text=memory_evidence_text,
|
||||||
|
user_message=normalized_user_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
if _is_emotion_heavy(normalized_user_message):
|
||||||
|
slot = primary_empty_slot(current_stage, empty_slots)
|
||||||
|
readable = (
|
||||||
|
SLOT_NAME_MAP.get(slot, slot or "")
|
||||||
|
if slot
|
||||||
|
else "(情绪优先时可暂不强绑某一槽位)"
|
||||||
|
)
|
||||||
|
return InterviewTurnPlan(
|
||||||
|
mode="emotion_first",
|
||||||
|
anchor_slot_key=slot,
|
||||||
|
anchor_slot_readable=readable,
|
||||||
|
anchor_snippet=snippet,
|
||||||
|
)
|
||||||
|
|
||||||
|
if stage_switched_this_turn:
|
||||||
|
return InterviewTurnPlan(
|
||||||
|
mode="follow_user_only",
|
||||||
|
anchor_slot_key=None,
|
||||||
|
anchor_slot_readable="(刚自然谈到本阶段,先顺着对方语势,勿问卷式首开)",
|
||||||
|
anchor_snippet=snippet,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not empty_slots:
|
||||||
|
return InterviewTurnPlan(
|
||||||
|
mode="follow_user_only",
|
||||||
|
anchor_slot_key=None,
|
||||||
|
anchor_slot_readable="(本阶段主要叙述槽已有素材)请 depth-first:接续画面或情绪线,别重启童年在哪长大式盘点",
|
||||||
|
anchor_snippet=snippet,
|
||||||
|
)
|
||||||
|
|
||||||
|
slot = primary_empty_slot(current_stage, empty_slots)
|
||||||
|
assert slot is not None
|
||||||
|
return InterviewTurnPlan(
|
||||||
|
mode="memoir_push",
|
||||||
|
anchor_slot_key=slot,
|
||||||
|
anchor_slot_readable=SLOT_NAME_MAP.get(slot, slot),
|
||||||
|
anchor_snippet=snippet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_interview_turn_directive_block(plan: InterviewTurnPlan) -> str:
|
||||||
|
"""注入 guided prompt 顶部的硬指令块。"""
|
||||||
|
snippet_line = (
|
||||||
|
plan.anchor_snippet
|
||||||
|
if plan.anchor_snippet
|
||||||
|
else "(无可用摘录时,必须从用户本轮原话里抽词作挂钩,禁止编造)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if plan.mode == "emotion_first":
|
||||||
|
mode_rules = (
|
||||||
|
"- **情绪优先**:本轮以承接、并肩与安全感为主,**不要**为推进「叙述槽大纲」而追加信息搜集型追问。\n"
|
||||||
|
"- 若末尾带问,只能是**贴着用户当前情绪或原词**的极轻一句;禁止切到盘点式下一题。\n"
|
||||||
|
"- 参考主槽「"
|
||||||
|
+ plan.anchor_slot_readable
|
||||||
|
+ "」仅供你心里知道后续方向,**不要**在本轮用问卷口吻硬推该槽。"
|
||||||
|
)
|
||||||
|
elif plan.mode == "follow_user_only":
|
||||||
|
mode_rules = (
|
||||||
|
"- **跟话头**:本轮禁止问卷式首开、禁止重启式盘点;顺着用户刚展开的画面、人物或情绪自然往下。\n"
|
||||||
|
"- 若带问句,最多**一个**,且必须**从用户原词或下面摘录**长出来,禁止空泛「还有吗」。"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
mode_rules = (
|
||||||
|
"- **回忆推进(memoir_push)**:对用户可见回复中须有**恰好一个**开放式回忆追问,\n"
|
||||||
|
" 且意图明显在补足下面「主追问方向」;问句必须挂住**挂钩摘录**或**用户本轮原词**(二者至少其一)。\n"
|
||||||
|
"- 禁止用日常寒暄替代该追问;仍遵守全篇「最多一个问句」「禁止晚会硬切」。"
|
||||||
|
)
|
||||||
|
|
||||||
|
return f"""## 本轮编排指令(硬规则,优先于后文一般性建议)
|
||||||
|
{mode_rules}
|
||||||
|
- **主追问方向(叙述槽)**:{plan.anchor_slot_readable}
|
||||||
|
- **挂钩摘录**(仅作衔接线索,**不是**用户本轮新说的内容;禁止写成就等于用户刚讲的原话):{snippet_line}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"InterviewTurnMode",
|
||||||
|
"InterviewTurnPlan",
|
||||||
|
"extract_anchor_snippet",
|
||||||
|
"format_interview_turn_directive_block",
|
||||||
|
"plan_interview_turn",
|
||||||
|
"primary_empty_slot",
|
||||||
|
]
|
||||||
@@ -254,12 +254,14 @@ class ChatOrchestrator:
|
|||||||
is_from_voice=is_from_voice,
|
is_from_voice=is_from_voice,
|
||||||
)
|
)
|
||||||
state = await get_or_create_state(user_id, db)
|
state = await get_or_create_state(user_id, db)
|
||||||
|
stage_before = state.current_stage
|
||||||
detected = await detect_primary_life_stage(
|
detected = await detect_primary_life_stage(
|
||||||
normalized_user_message,
|
normalized_user_message,
|
||||||
state.current_stage,
|
state.current_stage,
|
||||||
self.interview_agent.llm,
|
self.interview_agent.llm,
|
||||||
)
|
)
|
||||||
if detected != state.current_stage:
|
stage_switched_this_turn = detected != stage_before
|
||||||
|
if stage_switched_this_turn:
|
||||||
state = await switch_stage(user_id, detected, db)
|
state = await switch_stage(user_id, detected, db)
|
||||||
|
|
||||||
if conversation and conversation.conversation_stage != state.current_stage:
|
if conversation and conversation.conversation_stage != state.current_stage:
|
||||||
@@ -317,6 +319,7 @@ class ChatOrchestrator:
|
|||||||
occupation=occupation,
|
occupation=occupation,
|
||||||
profile_birth_year=profile_birth_year,
|
profile_birth_year=profile_birth_year,
|
||||||
profile_era_place=profile_era_place,
|
profile_era_place=profile_era_place,
|
||||||
|
stage_switched_this_turn=stage_switched_this_turn,
|
||||||
)
|
)
|
||||||
recent_questions = prompt_state.recent_questions
|
recent_questions = prompt_state.recent_questions
|
||||||
if turn.interview_state_meta and isinstance(turn.interview_state_meta, dict):
|
if turn.interview_state_meta and isinstance(turn.interview_state_meta, dict):
|
||||||
@@ -413,6 +416,7 @@ class ChatOrchestrator:
|
|||||||
occupation: str = "",
|
occupation: str = "",
|
||||||
profile_birth_year: int | None = None,
|
profile_birth_year: int | None = None,
|
||||||
profile_era_place: str = "",
|
profile_era_place: str = "",
|
||||||
|
stage_switched_this_turn: bool = False,
|
||||||
) -> AgentChatTurn:
|
) -> AgentChatTurn:
|
||||||
"""委托 InterviewAgent 生成访谈回复(持久化由调用方负责)。"""
|
"""委托 InterviewAgent 生成访谈回复(持久化由调用方负责)。"""
|
||||||
return await self.interview_agent.generate_response_with_state(
|
return await self.interview_agent.generate_response_with_state(
|
||||||
@@ -427,6 +431,7 @@ class ChatOrchestrator:
|
|||||||
occupation=occupation,
|
occupation=occupation,
|
||||||
profile_birth_year=profile_birth_year,
|
profile_birth_year=profile_birth_year,
|
||||||
profile_era_place=profile_era_place,
|
profile_era_place=profile_era_place,
|
||||||
|
stage_switched_this_turn=stage_switched_this_turn,
|
||||||
)
|
)
|
||||||
|
|
||||||
def detect_user_stage(self, user_message: str) -> str:
|
def detect_user_stage(self, user_message: str) -> str:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from app.agents.chat.interview_turn_plan import InterviewTurnPlan
|
||||||
from app.agents.state_schema import KnownFact, PersonaThread
|
from app.agents.state_schema import KnownFact, PersonaThread
|
||||||
|
|
||||||
|
|
||||||
@@ -27,11 +28,20 @@ class ChatPromptContext:
|
|||||||
known_facts: List[KnownFact] | None = None
|
known_facts: List[KnownFact] | None = None
|
||||||
persona_threads: List[PersonaThread] | None = None
|
persona_threads: List[PersonaThread] | None = None
|
||||||
recent_questions: List[str] | None = None
|
recent_questions: List[str] | None = None
|
||||||
|
turn_plan: InterviewTurnPlan | None = None
|
||||||
|
|
||||||
def guided_system_prompt(self) -> str:
|
def guided_system_prompt(self) -> str:
|
||||||
"""用户原话仅以对话历史 + HumanMessage 注入模型。"""
|
"""用户原话仅以对话历史 + HumanMessage 注入模型。"""
|
||||||
|
from app.agents.chat.interview_turn_plan import (
|
||||||
|
format_interview_turn_directive_block,
|
||||||
|
)
|
||||||
from app.agents.chat.prompts_conversation import get_guided_conversation_prompt
|
from app.agents.chat.prompts_conversation import get_guided_conversation_prompt
|
||||||
|
|
||||||
|
directive = (
|
||||||
|
format_interview_turn_directive_block(self.turn_plan)
|
||||||
|
if self.turn_plan is not None
|
||||||
|
else ""
|
||||||
|
)
|
||||||
return get_guided_conversation_prompt(
|
return get_guided_conversation_prompt(
|
||||||
current_stage=self.current_stage,
|
current_stage=self.current_stage,
|
||||||
empty_slots=self.empty_slots,
|
empty_slots=self.empty_slots,
|
||||||
@@ -48,4 +58,5 @@ class ChatPromptContext:
|
|||||||
known_facts=self.known_facts or [],
|
known_facts=self.known_facts or [],
|
||||||
persona_threads=self.persona_threads or [],
|
persona_threads=self.persona_threads or [],
|
||||||
recent_questions=self.recent_questions or [],
|
recent_questions=self.recent_questions or [],
|
||||||
|
turn_directive_block=directive,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ def get_guided_conversation_prompt(
|
|||||||
known_facts: list[KnownFact] | None = None,
|
known_facts: list[KnownFact] | None = None,
|
||||||
persona_threads: list[PersonaThread] | None = None,
|
persona_threads: list[PersonaThread] | None = None,
|
||||||
recent_questions: list[str] | None = None,
|
recent_questions: list[str] | None = None,
|
||||||
|
turn_directive_block: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""生成状态感知的对话提示词;用户原话仅以 HumanMessage 传入,不写入本 system 文本。"""
|
"""生成状态感知的对话提示词;用户原话仅以 HumanMessage 传入,不写入本 system 文本。"""
|
||||||
persona_key = normalize_interview_persona(persona)
|
persona_key = normalize_interview_persona(persona)
|
||||||
@@ -365,7 +366,9 @@ def get_guided_conversation_prompt(
|
|||||||
current_stage, empty_slots
|
current_stage, empty_slots
|
||||||
)
|
)
|
||||||
|
|
||||||
return f"""你是「岁月知己」——**主持式知己**:语气像最懂我的老朋友,**职责是帮用户把人生故事口述清楚**。{tone_line}
|
_prefix = f"{turn_directive_block.rstrip()}\n\n" if (turn_directive_block or "").strip() else ""
|
||||||
|
|
||||||
|
return f"""{_prefix}你是「岁月知己」——**主持式知己**:语气像最懂我的老朋友,**职责是帮用户把人生故事口述清楚**。{tone_line}
|
||||||
|
|
||||||
{topic_desc}
|
{topic_desc}
|
||||||
|
|
||||||
|
|||||||
65
api/tests/test_interview_turn_plan.py
Normal file
65
api/tests/test_interview_turn_plan.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
"""interview_turn_plan:轮次模式与主槽选择(服务端硬编排)。"""
|
||||||
|
|
||||||
|
from app.agents.chat.interview_turn_plan import (
|
||||||
|
extract_anchor_snippet,
|
||||||
|
plan_interview_turn,
|
||||||
|
primary_empty_slot,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_primary_empty_slot_order():
|
||||||
|
assert primary_empty_slot("childhood", ["emotion", "place"]) == "place"
|
||||||
|
assert primary_empty_slot("childhood", ["emotion"]) == "emotion"
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_anchor_snippet_prefers_memory():
|
||||||
|
mem = "摘录的一段记忆\n\n[场景氛围提示"
|
||||||
|
assert "摘录的一段记忆" in extract_anchor_snippet(
|
||||||
|
memory_evidence_text=mem, user_message="用户说很长一句" * 3
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan_memoir_push():
|
||||||
|
p = plan_interview_turn(
|
||||||
|
current_stage="childhood",
|
||||||
|
empty_slots=["place", "people"],
|
||||||
|
normalized_user_message="我小时候住在河边,夏天常去玩水。",
|
||||||
|
memory_evidence_text="",
|
||||||
|
stage_switched_this_turn=False,
|
||||||
|
)
|
||||||
|
assert p.mode == "memoir_push"
|
||||||
|
assert p.anchor_slot_key == "place"
|
||||||
|
assert p.anchor_snippet
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan_emotion_first():
|
||||||
|
p = plan_interview_turn(
|
||||||
|
current_stage="childhood",
|
||||||
|
empty_slots=["place"],
|
||||||
|
normalized_user_message="想起来还是很难受,忍不住想哭。",
|
||||||
|
memory_evidence_text="",
|
||||||
|
stage_switched_this_turn=False,
|
||||||
|
)
|
||||||
|
assert p.mode == "emotion_first"
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan_follow_on_stage_switch():
|
||||||
|
p = plan_interview_turn(
|
||||||
|
current_stage="education",
|
||||||
|
empty_slots=["school", "city"],
|
||||||
|
normalized_user_message="后来我去省城读中学了。",
|
||||||
|
memory_evidence_text="",
|
||||||
|
stage_switched_this_turn=True,
|
||||||
|
)
|
||||||
|
assert p.mode == "follow_user_only"
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan_follow_when_no_empty_slots():
|
||||||
|
p = plan_interview_turn(
|
||||||
|
current_stage="childhood",
|
||||||
|
empty_slots=[],
|
||||||
|
normalized_user_message="嗯。",
|
||||||
|
memory_evidence_text="",
|
||||||
|
stage_switched_this_turn=False,
|
||||||
|
)
|
||||||
|
assert p.mode == "follow_user_only"
|
||||||
Reference in New Issue
Block a user