Files
xiaobai-review/tests/test_heaven_prompts.py
T

198 lines
11 KiB
Python

from __future__ import annotations
import unittest
from types import SimpleNamespace
from unittest.mock import patch
from backend.features.heaven.agent import (
HeavenAgentError,
_system_prompt,
_validate_answer,
interpret_heaven,
)
class HeavenPromptTests(unittest.TestCase):
def test_trend_prompt_requires_direction_without_fixed_advice_vocabulary(self):
prompt = _system_prompt("trend")
self.assertIn("必须明确给出卦义上的当下倾向", prompt)
self.assertIn("不得用“谨慎、等待、守信、辨伪”一类泛化劝诫代替解势", prompt)
self.assertNotIn("卦理约占九成", prompt)
self.assertNotIn("全文控制在300至450", prompt)
self.assertIn("## 核心判断", prompt)
self.assertIn("## 决策映射", prompt)
def test_fortune_prompt_excludes_weighted_conclusion(self):
prompt = _system_prompt("fortune")
self.assertIn("五行权重、主导元素和预制复合断语已明确排除", prompt)
self.assertIn("相生不直接等于吉,相克不直接等于凶", prompt)
self.assertIn("不得使用藏干", prompt)
self.assertIn("只能使用knowledge中本次命中的关系释义", prompt)
self.assertNotIn("全文控制在420至600", prompt)
self.assertIn("calculation.industry_symbols", prompt)
self.assertIn("## 行业影响", prompt)
self.assertIn("不得预测行业涨跌", prompt)
def test_heart_prompt_uses_question_and_deterministic_chart(self):
prompt = _system_prompt("heart")
self.assertIn("用户在起卦前确定的问题位于calculation.question", prompt)
self.assertIn("纳甲、卦宫、世应、六亲、六神", prompt)
self.assertIn("不是商业合作、融资、借贷或寻找资金方", prompt)
self.assertIn("不得假定用户已经入场", prompt)
self.assertIn("不得制造外围资金、外围资源", prompt)
self.assertIn("不得输出使用竖线分栏的Markdown表格", prompt)
self.assertIn("不得用旬空、填实、出空或干支日推算", prompt)
self.assertNotIn("最后三句极短的问心句", prompt)
self.assertIn("## 所问之答", prompt)
self.assertIn("## 可验证之处", prompt)
def test_fortune_validator_requires_industry_section_when_symbols_are_present(self):
context = {
"calculation": {
"industry_symbols": [
{"element": "水", "basis": ["中运"], "industries": ["航运"]}
]
}
}
answer = "年纲与客主关系已经说明,日辰只作当日触发,最后给出个人观察和制衡动作。" * 2
with self.assertRaises(HeavenAgentError):
_validate_answer("fortune", answer, context)
_validate_answer("fortune", answer + "行业影响只作航运等水属性行业的传统取象,不预测涨跌。", context)
def test_trend_validator_rejects_market_story_added_after_casting(self):
answer = "本卦显示当下偏守,动爻揭示由增益转入受阻,之卦说明变化后应先稳住结构。" * 2
_validate_answer("trend", answer, {})
with self.assertRaises(HeavenAgentError):
_validate_answer("trend", answer + "白酒行业政策可能带来外部压力。", {})
def test_fortune_validator_rejects_invented_natal_strength(self):
context = {
"calculation": {
"pillars": {"year": "丙午", "month": "乙未", "day": "庚戌"},
"personal": {
"today_relative_to_natal_day_master": {
"pillars": {"year": "丙午", "month": "乙未", "day": "庚戌"}
}
},
}
}
answer = "丙午年纲、乙未客主和庚戌日辰形成相互制约,个人合参只说明当日关系,不据此判断命局强弱。" * 2
_validate_answer("fortune", answer, context)
with self.assertRaises(HeavenAgentError):
_validate_answer("fortune", answer + "个人本身偏财较重。", context)
with self.assertRaises(HeavenAgentError):
_validate_answer("fortune", answer + "戊辰年进一步增加土气。", context)
with self.assertRaises(HeavenAgentError):
_validate_answer("fortune", answer + "午中藏丁火,所以内在火气被引动。", context)
with self.assertRaises(HeavenAgentError):
_validate_answer("fortune", answer + "戌为火库,内藏火之余气。", context)
with self.assertRaises(HeavenAgentError):
_validate_answer("fortune", answer + "戌属土、含太阳寒水之气。", context)
with self.assertRaises(HeavenAgentError):
_validate_answer("fortune", answer + "木生火、火生土,因此本命日主受到泄耗。", context)
with self.assertRaises(HeavenAgentError):
_validate_answer("fortune", answer + "戌月进一步加重土气。", context)
with self.assertRaises(HeavenAgentError):
_validate_answer("fortune", answer + "日柱庚戌本身就是金。", context)
with self.assertRaises(HeavenAgentError):
_validate_answer("fortune", answer + "戌的寒水属性使气机进一步收敛。", context)
def test_invalid_first_draft_is_rewritten_before_returning(self):
invalid = "本卦解释完整,但白酒行业政策和基本面正在改变外部环境,因此当下应偏守。" * 2
valid = "本卦显示当下偏守,实际动爻揭示由增益转入受阻,之卦说明变化后要先确认结构能否重新承接。" * 2
with patch(
"backend.features.heaven.agent.llm_transport.chat_completion",
side_effect=[
SimpleNamespace(content=invalid, latency_ms=1),
SimpleNamespace(content=valid, latency_ms=1),
],
) as completion:
result = interpret_heaven(
"trend", {}, "key", "https://example.invalid", "test-model"
)
self.assertEqual(result["answer"], valid)
self.assertEqual(completion.call_count, 2)
repair_messages = completion.call_args_list[1].kwargs["messages"]
self.assertIn("未通过本地一致性校验", repair_messages[-1]["content"])
def test_heart_validator_rejects_invented_financing_and_timing(self):
context = {
"calculation": {
"question": "关于我心中的这笔交易,此刻最需要看清的机会、阻碍与风险是什么?"
}
}
answer = "本卦说明条件仍有矛盾,世应提示内外预期并不一致,动爻要求先核对退出条件,再看之卦所示的收束方向。" * 2
_validate_answer("heart", answer, context)
with self.assertRaises(HeavenAgentError):
_validate_answer("heart", answer + "对方资金提供方会转为合作方。", context)
with self.assertRaises(HeavenAgentError):
_validate_answer("heart", answer + "未来三日等到旬空填实后再行动。", context)
def test_heart_validator_rejects_unstated_position_assumptions(self):
context = {
"calculation": {
"question": "关于我心中的这笔交易,此刻最需要看清的机会、阻碍与风险是什么?"
}
}
answer = "本卦说明条件尚未完全衔接,世应提示内外预期有差异,动爻要求先核对参与条件,再看变化后的风险边界。" * 2
_validate_answer("heart", answer, context)
for term in ("持仓", "仓位", "建仓", "入场", "持有", "买入", "卖出", "止损", "止盈"):
with self.subTest(term=term), self.assertRaises(HeavenAgentError):
_validate_answer("heart", answer + f"用户当前的{term}安排已经明确。", context)
def test_heart_validator_allows_position_language_when_question_states_it(self):
context = {"calculation": {"question": "我已经持仓,这笔交易的止损条件是否清楚?"}}
answer = "本卦所示的内外条件仍有差异,世应关系提醒先核对持仓依据;动爻显示原判断正在变化,止损条件应以用户已经写下的客观边界为准。" * 2
_validate_answer("heart", answer, context)
def test_heart_validator_rejects_invented_resource_paths(self):
context = {"calculation": {"question": "这笔交易最需要防范什么?"}}
answer = "本卦说明条件尚未完全衔接,世应提示内外预期有差异,动爻要求先核对参与条件,再看变化后的风险边界。" * 2
for scenario in (
"外围资金可能进入。",
"外围资源进入路径尚不清楚。",
"资金来源会在之后出现。",
"资源提供的安排已经成形。",
):
with self.subTest(scenario=scenario), self.assertRaises(HeavenAgentError):
_validate_answer("heart", answer + scenario, context)
def test_heart_validator_rejects_markdown_tables(self):
context = {"calculation": {"question": "这笔交易最需要防范什么?"}}
answer = "本卦说明条件尚未完全衔接,世应提示内外预期有差异,动爻要求先核对参与条件,再看变化后的风险边界。" * 2
tables = (
answer + "\n| 项目 | 解释 |\n| --- | --- |\n| 世爻 | 当前条件 |",
answer + "\n项目 | 解释\n---|---\n世爻 | 当前条件",
)
for table in tables:
with self.subTest(table=table), self.assertRaises(HeavenAgentError):
_validate_answer("heart", table, context)
def test_heart_invalid_first_draft_is_rewritten_before_returning(self):
context = {"calculation": {"question": "这笔交易最需要防范什么?"}}
invalid = "本卦提示外围资源进入路径尚未落实,用户应先调整仓位,再等待资金进入。" * 3
valid = "本卦说明条件尚未完全衔接,世应提示内外预期有差异,动爻要求先核对参与条件,再看变化后的风险边界。" * 2
with patch(
"backend.features.heaven.agent.llm_transport.chat_completion",
side_effect=[
SimpleNamespace(content=invalid, latency_ms=1),
SimpleNamespace(content=valid, latency_ms=1),
],
) as completion:
result = interpret_heaven(
"heart", context, "key", "https://example.invalid", "test-model"
)
self.assertEqual(result["answer"], valid)
self.assertEqual(completion.call_count, 2)
if __name__ == "__main__":
unittest.main()