Files
xiaobaifupan/app/backend/features/heaven/readings.py
T

245 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import json
import secrets
from datetime import date
from typing import Any
from backend.bootstrap.config import normalize_date
from backend.features.heaven.agent import (
HEAVEN_PROMPT_VERSIONS,
HeavenAgentError,
interpret_heaven,
)
from backend.features.heaven.engine import (
build_five_phase_field,
hexagram_from_lines,
)
from backend.features.heaven.knowledge import prepare_heaven_context
from backend.features.heaven.six_yao import build_six_yao_chart
from backend.features.market import MarketServiceMixin
class HeavenReadingMixin:
def heaven_personal(self, payload: dict[str, Any]) -> dict[str, Any]:
trade_date = normalize_date(str(payload.get("trade_date") or date.today().isoformat()))
field = build_five_phase_field(
trade_date,
self.database.list_sector_phase_overrides(),
)
personal = self.account_personal_field(trade_date, field, public=True)
if not personal:
raise ValueError("请先在账号设置中保存个人命理资料。")
return personal
def heaven_hexagram(self, raw_lines: Any) -> dict[str, Any]:
if not isinstance(raw_lines, list):
raise ValueError("六爻起卦结果格式不正确。")
try:
lines = [int(value) for value in raw_lines]
except (TypeError, ValueError) as exc:
raise ValueError("六爻必须由六、七、八、九组成。") from exc
return hexagram_from_lines(lines)
def heaven_readings(
self, mode: str, context_date: str = "", limit: int = 100
) -> dict[str, Any]:
mode = str(mode or "").strip()
if mode not in {"trend", "fortune", "heart"}:
raise ValueError("解读记录类型不正确。")
normalized_date = normalize_date(context_date) if context_date else ""
return {
"mode": mode,
"items": self.database.list_heaven_readings(
self.current_user_id, mode, normalized_date, limit
),
}
@staticmethod
def _heaven_reading_identity(
mode: str, context_date: str, context: dict[str, Any]
) -> tuple[str, str]:
display_date = MarketServiceMixin._display_compact_date(context_date)
if mode == "trend":
stock = (context.get("selected_focus") or {}).get("stock") or {}
code = str(stock.get("code") or "").strip()
name = str(stock.get("name") or "").strip()
hexagram = context.get("hexagram") or {}
transformed = hexagram.get("transformed") or {}
subject = " ".join(item for item in (code, name) if item) or "观势"
detail = f"{display_date} · {hexagram.get('name') or '--'}{transformed.get('name') or '--'}"
return subject, detail
if mode == "fortune":
field = context.get("five_phase_field") or {}
pillars = field.get("pillars") or {}
dominant = (field.get("balance") or [{}])[0]
subject = f"{display_date} 观气"
detail = (
f"{pillars.get('year') or '--'}年 · {pillars.get('month') or '--'}月 · "
f"{pillars.get('day') or '--'}日 · {dominant.get('element') or '--'}气偏显"
)
return subject, detail
hexagram = context.get("hexagram") or {}
transformed = hexagram.get("transformed") or {}
question = str(context.get("question") or "").strip()
question_detail = f" · {question[:48]}" if question else ""
return (
f"{display_date} 观心",
f"{hexagram.get('name') or '--'}{transformed.get('name') or '--'}{question_detail}",
)
def heaven_interpret(self, payload: dict[str, Any]) -> dict[str, Any]:
mode = str(payload.get("mode") or "").strip()
if mode not in {"trend", "fortune", "heart"}:
raise ValueError("问天解读模式不正确。")
trade_date = normalize_date(str(payload.get("trade_date") or date.today().isoformat()))
prompt_version = HEAVEN_PROMPT_VERSIONS[mode]
stale_fortune: dict[str, Any] | None = None
if mode == "fortune":
existing = self.database.latest_heaven_reading(
self.current_user_id, "fortune", trade_date
)
if self._legacy_truncated_heaven_reading(existing):
self.database.delete_heaven_reading(
self.current_user_id, int(existing["id"])
)
existing = None
if existing and self.database.heaven_reading_interpretation_version(
self.current_user_id, int(existing["id"])
) == prompt_version:
return {
"answer": existing["answer"],
"mode": mode,
"compiler": "stored",
"notice": "",
"reading": existing,
"reused": True,
}
stale_fortune = existing
if mode in {"trend", "fortune"}:
setup = self.heaven_setup(
trade_date,
str(payload.get("sector") or ""),
str(payload.get("stock_code") or ""),
payload.get("manual_data"),
)
if mode == "trend":
chart = setup["chart"]
if not chart.get("available"):
issues = "".join((chart.get("quality") or {}).get("issues") or [])
raise ValueError(f"观势数据未通过六爻校验,暂不解势:{issues}")
hexagram_context = json.loads(json.dumps(chart["hexagram"], ensure_ascii=False))
for line in hexagram_context.get("lines", []):
line.pop("evidence", None)
line.pop("score", None)
line.pop("talent", None)
line.pop("layer", None)
line.pop("role", None)
if not line.get("moving"):
line.pop("text", None)
line.pop("image", None)
line.pop("line_name", None)
context = {
"data_trade_date": setup["trade_date"],
"selected_focus": {
"sector": chart.get("sector") or "",
"stock": chart.get("stock") or {},
},
"hexagram": hexagram_context,
"movement": chart.get("movement") or {},
}
else:
personal_profile = self.account_personal_field(
setup["calendar_date"],
setup["field"],
public=False,
)
context = {
"calendar_date": setup["calendar_date"],
"five_phase_field": setup["field"],
"personal_profile": personal_profile,
}
context_date = setup["calendar_date"]
if mode == "trend":
context_date = setup["trade_date"]
else:
question = str(payload.get("question") or "").strip()
if len(question) > 300:
raise ValueError("观心问题不能超过300个字符。")
question_preset = str(payload.get("question_preset") or "unthemed").strip()
if question_preset not in {"trade", "mind", "unthemed", "custom"}:
question_preset = "custom"
if not question:
question = "不设具体问题,只观此刻一念。"
question_preset = "unthemed"
raw_lines = payload.get("lines")
hexagram = self.heaven_hexagram(raw_lines)
context = {
"question": question,
"question_preset": question_preset,
"hexagram": hexagram,
"six_yao": build_six_yao_chart(
[int(value) for value in raw_lines],
str(payload.get("cast_at") or ""),
),
"ritual": {
"breathing": "用户已完成1秒准备与五轮吸3秒、顿2秒、呼4秒的静心呼吸。",
"casting": "用户以三枚铜钱自初爻至上爻投掷六次。",
"reflection": "用户已在看见卦象后察看第一念。",
},
}
context_date = trade_date
agent_context = prepare_heaven_context(mode, context)
agent_context["interpretation_version"] = prompt_version
result, compiler = self._call_heaven_agent(mode, agent_context)
subject, subject_detail = self._heaven_reading_identity(
mode, context_date, context
)
dedupe_key = (
f"fortune:{context_date}"
if mode == "fortune"
else f"{mode}:{context_date}:{secrets.token_urlsafe(12)}"
)
if stale_fortune:
self.database.delete_heaven_reading(
self.current_user_id, int(stale_fortune["id"])
)
reading = self.database.save_heaven_reading(
self.current_user_id,
mode,
context_date,
subject,
subject_detail,
str(result.get("answer") or ""),
agent_context,
dedupe_key,
)
return {
**result,
"mode": mode,
"compiler": compiler,
"notice": "当前智能服务繁忙,已自动切换备用服务。" if compiler == "fallback" else "",
"reading": reading,
"reused": False,
}
@staticmethod
def _legacy_truncated_heaven_reading(reading: dict[str, Any] | None) -> bool:
return bool(reading and str(reading.get("answer") or "").rstrip().endswith("……"))
def _call_heaven_agent(self, mode: str, context: dict[str, Any]) -> tuple[dict[str, Any], str]:
prompt_version = HEAVEN_PROMPT_VERSIONS[mode]
result = self.llm_gateway.call(
f"heaven_{mode}",
prompt_version,
lambda profile: interpret_heaven(
mode,
context,
profile.api_key,
profile.base_url,
profile.model,
),
(HeavenAgentError,),
)
return result.value, result.role