143 lines
4.7 KiB
Python
143 lines
4.7 KiB
Python
from typing import Any
|
|||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from app.service.goal_conversation_service import (
|
||
|
|
MAX_CLARIFICATION_ROUNDS,
|
||
|
|
REQUIRED_FIELDS,
|
||
|
|
GoalConversationService,
|
||
|
|
extract_goal_entities,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_extracts_explicit_goal_entities() -> None:
|
||
|
|
result = extract_goal_entities(
|
||
|
|
"我希望年化收益6%-10%,最大回撤15%,7天内能用钱,期限3年,基准沪深300指数"
|
||
|
|
)
|
||
|
|
|
||
|
|
assert result == {
|
||
|
|
"annualized_return_lower_pct": 6.0,
|
||
|
|
"annualized_return_upper_pct": 10.0,
|
||
|
|
"max_drawdown_pct": 15.0,
|
||
|
|
"liquidity_requirement": "within_7_days",
|
||
|
|
"investment_horizon_months": 36,
|
||
|
|
"benchmark_name": "沪深300指数",
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def test_collection_detection_does_not_intercept_goal_read() -> None:
|
||
|
|
assert GoalConversationService.should_collect("我想制定投资目标")
|
||
|
|
assert GoalConversationService.should_collect("收益目标是8%")
|
||
|
|
assert not GoalConversationService.should_collect("查看我的投资目标")
|
||
|
|
|
||
|
|
|
||
|
|
class _AsyncContext:
|
||
|
|
async def __aenter__(self) -> None:
|
||
|
|
return None
|
||
|
|
|
||
|
|
async def __aexit__(self, *_args: object) -> bool:
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
class _SessionRow:
|
||
|
|
clarification_round = 0
|
||
|
|
|
||
|
|
|
||
|
|
class _Message:
|
||
|
|
def __init__(self, message_id: int, content: str, trace_id: str) -> None:
|
||
|
|
self.id = message_id
|
||
|
|
self.content = content
|
||
|
|
self.trace_id = trace_id
|
||
|
|
|
||
|
|
|
||
|
|
class _FakeSession:
|
||
|
|
def __init__(self, messages: list[_Message], rounds: int = 0) -> None:
|
||
|
|
self.session_row = _SessionRow()
|
||
|
|
self.session_row.clarification_round = rounds
|
||
|
|
self.messages = messages
|
||
|
|
self.added: list[Any] = []
|
||
|
|
self.scalar_calls = 0
|
||
|
|
|
||
|
|
def begin(self) -> _AsyncContext:
|
||
|
|
return _AsyncContext()
|
||
|
|
|
||
|
|
async def scalar(self, _statement: object) -> object:
|
||
|
|
self.scalar_calls += 1
|
||
|
|
if self.scalar_calls == 1:
|
||
|
|
return self.session_row
|
||
|
|
if self.scalar_calls == 2:
|
||
|
|
return next((item for item in self.messages if item.trace_id == "trace-2"), None)
|
||
|
|
return None
|
||
|
|
|
||
|
|
async def scalars(self, _statement: object) -> list[_Message]:
|
||
|
|
return self.messages
|
||
|
|
|
||
|
|
def add(self, item: Any) -> None:
|
||
|
|
self.added.append(item)
|
||
|
|
|
||
|
|
|
||
|
|
class _SessionFactory:
|
||
|
|
def __init__(self, session: _FakeSession) -> None:
|
||
|
|
self.session = session
|
||
|
|
|
||
|
|
def __call__(self) -> "_SessionContext":
|
||
|
|
return _SessionContext(self.session)
|
||
|
|
|
||
|
|
|
||
|
|
class _SessionContext:
|
||
|
|
def __init__(self, session: _FakeSession) -> None:
|
||
|
|
self.session = session
|
||
|
|
|
||
|
|
async def __aenter__(self) -> _FakeSession:
|
||
|
|
return self.session
|
||
|
|
|
||
|
|
async def __aexit__(self, *_args: object) -> bool:
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_process_merges_turns_and_persists_only_internal_snapshot() -> None:
|
||
|
|
session = _FakeSession([
|
||
|
|
_Message(1, "我想做投资目标,收益6%-10%,最大回撤15%", "trace-1"),
|
||
|
|
_Message(2, "7天内用钱,期限3年,基准沪深300", "trace-2"),
|
||
|
|
])
|
||
|
|
|
||
|
|
result = await GoalConversationService(_SessionFactory(session)).process(
|
||
|
|
session_id="session-1", customer_id=9000001, trace_id="trace-2",
|
||
|
|
message=session.messages[-1].content,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert result["status"] == "complete"
|
||
|
|
assert result["missing"] == []
|
||
|
|
assert session.session_row.clarification_round == 0
|
||
|
|
extraction = next(
|
||
|
|
item for item in session.added
|
||
|
|
if item.__class__.__name__ == "AdvisorGoalConversationExtraction"
|
||
|
|
)
|
||
|
|
assert extraction.missing_fields == []
|
||
|
|
assert extraction.extracted_fields["investment_horizon_months"] == 36
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_process_asks_only_for_missing_fields_and_honors_limit() -> None:
|
||
|
|
session = _FakeSession([_Message(3, "我希望年化收益6%-10%", "trace-2")])
|
||
|
|
|
||
|
|
result = await GoalConversationService(_SessionFactory(session)).process(
|
||
|
|
session_id="session-1", customer_id=9000001, trace_id="trace-2",
|
||
|
|
message="我希望年化收益6%-10%",
|
||
|
|
)
|
||
|
|
|
||
|
|
assert result["status"] == "partial"
|
||
|
|
assert result["missing"] == [field for field in REQUIRED_FIELDS if field not in {
|
||
|
|
"annualized_return_lower_pct", "annualized_return_upper_pct"
|
||
|
|
}]
|
||
|
|
assert session.session_row.clarification_round == 1
|
||
|
|
assert "年化收益目标下限" not in GoalConversationService.customer_prompt(result)
|
||
|
|
|
||
|
|
limited = _FakeSession(session.messages, rounds=MAX_CLARIFICATION_ROUNDS)
|
||
|
|
limited_result = await GoalConversationService(_SessionFactory(limited)).process(
|
||
|
|
session_id="session-1", customer_id=9000001, trace_id="trace-2", message="我还没想好"
|
||
|
|
)
|
||
|
|
assert limited_result["status"] == "clarification_limit"
|
||
|
|
assert "转人工顾问" in GoalConversationService.customer_prompt(limited_result)
|