test_persona_agent.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. """Persona scoring must remain evidence-based and configurable."""
  2. from __future__ import annotations
  3. import json
  4. import pytest
  5. from src.agents.persona_agent import PersonaAgent
  6. from .fakes import FakeRuntime
  7. def _patterns(**overrides) -> dict:
  8. base = {
  9. "late_night": {"share": 0.0, "count": 0, "amount": 0.0},
  10. "weekend": {"share": 0.0, "count": 0, "amount": 0.0},
  11. "payday_window": {"share": 0.0, "count": 0, "amount": 0.0},
  12. "frequent_small": {"count": 0, "amount": 0.0, "average": 0.0},
  13. }
  14. base.update(overrides)
  15. return base
  16. def test_persona_uses_scoring_and_evidence_validation() -> None:
  17. agent = PersonaAgent(FakeRuntime())
  18. persona, trace = agent.run(
  19. {"expense": 1000.0, "savings_rate": 8.0},
  20. {"餐饮": 350.0, "娱乐": 180.0, "购物": 120.0, "订阅": 0.0, "学习": 0.0},
  21. _patterns(
  22. late_night={"share": 28.0, "count": 5, "amount": 280.0},
  23. frequent_small={"count": 7, "amount": 210.0, "average": 30.0},
  24. ),
  25. [],
  26. )
  27. assert persona["archetype"] == "late_night_focus"
  28. assert persona["primary"] == "夜行消费探索者"
  29. assert persona["score"] >= 52
  30. assert persona["confidence"] == round(persona["score"] / 100, 2)
  31. assert any("深夜消费占比" in item for item in persona["evidence"])
  32. assert trace["llm_role"].startswith("仅生成")
  33. assert trace["candidates"][0]["archetype"] == "late_night_focus"
  34. def test_generic_food_and_small_spending_never_claims_coffee_persona() -> None:
  35. agent = PersonaAgent(FakeRuntime())
  36. persona, _ = agent.run(
  37. {"expense": 1000.0, "savings_rate": 5.0},
  38. {"餐饮": 400.0, "娱乐": 0.0, "购物": 0.0},
  39. _patterns(frequent_small={"count": 9, "amount": 280.0, "average": 31.0}),
  40. [],
  41. )
  42. assert persona["archetype"] == "frequent_small_spend"
  43. assert persona["primary"] == "高频小额行动派"
  44. assert all("咖啡" not in label for label in persona["labels"])
  45. def test_learning_persona_requires_history_not_a_single_large_month() -> None:
  46. agent = PersonaAgent(FakeRuntime())
  47. persona, trace = agent.run(
  48. {"expense": 1000.0, "savings_rate": 5.0},
  49. {"学习": 300.0},
  50. _patterns(),
  51. [],
  52. )
  53. learning = next(item for item in trace["candidates"] if item["archetype"] == "learning_investor")
  54. assert learning["evidence_valid"] is False
  55. assert persona["archetype"] != "learning_investor"
  56. def test_persona_config_rejects_unknown_feature_reference(tmp_path) -> None:
  57. config_path = tmp_path / "personas.json"
  58. config_path.write_text(
  59. json.dumps(
  60. {
  61. "archetypes": [
  62. {
  63. "id": "typo_guard",
  64. "name": "配置校验测试",
  65. "minimum_score": 50,
  66. "required_features": {"nightt": 40},
  67. "weights": {"nightt": 1.0},
  68. "evidence_metrics": ["late_night_share"],
  69. }
  70. ],
  71. "fallback": {"id": "balanced", "name": "均衡", "evidence_metrics": []},
  72. },
  73. ensure_ascii=False,
  74. ),
  75. encoding="utf-8",
  76. )
  77. with pytest.raises(ValueError, match="未知特征: nightt"):
  78. PersonaAgent(FakeRuntime(), config_path=config_path)
  79. def test_richer_persona_catalog_exposes_distinct_data_driven_archetypes() -> None:
  80. agent = PersonaAgent(FakeRuntime())
  81. configured_ids = {item.archetype_id for item in agent.archetypes}
  82. assert len(configured_ids) >= 12
  83. assert {
  84. "payday_rhythm",
  85. "weekend_social",
  86. "food_routine",
  87. "savings_sprinter",
  88. "digital_lifestyle",
  89. "learning_consistent",
  90. "flexible_adventurer",
  91. "mindful_minimalist",
  92. } <= configured_ids
  93. def test_payday_persona_is_selected_from_payday_evidence() -> None:
  94. agent = PersonaAgent(FakeRuntime())
  95. persona, trace = agent.run(
  96. {"expense": 1000.0, "savings_rate": 18.0},
  97. {"餐饮": 200.0, "娱乐": 80.0},
  98. _patterns(
  99. payday_window={"share": 68.0, "count": 6, "amount": 680.0},
  100. frequent_small={"count": 7, "amount": 210.0, "average": 30.0},
  101. ),
  102. [],
  103. )
  104. assert persona["archetype"] == "payday_rhythm"
  105. assert any(item["archetype"] == "payday_rhythm" and item["evidence_valid"] for item in trace["candidates"])
  106. assert "工资到账后消费占比" in ";".join(persona["evidence"])
  107. def test_food_routine_persona_requires_repeated_small_food_behavior() -> None:
  108. agent = PersonaAgent(FakeRuntime())
  109. persona, _ = agent.run(
  110. {"expense": 1000.0, "savings_rate": 5.0},
  111. {"餐饮": 700.0, "娱乐": 0.0, "购物": 0.0},
  112. _patterns(frequent_small={"count": 6, "amount": 50.0, "average": 8.33}),
  113. [],
  114. )
  115. assert persona["archetype"] == "food_routine"
  116. assert persona["primary"] == "日常餐饮探索家"
  117. def test_savings_feature_keeps_the_verified_savings_rate_shape() -> None:
  118. agent = PersonaAgent(FakeRuntime())
  119. persona, _ = agent.run(
  120. {"expense": 1000.0, "savings_rate": 47.5},
  121. {"餐饮": 300.0},
  122. _patterns(),
  123. [],
  124. )
  125. assert persona["feature_vector"]["savings"] == 47.5