migration: preserve heaven trend fortune and heart slice
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
from .agent import HeavenAgentError, interpret_heaven
|
||||
from .engine import (
|
||||
build_five_phase_field,
|
||||
build_market_hexagram,
|
||||
build_manual_market_hexagram,
|
||||
build_personal_field,
|
||||
hexagram_from_lines,
|
||||
)
|
||||
from .http import HeavenHttpMixin
|
||||
from .repository import HeavenRepositoryMixin
|
||||
from .service import HeavenServiceMixin
|
||||
|
||||
__all__ = [
|
||||
"HeavenAgentError",
|
||||
"HeavenHttpMixin",
|
||||
"HeavenRepositoryMixin",
|
||||
"HeavenServiceMixin",
|
||||
"build_five_phase_field",
|
||||
"build_manual_market_hexagram",
|
||||
"build_market_hexagram",
|
||||
"build_personal_field",
|
||||
"hexagram_from_lines",
|
||||
"interpret_heaven",
|
||||
]
|
||||
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
|
||||
class HeavenAgentError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def interpret_heaven(
|
||||
mode: str,
|
||||
context: dict[str, Any],
|
||||
api_key: str,
|
||||
base_url: str,
|
||||
model: str,
|
||||
timeout: int = 90,
|
||||
) -> dict[str, Any]:
|
||||
if mode not in {"trend", "fortune", "heart"}:
|
||||
raise HeavenAgentError("不支持的问天解读模式。")
|
||||
if not api_key or not model:
|
||||
raise HeavenAgentError("LLM API Key 或模型尚未配置。")
|
||||
system_prompt = _system_prompt(mode)
|
||||
payload = json.dumps(
|
||||
{
|
||||
"model": model,
|
||||
"messages": [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{
|
||||
"role": "user",
|
||||
"content": json.dumps(context, ensure_ascii=False, separators=(",", ":")),
|
||||
},
|
||||
],
|
||||
"stream": False,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8")
|
||||
request = urllib.request.Request(
|
||||
f"{base_url.rstrip('/')}/chat/completions",
|
||||
data=payload,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"User-Agent": "XiaobaiReviewWeb/0.7",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||
result = json.loads(response.read().decode("utf-8"))
|
||||
answer = str(result["choices"][0]["message"]["content"]).strip()
|
||||
if not answer:
|
||||
raise KeyError("empty response")
|
||||
except urllib.error.HTTPError as exc:
|
||||
raise HeavenAgentError(_http_error_message(exc)) from exc
|
||||
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError, IndexError) as exc:
|
||||
raise HeavenAgentError(f"问天模型调用失败:{exc}") from exc
|
||||
return {
|
||||
"answer": answer,
|
||||
"model": model,
|
||||
"latency_ms": round((time.perf_counter() - started) * 1000),
|
||||
}
|
||||
|
||||
|
||||
def _system_prompt(mode: str) -> str:
|
||||
common = """
|
||||
你是“小白复盘”的问天解读器。所有历法、卦象、爻位和市场指标已经由确定性程序计算,你只能解释提供的数据,不得改卦、改爻、改干支或编造行情。
|
||||
问天属于传统文化与娱乐化观察,不是预测模型,不承诺应验,不输出无条件买卖指令,不用神秘话术制造确定性。
|
||||
使用中文,先给核心判断,再解释结构。引用市场数字时标明数据日期。输出纯文本,可使用简短标题。
|
||||
""".strip()
|
||||
if mode == "trend":
|
||||
return common + """
|
||||
|
||||
当前任务是“观势·解势”。六爻从初爻到上爻依次是个股内核、个股外显、板块内核、板块外显、指数内核、指数外显;初二为地、三四为人、五上为天。
|
||||
行情数据只负责生成六爻,本次解势必须以卦象本身为主,不得根据指数涨跌、板块强弱、涨停家数、成交量或个股表现直接推演方向。context中不会提供这些数字,也不会提供爻位对应的市场角色。
|
||||
先解释本卦卦名的核心义、上下卦组合及大象;再只解释实际动爻所代表的转折,并说明本卦如何走向之卦;最后可把这一组卦势翻译成克制的市场语言。
|
||||
重点是“本卦为当下之势,动爻为变化关节,之卦为所趋之势”。不要说明某一动爻对应指数、板块或个股,也不要输出“一看指数、二看涨停家数”一类行情观察条件。
|
||||
全文控制在300至450个中文字符,最多四小段。卦理约占九成,市场翻译最多一句,只能落到节制、等待、守信、辨伪等行为态度,不得据此预测市场下一阶段、涨跌方向或动能变化。不直接荐股,不使用Markdown表格。
|
||||
不要使用“必然、确定、必涨、必跌、后续将、进入某阶段”等断语;天机只点出势的性质与变化关系,不替用户宣布结果。
|
||||
""".strip()
|
||||
if mode == "fortune":
|
||||
return common + """
|
||||
|
||||
当前任务是“观气·解运”。严格区分五运、六气、节气、月令和日干,不把丙午简单解释为火年。
|
||||
严格服从five_phase_field.framework提供的确定性结构,不自行重新计算五行:年纲由中运与司天在泉构成;岁半以前司天为主、在泉为辅,岁半以后在泉为主、司天为辅;当前六气层以客气加临主气为核心;日辰只负责触发。节气只用于定位当前六气阶段,不得再次叠加为独立力量。
|
||||
重点解释framework.relations中的客主同气、客生主、主生客、客克主或主克客,以及客胜为从、主胜为逆、司天在泉同位、天符岁会等已经判定的关系。不得把司天、在泉、主气、客气视为彼此独立的证据重复计权,也不得自行增删传统格局。
|
||||
首要解释当日气场容易放大参与者的哪些情绪、判断偏差和操作冲动,例如急躁、恐惧、迟疑、追涨、过早止损或路径依赖;再给出一至两个调节动作。
|
||||
如有personal_profile,结合其日主、十神、五行平衡倾向说明当日对该用户主观状态的影响,但不得把简化平衡倾向说成唯一喜用神,也不得复述或猜测出生日期。
|
||||
不得引用市场上涨下跌家数、涨跌停数量、成交额、板块强度或个股表现来证明气场。industry_affinity只是五行行业取象示例,不是行情旁证;行业契合度最多在末尾用一句话说明,不得写“当日共振”或暗示相关行业必然涨跌。
|
||||
全文控制在420至600个中文字符,按“三层气机、人的状态、操作偏向、个人影响(如有)、制衡动作”组织,标题必须写“三层气机”。明确这些是传统历法框架下的观察语言,不宣称气候或五行直接导致股价。
|
||||
""".strip()
|
||||
return common + """
|
||||
|
||||
当前任务是“观心·解卦”。用户的问题始终只在心中,没有输入给你,因此你不能猜测问题内容,也不能替用户作具体决定。
|
||||
全文控制在180至350个中文字符。只写一句卦意;一小段动爻与之卦;最后三句极短的问心句。
|
||||
不要重述六条爻辞,不猜用户未说出口的问题,不以吉凶二字替代思考,不给出股票涨跌预测。语气安静、克制,越短越有余味。
|
||||
""".strip()
|
||||
|
||||
|
||||
def _http_error_message(exc: urllib.error.HTTPError) -> str:
|
||||
detail = ""
|
||||
try:
|
||||
payload = json.loads(exc.read().decode("utf-8", errors="replace"))
|
||||
error = payload.get("error")
|
||||
if isinstance(error, dict):
|
||||
detail = str(error.get("message") or error.get("code") or "")
|
||||
elif error:
|
||||
detail = str(error)
|
||||
elif payload.get("message"):
|
||||
detail = str(payload["message"])
|
||||
except (json.JSONDecodeError, OSError):
|
||||
detail = ""
|
||||
suffix = f":{detail[:300]}" if detail else ""
|
||||
return f"问天模型调用失败(HTTP {exc.code}){suffix}"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from http import HTTPStatus
|
||||
|
||||
|
||||
class HeavenHttpMixin:
|
||||
def heaven_hexagram(self) -> None:
|
||||
try:
|
||||
body = self.read_json_body()
|
||||
result = self.application_service.heaven_hexagram(body.get("lines"))
|
||||
self.send_json({"ok": True, "hexagram": result})
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
|
||||
|
||||
def heaven_personal(self) -> None:
|
||||
try:
|
||||
body = self.read_json_body()
|
||||
result = self.application_service.heaven_personal(body)
|
||||
self.send_json({"ok": True, "personal": result})
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
|
||||
|
||||
def heaven_interpret(self) -> None:
|
||||
try:
|
||||
body = self.read_json_body()
|
||||
result = self.application_service.heaven_interpret(body)
|
||||
self.send_json({"ok": True, **result})
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
|
||||
@@ -0,0 +1,111 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
|
||||
class HeavenRepositoryMixin:
|
||||
@staticmethod
|
||||
def _heaven_reading_dict(row: sqlite3.Row | None) -> dict[str, Any] | None:
|
||||
if not row:
|
||||
return None
|
||||
return {
|
||||
"id": int(row["id"]),
|
||||
"mode": str(row["mode"]),
|
||||
"context_date": str(row["context_date"]),
|
||||
"subject": str(row["subject"]),
|
||||
"subject_detail": str(row["subject_detail"]),
|
||||
"answer": str(row["answer"]),
|
||||
"created_at": str(row["created_at"]),
|
||||
}
|
||||
|
||||
def save_heaven_reading(
|
||||
self,
|
||||
user_id: int,
|
||||
mode: str,
|
||||
context_date: str,
|
||||
subject: str,
|
||||
subject_detail: str,
|
||||
answer: str,
|
||||
context_snapshot: dict[str, Any],
|
||||
dedupe_key: str,
|
||||
) -> dict[str, Any]:
|
||||
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
||||
snapshot_json = json.dumps(
|
||||
context_snapshot, ensure_ascii=False, separators=(",", ":")
|
||||
)
|
||||
with self.connect() as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO heaven_readings
|
||||
(user_id, mode, context_date, subject, subject_detail, answer,
|
||||
context_snapshot, dedupe_key, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(user_id, dedupe_key) DO NOTHING
|
||||
""",
|
||||
(
|
||||
int(user_id), mode, context_date, subject, subject_detail,
|
||||
answer, snapshot_json, dedupe_key, now,
|
||||
),
|
||||
)
|
||||
row = connection.execute(
|
||||
"""
|
||||
SELECT id, mode, context_date, subject, subject_detail, answer, created_at
|
||||
FROM heaven_readings WHERE user_id = ? AND dedupe_key = ?
|
||||
""",
|
||||
(int(user_id), dedupe_key),
|
||||
).fetchone()
|
||||
connection.execute(
|
||||
"""
|
||||
DELETE FROM heaven_readings
|
||||
WHERE user_id = ? AND mode = ? AND id NOT IN (
|
||||
SELECT id FROM heaven_readings
|
||||
WHERE user_id = ? AND mode = ? ORDER BY id DESC LIMIT 100
|
||||
)
|
||||
""",
|
||||
(int(user_id), mode, int(user_id), mode),
|
||||
)
|
||||
result = self._heaven_reading_dict(row)
|
||||
if not result:
|
||||
raise ValueError("解读记录保存失败。")
|
||||
return result
|
||||
|
||||
def list_heaven_readings(
|
||||
self,
|
||||
user_id: int,
|
||||
mode: str,
|
||||
context_date: str = "",
|
||||
limit: int = 100,
|
||||
) -> list[dict[str, Any]]:
|
||||
clauses = ["user_id = ?", "mode = ?"]
|
||||
parameters: list[Any] = [int(user_id), mode]
|
||||
if context_date:
|
||||
clauses.append("context_date = ?")
|
||||
parameters.append(context_date)
|
||||
parameters.append(max(1, min(100, int(limit))))
|
||||
with self.connect() as connection:
|
||||
rows = connection.execute(
|
||||
f"""
|
||||
SELECT id, mode, context_date, subject, subject_detail, answer, created_at
|
||||
FROM heaven_readings WHERE {' AND '.join(clauses)}
|
||||
ORDER BY context_date DESC, id DESC LIMIT ?
|
||||
""",
|
||||
parameters,
|
||||
).fetchall()
|
||||
return [self._heaven_reading_dict(row) for row in rows if row]
|
||||
|
||||
def latest_heaven_reading(
|
||||
self, user_id: int, mode: str, context_date: str = ""
|
||||
) -> dict[str, Any] | None:
|
||||
items = self.list_heaven_readings(user_id, mode, context_date, 1)
|
||||
return items[0] if items else None
|
||||
|
||||
def delete_heaven_reading(self, user_id: int, reading_id: int) -> bool:
|
||||
with self.connect() as connection:
|
||||
cursor = connection.execute(
|
||||
"DELETE FROM heaven_readings WHERE id = ? AND user_id = ?",
|
||||
(int(reading_id), int(user_id)),
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user