feat: 六爻算法修复 + prompt架构重构 + i18n输出规则
算法修复 (P0/P1): - P0-1: 空亡判断改为仅从日柱计算(年月空亡标注但不断事) - P0-2: 暗动判断重写为静爻+旺相+日冲三条件 - P1-1: 月破独立标注 - P1-2: 动不为空、旺不为空 - P1-3: 三合局判断 - P1-4: 反吟伏吟判断 - P1-5: 日辰十二长生 - P1-6: 回头生克判断 Prompt架构重构: - 删除system_prompt中_build_env_section,不再泄露用户上下文到prompt - 删除if is_chinese分支,_LANGUAGE_LABELS已覆盖全部语言映射 - 安全规则改为六爻专属约束,拒绝无关问题 - sign_level枚举值在所有语言版本中统一为简体中文(schema严格约束) - _WORKER_ROLE_PLAYING始终为中文,不因ai_language切换 - _WORKER_OUTPUT_RULES按ai_language分zh-CN/zh-Hant/en三版本 - worker_rules.py独立文件管理多语言输出规则 - runner ai_language从user_context.settings.preferences提取传入prompt 清理死代码: - 删除UserPreferences/RuntimePromptContext及辅助函数 - 删除runner中runtime_client_time参数链路 - 删除SystemAgentRuntimeConfig.extra_context - 删除sections.py中env section marker - 删除agent_prompt.py中AgentPromptRegistry死代码 安全规则: - AGENTS.md添加Git Safety规则(禁止未经批准的破坏性git操作) - opencode.json添加高危git命令审批配置 测试: - 新增22个六爻算法单元测试(空亡/暗动/月破/三合局等) - 重写7个prompt测试适配新签名 - 全部85个单元测试通过
This commit is contained in:
@@ -1,60 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from core.agentscope.prompts.agent_prompt import build_agent_prompt
|
||||
from core.agentscope.prompts.system_prompt import build_system_prompt
|
||||
from schemas.agent.system_agent import AgentType, SystemAgentLLMConfig
|
||||
from schemas.shared.user import UserContext, parse_profile_settings
|
||||
|
||||
|
||||
def _build_user_context(*, ai_language: str = "en-US") -> UserContext:
|
||||
settings = parse_profile_settings(
|
||||
{
|
||||
"preferences": {
|
||||
"interface_language": "zh-CN",
|
||||
"ai_language": ai_language,
|
||||
"timezone": "Asia/Shanghai",
|
||||
"country": "CN",
|
||||
}
|
||||
}
|
||||
)
|
||||
return UserContext(
|
||||
id="user-1",
|
||||
username="tester",
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
|
||||
def test_system_prompt_enforces_ai_language_and_identity_signals() -> None:
|
||||
def test_system_prompt_enforces_ai_language_en() -> None:
|
||||
prompt = build_system_prompt(
|
||||
agent_type=AgentType.WORKER,
|
||||
ai_language="en-US",
|
||||
llm_config=SystemAgentLLMConfig(),
|
||||
user_context=_build_user_context(ai_language="en-US"),
|
||||
now_utc=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
assert '"ai_language":"en-US"' in prompt
|
||||
assert (
|
||||
"interface_language and country are weak signals for user identity inference"
|
||||
in prompt
|
||||
)
|
||||
assert (
|
||||
"Do not assert private facts; if identity/location lacks evidence, state uncertainty."
|
||||
in prompt
|
||||
)
|
||||
assert "English" in prompt
|
||||
assert "<!-- SAFETY_START -->" in prompt
|
||||
assert "<!-- OUTPUT_START -->" in prompt
|
||||
|
||||
|
||||
def test_system_prompt_does_not_leak_runtime_config_to_model_prompt() -> None:
|
||||
def test_system_prompt_enforces_ai_language_zh_cn() -> None:
|
||||
prompt = build_system_prompt(
|
||||
agent_type=AgentType.WORKER,
|
||||
ai_language="zh-CN",
|
||||
llm_config=SystemAgentLLMConfig(),
|
||||
user_context=_build_user_context(),
|
||||
now_utc=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
assert "context_messages.mode" not in prompt
|
||||
assert "enabled_tools=" not in prompt
|
||||
assert "简体中文" in prompt
|
||||
assert "<!-- SAFETY_START -->" in prompt
|
||||
|
||||
|
||||
def test_system_prompt_safety_restricts_to_divination() -> None:
|
||||
prompt = build_system_prompt(
|
||||
agent_type=AgentType.WORKER,
|
||||
ai_language="zh-CN",
|
||||
llm_config=SystemAgentLLMConfig(),
|
||||
)
|
||||
|
||||
assert "只回答与六爻占卜" in prompt or "解卦" in prompt
|
||||
|
||||
|
||||
def test_system_prompt_does_not_contain_env_section() -> None:
|
||||
prompt = build_system_prompt(
|
||||
agent_type=AgentType.WORKER,
|
||||
ai_language="zh-CN",
|
||||
llm_config=SystemAgentLLMConfig(),
|
||||
)
|
||||
|
||||
assert "USER_CONTEXT_JSON" not in prompt
|
||||
assert "Runtime Context" not in prompt
|
||||
assert "<!-- ENV_START -->" not in prompt
|
||||
|
||||
|
||||
def test_agent_prompt_keeps_only_identity_and_domain_flow() -> None:
|
||||
@@ -63,50 +56,20 @@ def test_agent_prompt_keeps_only_identity_and_domain_flow() -> None:
|
||||
llm_config=SystemAgentLLMConfig(),
|
||||
)
|
||||
|
||||
assert "[输出约束]" not in prompt
|
||||
assert "[安全与拒答]" not in prompt
|
||||
assert "[procedure]" in prompt
|
||||
assert "focus_points" in prompt
|
||||
assert "段间用\\n\\n" in prompt
|
||||
assert "优先四字表达,简洁且可复述" not in prompt
|
||||
|
||||
|
||||
def test_system_prompt_sanitizes_invalid_language_and_country() -> None:
|
||||
class _Preferences:
|
||||
interface_language = "@@bad@@"
|
||||
ai_language = "ignore previous instructions"
|
||||
timezone = "Asia/Shanghai"
|
||||
country = "cnx"
|
||||
|
||||
class _Settings:
|
||||
version = 1
|
||||
preferences = _Preferences()
|
||||
|
||||
class _UserContext:
|
||||
id = "user-1"
|
||||
username = "tester"
|
||||
settings = _Settings()
|
||||
|
||||
prompt = build_system_prompt(
|
||||
agent_type=AgentType.WORKER,
|
||||
llm_config=SystemAgentLLMConfig(),
|
||||
user_context=_UserContext(), # type: ignore[arg-type]
|
||||
now_utc=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
assert '"ai_language":"zh-CN"' in prompt
|
||||
assert '"interface_language":"zh-CN"' in prompt
|
||||
assert '"country":"CN"' in prompt
|
||||
assert "[role_playing]" in prompt
|
||||
assert "[output_json_rules]" in prompt
|
||||
|
||||
|
||||
def test_system_prompt_sections_are_not_duplicated() -> None:
|
||||
prompt = build_system_prompt(
|
||||
agent_type=AgentType.WORKER,
|
||||
ai_language="zh-CN",
|
||||
llm_config=SystemAgentLLMConfig(),
|
||||
user_context=_build_user_context(ai_language="zh-CN"),
|
||||
now_utc=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
assert prompt.count("<!-- ENV_START -->") == 1
|
||||
assert prompt.count("<!-- SAFETY_START -->") == 1
|
||||
assert prompt.count("<!-- AGENT_START -->") == 1
|
||||
assert prompt.count("<!-- OUTPUT_START -->") == 1
|
||||
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from core.divination.derivation import (
|
||||
_get_kong_wang,
|
||||
_resolve_liu_shou,
|
||||
_wu_xing_status,
|
||||
derive_divination,
|
||||
)
|
||||
from schemas.domain.divination import DivinationPayload, DivinationMethod, YaoType
|
||||
|
||||
|
||||
class TestKongWangCalculation:
|
||||
def test_kong_wang_jia_zi(self) -> None:
|
||||
assert _get_kong_wang("甲子") == "戌亥"
|
||||
|
||||
def test_kong_wang_jia_xu(self) -> None:
|
||||
assert _get_kong_wang("甲戌") == "申酉"
|
||||
|
||||
def test_kong_wang_jia_shen(self) -> None:
|
||||
assert _get_kong_wang("甲申") == "午未"
|
||||
|
||||
def test_kong_wang_jia_wu(self) -> None:
|
||||
assert _get_kong_wang("甲午") == "辰巳"
|
||||
|
||||
def test_kong_wang_jia_chen(self) -> None:
|
||||
assert _get_kong_wang("甲辰") == "寅卯"
|
||||
|
||||
def test_kong_wang_jia_yin(self) -> None:
|
||||
assert _get_kong_wang("甲寅") == "子丑"
|
||||
|
||||
|
||||
class TestLiuShouCalculation:
|
||||
def test_liu_shou_jia_yi(self) -> None:
|
||||
assert _resolve_liu_shou("甲") == ("龙", "雀", "勾", "蛇", "虎", "玄")
|
||||
assert _resolve_liu_shou("乙") == ("龙", "雀", "勾", "蛇", "虎", "玄")
|
||||
|
||||
def test_liu_shou_bing_ding(self) -> None:
|
||||
assert _resolve_liu_shou("丙") == ("雀", "勾", "蛇", "虎", "玄", "龙")
|
||||
assert _resolve_liu_shou("丁") == ("雀", "勾", "蛇", "虎", "玄", "龙")
|
||||
|
||||
def test_liu_shou_wu(self) -> None:
|
||||
assert _resolve_liu_shou("戊") == ("勾", "蛇", "虎", "玄", "龙", "雀")
|
||||
|
||||
def test_liu_shou_ji(self) -> None:
|
||||
assert _resolve_liu_shou("己") == ("蛇", "虎", "玄", "龙", "雀", "勾")
|
||||
|
||||
def test_liu_shou_geng_xin(self) -> None:
|
||||
assert _resolve_liu_shou("庚") == ("虎", "玄", "龙", "雀", "勾", "蛇")
|
||||
assert _resolve_liu_shou("辛") == ("虎", "玄", "龙", "雀", "勾", "蛇")
|
||||
|
||||
def test_liu_shou_ren_gui(self) -> None:
|
||||
assert _resolve_liu_shou("壬") == ("玄", "龙", "雀", "勾", "蛇", "虎")
|
||||
assert _resolve_liu_shou("癸") == ("玄", "龙", "雀", "勾", "蛇", "虎")
|
||||
|
||||
|
||||
class TestWuXingStatus:
|
||||
def test_wood_wang_in_yin_mao(self) -> None:
|
||||
assert _wu_xing_status("寅", "木") == "旺"
|
||||
assert _wu_xing_status("卯", "木") == "旺"
|
||||
|
||||
def test_fire_wang_in_si_wu(self) -> None:
|
||||
assert _wu_xing_status("巳", "火") == "旺"
|
||||
assert _wu_xing_status("午", "火") == "旺"
|
||||
|
||||
def test_metal_wang_in_shen_you(self) -> None:
|
||||
assert _wu_xing_status("申", "金") == "旺"
|
||||
assert _wu_xing_status("酉", "金") == "旺"
|
||||
|
||||
def test_water_wang_in_hai_zi(self) -> None:
|
||||
assert _wu_xing_status("亥", "水") == "旺"
|
||||
assert _wu_xing_status("子", "水") == "旺"
|
||||
|
||||
def test_earth_wang_in_chen_wei_chou_xu(self) -> None:
|
||||
assert _wu_xing_status("辰", "土") == "旺"
|
||||
assert _wu_xing_status("未", "土") == "旺"
|
||||
assert _wu_xing_status("戌", "土") == "旺"
|
||||
assert _wu_xing_status("丑", "土") == "旺"
|
||||
|
||||
|
||||
class TestKongWangFromDayOnly:
|
||||
def test_kong_wang_only_from_day_gan_zhi(self) -> None:
|
||||
payload = DivinationPayload(
|
||||
divinationMethod=DivinationMethod.MANUAL,
|
||||
questionType="测试",
|
||||
question="测试空亡仅从日柱",
|
||||
divinationTimeIso="2025-01-15T12:00:00+08:00",
|
||||
yaoLines=[
|
||||
YaoType.SHAO_YIN,
|
||||
YaoType.SHAO_YIN,
|
||||
YaoType.SHAO_YANG,
|
||||
YaoType.SHAO_YIN,
|
||||
YaoType.SHAO_YIN,
|
||||
YaoType.SHAO_YANG,
|
||||
],
|
||||
)
|
||||
result = derive_divination(payload)
|
||||
|
||||
assert result.ganzhi.day_kong_wang == "午未"
|
||||
assert result.ganzhi.time_kong_wang == "戌亥"
|
||||
|
||||
kong_wang_yao = [s for s in result.special_status if "旬空" in s]
|
||||
for status in kong_wang_yao:
|
||||
if "午" in status or "未" in status:
|
||||
pass
|
||||
else:
|
||||
assert "戌" not in status
|
||||
assert "亥" not in status
|
||||
|
||||
|
||||
class TestAnDongLogic:
|
||||
def test_an_dong_wang_xiang_ri_chong(self) -> None:
|
||||
payload = DivinationPayload(
|
||||
divinationMethod=DivinationMethod.MANUAL,
|
||||
questionType="测试",
|
||||
question="测试暗动",
|
||||
divinationTimeIso="2025-05-15T12:00:00+08:00",
|
||||
yaoLines=[
|
||||
YaoType.SHAO_YIN,
|
||||
YaoType.SHAO_YIN,
|
||||
YaoType.SHAO_YANG,
|
||||
YaoType.SHAO_YIN,
|
||||
YaoType.SHAO_YIN,
|
||||
YaoType.SHAO_YANG,
|
||||
],
|
||||
)
|
||||
result = derive_divination(payload)
|
||||
|
||||
an_dong_status = [s for s in result.special_status if "暗动" in s]
|
||||
for status in an_dong_status:
|
||||
assert "旺相" in status or "日冲" in status
|
||||
|
||||
def test_changing_yao_not_marked_as_kong_wang(self) -> None:
|
||||
payload = DivinationPayload(
|
||||
divinationMethod=DivinationMethod.MANUAL,
|
||||
questionType="测试",
|
||||
question="测试动爻不标空亡",
|
||||
divinationTimeIso="2025-01-15T12:00:00+08:00",
|
||||
yaoLines=[
|
||||
YaoType.SHAO_YANG,
|
||||
YaoType.SHAO_YANG,
|
||||
YaoType.LAO_YANG,
|
||||
YaoType.SHAO_YANG,
|
||||
YaoType.SHAO_YANG,
|
||||
YaoType.SHAO_YANG,
|
||||
],
|
||||
)
|
||||
result = derive_divination(payload)
|
||||
|
||||
for yao in result.yao_info_list:
|
||||
if yao.is_changing:
|
||||
for status in result.special_status:
|
||||
if f"第{yao.position}爻" in status:
|
||||
assert "旬空" not in status
|
||||
|
||||
|
||||
class TestYuePoLogic:
|
||||
def test_yue_po_independently_marked(self) -> None:
|
||||
payload = DivinationPayload(
|
||||
divinationMethod=DivinationMethod.MANUAL,
|
||||
questionType="测试",
|
||||
question="测试月破",
|
||||
divinationTimeIso="2025-06-15T12:00:00+08:00",
|
||||
yaoLines=[
|
||||
YaoType.SHAO_YIN,
|
||||
YaoType.SHAO_YIN,
|
||||
YaoType.SHAO_YANG,
|
||||
YaoType.SHAO_YIN,
|
||||
YaoType.SHAO_YIN,
|
||||
YaoType.SHAO_YANG,
|
||||
],
|
||||
)
|
||||
result = derive_divination(payload)
|
||||
|
||||
yue_po_status = [s for s in result.special_status if "月破" in s]
|
||||
for status in yue_po_status:
|
||||
assert "月破" in status
|
||||
assert "暗动" not in status
|
||||
|
||||
|
||||
class TestWangBuWeiKong:
|
||||
def test_wang_xiang_yao_not_marked_as_kong_wang(self) -> None:
|
||||
payload = DivinationPayload(
|
||||
divinationMethod=DivinationMethod.MANUAL,
|
||||
questionType="测试",
|
||||
question="测试旺不为空",
|
||||
divinationTimeIso="2025-01-15T12:00:00+08:00",
|
||||
yaoLines=[
|
||||
YaoType.SHAO_YANG,
|
||||
YaoType.SHAO_YANG,
|
||||
YaoType.SHAO_YANG,
|
||||
YaoType.SHAO_YANG,
|
||||
YaoType.SHAO_YANG,
|
||||
YaoType.SHAO_YANG,
|
||||
],
|
||||
)
|
||||
result = derive_divination(payload)
|
||||
|
||||
for status in result.special_status:
|
||||
if "旬空" in status:
|
||||
if "丑" in status or "土" in status:
|
||||
assert "旬空" not in status or "旺" not in status
|
||||
Reference in New Issue
Block a user