Files
life-echo/api/tests/test_interview_reply_length.py

208 lines
6.1 KiB
Python
Raw Normal View History

"""访谈回复长度策略:分桶与 InterviewAgent 的 max_tokens / 截断联动。"""
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
refactor(agents): 抽取阶段常量与对话上下文;快档 LLM;图片 prompt 可禁止回退 访谈与阶段 - 新增 app/agents/stage_constants.py:集中 CHAT_STAGES、章节分类/顺序、阶段到默认 memoir 类别等,与 MemoirState 默认槽位顺序对齐;减少散落在 prompts 内的重复常量。 - 新增 app/agents/chat/prompt_context.py:以 ChatPromptContext 汇总 guided 系统提示所需字段(阶段、槽位、轮次、人设、记忆证据、回复长度模式、背景声线、职业等),统一走 get_guided_conversation_prompt。 - 大幅收敛 app/agents/chat/prompts_conversation.py;调整 prompts.py、stage_prompts.py、stage_detection.py;同步 interview_agent、profile_agent、helpers 与 state_schema,使对话侧构造提示的方式一致、可测。 回忆录流水线 - memoir/prompts.py 删除已迁至 stage_constants / 独立模板的大段常量与图片占位相关逻辑;classification / extraction / fidelity / narrative agents 与 orchest(全量历史仍可用于计数,注入模型时按轮次与字符上限截断)、image_prompt_fallback_disabled。 - dependencies 增加 get_llm_provider_fast(LRU 缓存,可与默认共用密钥与 base_url)。 任务与编排 - memoir_tasks:prepare_batches 注入 llm_fast;开启独立快档模型时打结构化日志。 - chapter_cover_tasks、story_image_tasks:与图片 prompt / JSON 工具路径或策略变更对齐(import 与行为一致)。 - story_pipeline_sync 等小处同步。 其它核心 - langchain_llm、text_normalize 随上述调用链微调。 开发者体验 - .cursor/settings.json:启用 redis-development、postman 插件。 测试 - 新增 test_image_prompt_policy:覆盖「禁止回退」等图片 prompt 策略。 - 更新 test_interview_prompts、test_interview_reply_length、test_experience_regressions、test_json_and_memory_utils,匹配新常量位置、json_utils 与对话/长度行为。
2026-04-02 12:00:00 +08:00
from app.agents.chat.helpers import HistoryWithWindow
from app.agents.chat.interview_reply_length import (
ReplyLengthMode,
bump_reply_plan_for_background_voice,
compute_reply_plan,
)
from app.agents.state_schema import MemoirStateSchema
def _fake_settings(**overrides: object) -> SimpleNamespace:
base = {
"chat_interview_max_tokens": 380,
"chat_interview_max_segments": 2,
"chat_interview_max_chars_per_segment": 260,
"chat_interview_brief_max_tokens": 260,
"chat_interview_brief_max_chars_per_segment": 200,
"chat_interview_expanded_max_tokens": 520,
"chat_interview_expanded_max_chars_per_segment": 380,
}
base.update(overrides)
return SimpleNamespace(**base)
def test_strategy_brief_when_very_short() -> None:
s = compute_reply_plan(
"x" * 5,
background_voice=None,
settings=_fake_settings(),
)
assert s.mode == ReplyLengthMode.brief
assert s.max_tokens == 260
assert s.max_chars_per_segment == 200
def test_strategy_standard_mid_length() -> None:
s = compute_reply_plan(
"x" * 50,
background_voice=None,
settings=_fake_settings(),
)
assert s.mode == ReplyLengthMode.standard
assert s.max_tokens == 380
assert s.max_chars_per_segment == 260
def test_strategy_long_chit_stays_standard() -> None:
msg = "今天天气真好哈哈" * 11
assert len(msg) >= 80
s = compute_reply_plan(
msg,
background_voice=None,
settings=_fake_settings(),
)
assert s.mode == ReplyLengthMode.standard
assert s.max_tokens == 380
def test_strategy_long_with_new_detail_expanded() -> None:
base = "第一次认识他"
msg = (base + "x" * 200)[:120]
assert len(msg) == 120
s = compute_reply_plan(
msg,
background_voice=None,
settings=_fake_settings(),
)
assert s.mode == ReplyLengthMode.expanded
assert s.max_tokens == 520
assert s.max_chars_per_segment == 380
def test_strategy_boundary_len_20_brief_len_21_standard() -> None:
a = compute_reply_plan(
"x" * 20,
background_voice=None,
settings=_fake_settings(),
)
b = compute_reply_plan(
"x" * 21,
background_voice=None,
settings=_fake_settings(),
)
assert a.mode == ReplyLengthMode.brief
assert b.mode == ReplyLengthMode.standard
def test_bump_standard_only_for_cadre_military() -> None:
s0 = compute_reply_plan(
"x" * 50,
background_voice=None,
settings=_fake_settings(),
)
bumped = bump_reply_plan_for_background_voice(
s0,
background_voice="cadre",
settings=_fake_settings(
chat_interview_cadre_military_standard_extra_tokens=40,
chat_interview_cadre_military_standard_extra_chars=40,
),
)
assert bumped.max_tokens == s0.max_tokens + 40
assert bumped.max_chars_per_segment == s0.max_chars_per_segment + 40
brief = compute_reply_plan(
"x" * 5,
background_voice=None,
settings=_fake_settings(
chat_interview_cadre_military_standard_extra_tokens=40,
chat_interview_cadre_military_standard_extra_chars=40,
),
)
same = bump_reply_plan_for_background_voice(
brief,
background_voice="military",
settings=_fake_settings(
chat_interview_cadre_military_standard_extra_tokens=40,
chat_interview_cadre_military_standard_extra_chars=40,
),
)
assert same.max_tokens == brief.max_tokens
def test_plan_short_information_rich_is_standard_not_brief() -> None:
"""短句但含高密度锚点(如「那年」「我爸」)→ standard避免误压成 brief。"""
p = compute_reply_plan(
"那年我爸突然病了",
background_voice=None,
settings=_fake_settings(),
)
assert p.mode == ReplyLengthMode.standard
assert p.information_rich is True
def test_plan_long_chit_stays_standard_not_expanded() -> None:
"""长段明显闲聊 → standard不因字数进入 expanded。"""
msg = "今天天气真好哈哈" * 11
assert len(msg) >= 80
p = compute_reply_plan(
msg,
background_voice=None,
settings=_fake_settings(),
)
assert p.mode == ReplyLengthMode.standard
assert p.likely_chit_chat is True
def test_strategy_boundary_len_79_standard_len_80_long_branch() -> None:
a = compute_reply_plan(
"x" * 79,
background_voice=None,
settings=_fake_settings(),
)
b = compute_reply_plan(
"x" * 80,
background_voice=None,
settings=_fake_settings(),
)
assert a.mode == ReplyLengthMode.standard
assert b.mode == ReplyLengthMode.standard
@pytest.mark.asyncio
async def test_interview_agent_passes_strategy_to_bind_and_truncate() -> None:
"""同一套 strategy 用于 llm.bind(max_tokens=) 与 truncate_chat_segments。"""
from app.agents.chat import interview_agent as ia
mock_llm = MagicMock()
mock_bound = MagicMock()
mock_bound.ainvoke = AsyncMock(
return_value=MagicMock(content="你好,后来呢?[SPLIT]还有吗?")
)
mock_llm.bind = MagicMock(return_value=mock_bound)
agent = ia.InterviewAgent()
agent.llm = mock_llm
state = MemoirStateSchema(
stage_order=["childhood"],
current_stage="childhood",
covered_stages=[],
slots={"childhood": {}},
)
with patch(
refactor(agents): 抽取阶段常量与对话上下文;快档 LLM;图片 prompt 可禁止回退 访谈与阶段 - 新增 app/agents/stage_constants.py:集中 CHAT_STAGES、章节分类/顺序、阶段到默认 memoir 类别等,与 MemoirState 默认槽位顺序对齐;减少散落在 prompts 内的重复常量。 - 新增 app/agents/chat/prompt_context.py:以 ChatPromptContext 汇总 guided 系统提示所需字段(阶段、槽位、轮次、人设、记忆证据、回复长度模式、背景声线、职业等),统一走 get_guided_conversation_prompt。 - 大幅收敛 app/agents/chat/prompts_conversation.py;调整 prompts.py、stage_prompts.py、stage_detection.py;同步 interview_agent、profile_agent、helpers 与 state_schema,使对话侧构造提示的方式一致、可测。 回忆录流水线 - memoir/prompts.py 删除已迁至 stage_constants / 独立模板的大段常量与图片占位相关逻辑;classification / extraction / fidelity / narrative agents 与 orchest(全量历史仍可用于计数,注入模型时按轮次与字符上限截断)、image_prompt_fallback_disabled。 - dependencies 增加 get_llm_provider_fast(LRU 缓存,可与默认共用密钥与 base_url)。 任务与编排 - memoir_tasks:prepare_batches 注入 llm_fast;开启独立快档模型时打结构化日志。 - chapter_cover_tasks、story_image_tasks:与图片 prompt / JSON 工具路径或策略变更对齐(import 与行为一致)。 - story_pipeline_sync 等小处同步。 其它核心 - langchain_llm、text_normalize 随上述调用链微调。 开发者体验 - .cursor/settings.json:启用 redis-development、postman 插件。 测试 - 新增 test_image_prompt_policy:覆盖「禁止回退」等图片 prompt 策略。 - 更新 test_interview_prompts、test_interview_reply_length、test_experience_regressions、test_json_and_memory_utils,匹配新常量位置、json_utils 与对话/长度行为。
2026-04-02 12:00:00 +08:00
"app.agents.chat.interview_agent.get_history_with_window",
new=AsyncMock(return_value=HistoryWithWindow(turn_total=0, window=[])),
):
turn = await agent.generate_response_with_state(
conversation_id="c1",
user_message="x" * 100 + "第一次认识他",
memoir_state=state,
)
mock_llm.bind.assert_called_once()
call_kw = mock_llm.bind.call_args[1]
assert call_kw["max_tokens"] == 520
assert len(turn.messages) >= 1
for seg in turn.messages:
assert len(seg) <= 380