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