feat(server): 会话上下文保留(LLM 历史 + 确定性兜底双保险)
解决用户删除草稿后说'再提交'丢失上下文的问题: - steward.py 新增 _inject_recent_conversation_history:build_plan 前读最近 10 条对话注入 context_json - steward_intent_agent.py 的 _build_messages 把 recent_history 暴露给模型,system prompt 加确认类话术引导 - 新建 steward_context_resume.py:should_resume_recent_task 检测'再提交'类话术 + state 有可恢复 flow,attach_resumed_task 从 state 恢复 task - 两个 plan 入口(/plans 和 /plans/stream)都已接入双保险 - 后端 67 passed,端到端验证'上海出差→再提交'成功恢复 task
This commit is contained in:
130
server/tests/test_steward_context_resume.py
Normal file
130
server/tests/test_steward_context_resume.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from app.schemas.steward import StewardPlanResponse, StewardTask, StewardThinkingEvent
|
||||
from app.services.steward_context_resume import (
|
||||
RESUME_CONFIRMATION_KEYWORDS,
|
||||
attach_resumed_task,
|
||||
resume_task_from_flow,
|
||||
should_resume_recent_task,
|
||||
)
|
||||
|
||||
|
||||
def _state_with_travel_application(fields: dict | None = None) -> dict:
|
||||
return {
|
||||
"active_flow": "travel_application",
|
||||
"flows": {
|
||||
"travel_application": {
|
||||
"flow_id": "travel_application",
|
||||
"status": "ready_for_confirmation",
|
||||
"fields": fields or {"location": "上海", "time_range": "2026-02-20 至 2026-02-23"},
|
||||
"missing_fields": [],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_should_resume_returns_flow_id_for_confirmation_keyword_with_state():
|
||||
state = _state_with_travel_application()
|
||||
for keyword in ("再提交", "继续提交", "重新提交", "再申请", "重新申请", "那就提交", "继续吧", "再试一次"):
|
||||
assert should_resume_recent_task(keyword, state) == "travel_application", f"keyword={keyword}"
|
||||
|
||||
|
||||
def test_should_resume_returns_none_when_state_empty():
|
||||
assert should_resume_recent_task("再提交", {}) is None
|
||||
assert should_resume_recent_task("再提交", None) is None
|
||||
|
||||
|
||||
def test_should_resume_returns_none_for_non_confirmation_message():
|
||||
state = _state_with_travel_application()
|
||||
assert should_resume_recent_task("今天天气不错", state) is None
|
||||
assert should_resume_recent_task("你好", state) is None
|
||||
assert should_resume_recent_task("查一下差旅标准", state) is None
|
||||
assert should_resume_recent_task("", state) is None
|
||||
|
||||
|
||||
def test_should_resume_returns_none_when_flow_has_no_fields():
|
||||
state = {
|
||||
"active_flow": "travel_application",
|
||||
"flows": {"travel_application": {"fields": {}, "missing_fields": []}},
|
||||
}
|
||||
assert should_resume_recent_task("再提交", state) is None
|
||||
|
||||
|
||||
def test_should_resume_finds_flow_when_active_flow_empty():
|
||||
# active_flow 已清空,但 flows 里仍有可恢复的 flow
|
||||
state = {
|
||||
"active_flow": "",
|
||||
"flows": {
|
||||
"travel_application": {
|
||||
"fields": {"location": "武汉"},
|
||||
}
|
||||
},
|
||||
}
|
||||
assert should_resume_recent_task("再提交", state) == "travel_application"
|
||||
|
||||
|
||||
def test_resume_task_from_flow_restores_travel_application():
|
||||
flow = {
|
||||
"flow_id": "travel_application",
|
||||
"fields": {"location": "上海", "time_range": "2026-02-20 至 2026-02-23"},
|
||||
"missing_fields": [],
|
||||
}
|
||||
task = resume_task_from_flow("travel_application", flow, task_index=1)
|
||||
assert task.task_type == "expense_application"
|
||||
assert task.assigned_agent == "application_assistant"
|
||||
assert task.ontology_fields["location"] == "上海"
|
||||
assert task.requested_action == "submit"
|
||||
assert task.status == "ready_to_delegate" # 无 missing_fields
|
||||
|
||||
|
||||
def test_resume_task_from_flow_marks_needs_confirmation_when_missing_fields():
|
||||
flow = {
|
||||
"fields": {"location": "武汉"},
|
||||
"missing_fields": ["time_range", "reason"],
|
||||
}
|
||||
task = resume_task_from_flow("travel_application", flow)
|
||||
assert task.missing_fields == ["time_range", "reason"]
|
||||
assert task.status == "needs_confirmation"
|
||||
|
||||
|
||||
def test_attach_resumed_task_adds_task_and_thinking_event():
|
||||
plan = StewardPlanResponse(
|
||||
plan_id="plan_test",
|
||||
planning_source="rule_fallback",
|
||||
summary="占位",
|
||||
tasks=[],
|
||||
thinking_events=[],
|
||||
pending_flow_confirmation={"status": "none"},
|
||||
)
|
||||
state = _state_with_travel_application({"location": "上海", "time_range": "2026-02-20 至 2026-02-23"})
|
||||
updated = attach_resumed_task(plan, state, "travel_application")
|
||||
assert len(updated.tasks) == 1
|
||||
assert updated.tasks[0].task_type == "expense_application"
|
||||
assert updated.tasks[0].ontology_fields["location"] == "上海"
|
||||
assert updated.planning_source == "context_resume"
|
||||
# thinking_event 应说明上下文已恢复
|
||||
assert any("恢复" in event.title or "恢复" in event.content for event in updated.thinking_events)
|
||||
|
||||
|
||||
def test_attach_resumed_task_returns_unchanged_when_flow_missing():
|
||||
plan = StewardPlanResponse(
|
||||
plan_id="plan_test",
|
||||
planning_source="rule_fallback",
|
||||
summary="占位",
|
||||
tasks=[],
|
||||
thinking_events=[],
|
||||
pending_flow_confirmation={"status": "none"},
|
||||
)
|
||||
updated = attach_resumed_task(plan, {"flows": {}}, "travel_application")
|
||||
assert updated is plan # 原样返回
|
||||
|
||||
|
||||
def test_resume_keywords_cover_common_variants():
|
||||
# 确认关键词覆盖场景里常见的表述
|
||||
assert "再提交" in RESUME_CONFIRMATION_KEYWORDS
|
||||
assert "继续提交" in RESUME_CONFIRMATION_KEYWORDS
|
||||
assert "重新申请" in RESUME_CONFIRMATION_KEYWORDS
|
||||
# "提交" 单独不在列表里(避免把"首次提交"误判为恢复)
|
||||
assert "提交" not in RESUME_CONFIRMATION_KEYWORDS
|
||||
@@ -98,3 +98,58 @@ def test_steward_intent_system_prompt_mentions_query_intent_guidance() -> None:
|
||||
assert "query_travel_standard" in system_prompt
|
||||
assert "差旅" in system_prompt
|
||||
assert "住宿标准" in system_prompt
|
||||
|
||||
|
||||
def test_steward_intent_system_prompt_includes_conversation_history_guidance() -> None:
|
||||
"""system prompt 应包含'结合对话历史理解确认类话术'的引导。"""
|
||||
from app.services import steward_intent_bootstrap # noqa: F401
|
||||
|
||||
messages = StewardIntentAgent._build_messages(
|
||||
StewardPlanRequest(message="再提交"),
|
||||
base_date=__import__("datetime").date(2026, 6, 24),
|
||||
canonical_fields=["location", "time_range"],
|
||||
)
|
||||
system_prompt = messages[0]["content"]
|
||||
assert "recent_history" in system_prompt
|
||||
assert "再提交" in system_prompt
|
||||
assert "确认类话术" in system_prompt
|
||||
|
||||
|
||||
def test_steward_intent_context_payload_includes_recent_history() -> None:
|
||||
"""context_payload 应携带 recent_history 结构化字段(role + content)。"""
|
||||
import json
|
||||
|
||||
request = StewardPlanRequest(
|
||||
message="再提交",
|
||||
context_json={
|
||||
"recent_history": [
|
||||
{"role": "user", "content": "2026-02-20 至 2026-02-23,去上海出差,火车"},
|
||||
{"role": "assistant", "content": "好的,为您整理出差申请预览。"},
|
||||
{"role": "user", "content": "直接提交"},
|
||||
{"role": "assistant", "content": "检测到重复申请,已暂停提交。"},
|
||||
],
|
||||
},
|
||||
)
|
||||
messages = StewardIntentAgent._build_messages(
|
||||
request,
|
||||
base_date=__import__("datetime").date(2026, 6, 24),
|
||||
canonical_fields=["location", "time_range"],
|
||||
)
|
||||
user_payload = json.loads(messages[1]["content"])
|
||||
assert "recent_history" in user_payload
|
||||
assert len(user_payload["recent_history"]) == 4
|
||||
assert user_payload["recent_history"][0]["role"] == "user"
|
||||
assert "上海" in user_payload["recent_history"][0]["content"]
|
||||
|
||||
|
||||
def test_steward_intent_context_payload_omits_empty_recent_history() -> None:
|
||||
"""无 recent_history 时不应注入空列表。"""
|
||||
import json
|
||||
|
||||
messages = StewardIntentAgent._build_messages(
|
||||
StewardPlanRequest(message="你好"),
|
||||
base_date=__import__("datetime").date(2026, 6, 24),
|
||||
canonical_fields=["location"],
|
||||
)
|
||||
user_payload = json.loads(messages[1]["content"])
|
||||
assert user_payload.get("recent_history", []) == []
|
||||
|
||||
Reference in New Issue
Block a user