feat: complete heaven readings and screener publication

This commit is contained in:
leefer
2026-08-05 23:44:17 +08:00
parent 6a058c2929
commit b3a21d05b7
34 changed files with 3538 additions and 225 deletions
+387
View File
@@ -0,0 +1,387 @@
from __future__ import annotations
import json
from functools import lru_cache
from typing import Any
from backend.bootstrap.config import APP_DIR
KNOWLEDGE_FILE = APP_DIR / "data" / "heaven_knowledge.json"
def prepare_heaven_context(mode: str, calculation: dict[str, Any]) -> dict[str, Any]:
"""Build the only context shape that may cross the LLM boundary."""
if mode == "trend":
prepared = _prepare_trend(calculation)
elif mode == "fortune":
prepared = _prepare_fortune(calculation)
elif mode == "heart":
prepared = _prepare_heart(calculation)
else:
raise ValueError("不支持的问天知识模式。")
prepared["knowledge"] = retrieve_heaven_knowledge(mode, prepared)
return prepared
def retrieve_heaven_knowledge(mode: str, context: dict[str, Any]) -> dict[str, Any]:
catalog = _knowledge_catalog()
source_ids: list[str]
records: list[dict[str, Any]]
if mode == "trend":
source_ids = ["zhouyi"]
records = _trend_records(catalog, context)
elif mode == "fortune":
source_ids = ["neijing"]
records = _fortune_records(catalog, context)
elif mode == "heart":
source_ids = ["zhouyi", "jingfang", "huozhulin", "zengshan"]
records = _heart_records(catalog, context)
else:
raise ValueError("不支持的问天知识模式。")
return {
"version": str(catalog.get("version") or ""),
"retrieval": "deterministic-keyed",
"sources": [
{"id": source_id, **dict(catalog["sources"][source_id])}
for source_id in source_ids
],
"records": records,
}
def _prepare_trend(context: dict[str, Any]) -> dict[str, Any]:
return {
"mode": "trend",
"calculation": {
"data_trade_date": context.get("data_trade_date") or "",
"hexagram": context.get("hexagram") or {},
"movement": context.get("movement") or {},
},
"interpretation_contract": {
"required": ["明确卦势倾向", "主要矛盾", "实际动爻转折", "本卦到之卦的变化关系"],
"allowed": ["偏进或偏守", "先难后易或由盛转收", "结论有条件或存在分歧"],
"forbidden": ["原始行情旁证", "具体涨跌预测", "时间点预测", "无条件买卖指令", "泛化劝诫代替解卦"],
},
}
def _prepare_fortune(context: dict[str, Any]) -> dict[str, Any]:
field = context.get("five_phase_field") or {}
framework = field.get("framework") or {}
relations = framework.get("relations") or {}
layers = {
str(item.get("id") or ""): item
for item in framework.get("layers") or []
if isinstance(item, dict)
}
six_qi = field.get("six_qi") or {}
movement = field.get("movement") or {}
pillars = field.get("pillars") or {}
personal = context.get("personal_profile") or {}
sector_catalog = {
str(group.get("element") or ""): [
str(item.get("name") or "").strip()
for item in group.get("industries") or []
if str(item.get("name") or "").strip()
]
for group in field.get("sector_catalog") or []
if isinstance(group, dict)
}
industry_bases: dict[str, list[str]] = {}
def add_industry_basis(element: str, basis: str) -> None:
if element not in sector_catalog or not sector_catalog[element]:
return
industry_bases.setdefault(element, [])
if basis not in industry_bases[element]:
industry_bases[element].append(basis)
add_industry_basis(str(movement.get("phase") or ""), "中运")
for label, qi in (
("司天", six_qi.get("sitian")),
("在泉", six_qi.get("zaiquan")),
("主气", six_qi.get("host_qi")),
("客气", six_qi.get("guest_qi")),
):
qi_text = str(qi or "")
add_industry_basis(qi_text[-1:] if qi_text else "", label)
day_master = personal.get("day_master") or {}
current = personal.get("current") or {}
personal_context = {}
if day_master:
current_ten_gods = current.get("ten_gods") or {}
personal_context = {
"natal_day_master": {
"stem": day_master.get("stem") or "",
"element": day_master.get("element") or "",
},
"today_relative_to_natal_day_master": {
"pillars": current.get("pillars") or {},
"stem_relations": {
key: str((current_ten_gods.get(key) or {}).get("stem") or "")
for key in ("year", "month", "day")
},
},
}
return {
"mode": "fortune",
"calculation": {
"calendar_date": context.get("calendar_date") or field.get("date") or "",
"lunar_date": field.get("lunar_date") or "",
"pillars": {
"year": pillars.get("year") or "",
"month": pillars.get("month") or "",
"day": pillars.get("day") or "",
},
"solar_terms": field.get("solar_terms") or {},
"year_movement": {
"phase": movement.get("phase") or "",
"tendency": movement.get("tendency") or "",
"label": movement.get("label") or "",
},
"annual_qi": {
"sitian": six_qi.get("sitian") or "",
"zaiquan": six_qi.get("zaiquan") or "",
"ruling": six_qi.get("ruling") or "",
"ruling_qi": six_qi.get("ruling_qi") or "",
"annual_pattern": relations.get("annual_pattern") or {},
},
"current_qi": {
"step": six_qi.get("step"),
"step_name": six_qi.get("step_name") or "",
"host_qi": six_qi.get("host_qi") or "",
"guest_qi": six_qi.get("guest_qi") or "",
"guest_host_relation": relations.get("guest_host") or {},
"alignment": relations.get("alignment") or six_qi.get("alignment") or "",
},
"day_trigger": {
"day_pillar": pillars.get("day") or "",
"summary": (layers.get("day") or {}).get("summary") or "",
},
"industry_symbols": [
{
"element": element,
"basis": bases,
"industries": sector_catalog[element],
}
for element, bases in industry_bases.items()
],
"personal": personal_context,
},
"excluded_from_interpretation": [
"五行权重与百分比",
"主导元素排序",
"权重生成的复合断语",
"预制情绪与交易行为结论",
"行业实时行情旁证",
"简化喜用神与强弱结论",
],
"interpretation_contract": {
"required": ["年纲", "当前客主加临", "日辰触发", "行业影响", "个人合参(如有)", "制衡动作"],
"forbidden": [
"重新计算五行权重",
"把相生直接判吉",
"把相克直接判凶",
"用市场涨跌证明气场",
"把行业取象写成行业涨跌预测或投资推荐",
"把当日日柱误称为用户命局",
"推断未提供的命局强弱或喜用神",
],
},
}
def _prepare_heart(context: dict[str, Any]) -> dict[str, Any]:
preset = str(context.get("question_preset") or "custom")
if preset not in {"trade", "mind", "unthemed", "custom"}:
preset = "custom"
return {
"mode": "heart",
"calculation": {
"question": str(context.get("question") or "").strip(),
"question_preset": preset,
"question_scope": {
"trade": "股票交易中的参与条件、机会、阻碍与风险,不是融资或商业合作问题。",
"mind": "影响股票交易判断的情绪、执念或盲点。",
"unthemed": "不指定事项的一般观照。",
"custom": "只按用户实际写出的事项理解,不补写背景。",
}[preset],
"ritual": context.get("ritual") or {},
"hexagram": context.get("hexagram") or {},
"six_yao": context.get("six_yao") or {},
},
"interpretation_contract": {
"required": ["回应所问", "本卦处境", "世应与相关六亲", "关键动变", "之卦趋向", "可验证动作"],
"plain_language": "专业术语首次出现时立即用白话解释。",
"forbidden": [
"修改纳甲排盘",
"猜测未输入的问题",
"把股票交易改写成融资或合作问题",
"把六亲直接等同于现实人物或资金来源",
"单凭六神或空亡断吉凶",
"根据旬空填实或干支日期预测应期",
"具体股价和时间点预测",
"无条件买卖指令",
],
},
}
def _trend_records(catalog: dict[str, Any], context: dict[str, Any]) -> list[dict[str, Any]]:
hexagram = (context.get("calculation") or {}).get("hexagram") or {}
moving = [line for line in hexagram.get("lines") or [] if line.get("moving")]
method_key = "stable" if not moving else "single" if len(moving) == 1 else "multiple"
rules = catalog["trend"]["rules"]
records = [
{"id": "trend-method", "source": "product_method", "text": catalog["trend"]["method"]},
{"id": f"trend-moving-{method_key}", "source": "product_method", "text": rules[method_key]},
_hexagram_record("primary", hexagram),
]
records.extend(_line_record(line) for line in moving)
transformed = hexagram.get("transformed") or {}
if transformed:
records.append(_hexagram_record("transformed", transformed))
return records
def _fortune_records(catalog: dict[str, Any], context: dict[str, Any]) -> list[dict[str, Any]]:
calculation = context.get("calculation") or {}
movement = calculation.get("year_movement") or {}
annual_qi = calculation.get("annual_qi") or {}
current_qi = calculation.get("current_qi") or {}
knowledge = catalog["fortune"]
records = [
{"id": "fortune-principle", "source": "neijing", "text": knowledge["principle"]},
]
if calculation.get("industry_symbols"):
records.append(
{
"id": "fortune-industry-boundary",
"source": "product_method",
"text": knowledge["industry_boundary"],
}
)
personal = calculation.get("personal") or {}
if personal:
records.append(
{
"id": "fortune-personal-boundary",
"source": "product_method",
"text": knowledge["personal_boundary"],
}
)
today = personal.get("today_relative_to_natal_day_master") or {}
relation_semantics = knowledge.get("personal_relations") or {}
for relation in dict.fromkeys((today.get("stem_relations") or {}).values()):
if relation in relation_semantics:
records.append(
{
"id": f"fortune-personal-{relation}",
"source": "product_method",
"subject": relation,
"text": relation_semantics[relation],
}
)
tendency = str(movement.get("tendency") or "")
if tendency in knowledge["movement"]:
records.append({"id": f"movement-{tendency}", "source": "neijing", "text": knowledge["movement"][tendency]})
for key in ("sitian", "zaiquan"):
qi = str(annual_qi.get(key) or "")
if qi in knowledge["qi"]:
records.append({"id": f"annual-{key}", "source": "neijing", "subject": qi, "text": knowledge["qi"][qi]})
for key in ("host_qi", "guest_qi"):
qi = str(current_qi.get(key) or "")
if qi in knowledge["qi"]:
records.append({"id": f"current-{key}", "source": "neijing", "subject": qi, "text": knowledge["qi"][qi]})
relation = current_qi.get("guest_host_relation") or {}
relation_type = str(relation.get("type") or "")
if relation_type in knowledge["relations"]:
records.append({"id": f"relation-{relation_type}", "source": "neijing", "subject": relation.get("label") or "", "text": knowledge["relations"][relation_type]})
records.append({"id": "day-trigger", "source": "neijing", "text": knowledge["day_trigger"]})
return records
def _heart_records(catalog: dict[str, Any], context: dict[str, Any]) -> list[dict[str, Any]]:
calculation = context.get("calculation") or {}
hexagram = calculation.get("hexagram") or {}
six_yao = calculation.get("six_yao") or {}
preset = str(calculation.get("question_preset") or "custom")
heart = catalog["heart"]
records = [
{"id": "heart-focus", "source": "product_method", "text": heart["focus"].get(preset, heart["focus"]["custom"])},
{"id": "heart-evidence-order", "source": "product_method", "items": heart["evidence_order"]},
{"id": "heart-limits", "source": "product_method", "text": heart["limits"]},
{"id": "heart-self-response", "source": "jingfang", "text": heart["semantics"]["self_response"]},
{"id": "heart-calendar", "source": "zengshan", "text": heart["semantics"]["calendar"]},
{"id": "heart-movement", "source": "huozhulin", "text": heart["semantics"]["movement"]},
{"id": "heart-six-spirits", "source": "zengshan", "text": heart["semantics"]["six_spirits"]},
{"id": "heart-timing-boundary", "source": "product_method", "text": heart["semantics"]["timing_boundary"]},
_hexagram_record("primary", hexagram),
]
relatives = {
str(line.get("relative") or "")
for line in six_yao.get("lines") or []
if line.get("relative")
}
for relative in sorted(relatives):
text = (heart["semantics"].get("relatives") or {}).get(relative)
if text:
records.append(
{
"id": f"heart-relative-{relative}",
"source": "huozhulin",
"subject": relative,
"text": text,
}
)
records.extend(_line_record(line) for line in hexagram.get("lines") or [] if line.get("moving"))
transformed = hexagram.get("transformed") or {}
if transformed:
records.append(_hexagram_record("transformed", transformed))
palace = six_yao.get("palace") or {}
records.append(
{
"id": "heart-palace",
"source": "jingfang",
"text": (
f"本卦归{palace.get('name') or '--'}{palace.get('stage') or '--'}"
f"世在{palace.get('self_position') or '--'}爻,应在{palace.get('response_position') or '--'}爻。"
),
}
)
return records
def _hexagram_record(kind: str, hexagram: dict[str, Any]) -> dict[str, Any]:
return {
"id": f"zhouyi-{kind}",
"source": "zhouyi",
"kind": kind,
"name": hexagram.get("name") or "",
"inner_trigram": hexagram.get("inner_trigram") or "",
"outer_trigram": hexagram.get("outer_trigram") or "",
"text": hexagram.get("text") or "",
"tuan": hexagram.get("tuan") or "",
"image": hexagram.get("image") or "",
}
def _line_record(line: dict[str, Any]) -> dict[str, Any]:
return {
"id": f"zhouyi-line-{line.get('position') or ''}",
"source": "zhouyi",
"position": line.get("position"),
"position_name": line.get("position_name") or "",
"line_name": line.get("line_name") or "",
"text": line.get("text") or "",
"image": line.get("image") or "",
}
@lru_cache(maxsize=1)
def _knowledge_catalog() -> dict[str, Any]:
payload = json.loads(KNOWLEDGE_FILE.read_text(encoding="utf-8"))
if not payload.get("version") or not isinstance(payload.get("sources"), dict):
raise ValueError("问天知识库格式不完整。")
return payload