diff --git a/manual_trading_hub/hub_ai/supervisor.py b/manual_trading_hub/hub_ai/supervisor.py index f717374..af42331 100644 --- a/manual_trading_hub/hub_ai/supervisor.py +++ b/manual_trading_hub/hub_ai/supervisor.py @@ -1,15 +1,23 @@ """交易监管:AI 评语与用户回聊。""" from __future__ import annotations +import sys +from pathlib import Path from typing import Any, Optional +_REPO_ROOT = Path(__file__).resolve().parents[2] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from ai_client import ai_generate # noqa: E402 + from hub_ai.client import generate_text, model_label from hub_ai.config import ( CHAT_MAX_OUTPUT_TOKENS, CHAT_TEMPERATURE, trading_day_reset_hour, ) -from hub_ai.context import build_chat_context, format_chat_context_for_chat +from hub_ai.context import build_chat_context, format_chat_context_for_chat, format_chat_position_overview from hub_ai.prompts import SUPERVISOR_SYSTEM, build_supervisor_ai_prompt, build_supervisor_chat_prompt from hub_ai.supervisor_store import ( append_supervisor_ai_message, @@ -17,8 +25,12 @@ from hub_ai.supervisor_store import ( get_supervisor_session_state, ) from hub_ai.store import append_chat_message +from hub_ai.text_util import is_ai_error_reply +from hub_supervisor_lib import build_supervisor_fallback_reply from hub_trades_lib import current_trading_day +SUPERVISOR_AI_MAX_TOKENS = 320 + def generate_supervisor_ai_reply( *, @@ -29,20 +41,21 @@ def generate_supervisor_ai_reply( exchanges: list[dict], ) -> str: ctx = build_chat_context(exchanges, trading_day=trading_day) - brief = format_chat_context_for_chat(ctx, max_chars=6000) + brief = format_chat_position_overview(ctx) + "\n" + format_chat_context_for_chat( + ctx, max_chars=2400 + ) user_prompt = build_supervisor_ai_prompt( context_text=brief, trading_day=trading_day, event=event, warnings=warnings, ) - return generate_text( - system=SUPERVISOR_SYSTEM, - user=user_prompt, - temperature=min(0.35, CHAT_TEMPERATURE), - max_tokens=min(512, CHAT_MAX_OUTPUT_TOKENS), - max_continuations=1, - ) + prompt = f"{SUPERVISOR_SYSTEM.strip()}\n\n---\n\n{user_prompt.strip()}" + text = ai_generate(prompt, temperature=0.35, max_tokens=SUPERVISOR_AI_MAX_TOKENS) + text = str(text or "").strip() + if not text or is_ai_error_reply(text): + return build_supervisor_fallback_reply(event, warnings) + return text def make_supervisor_ai_reply_fn(exchanges: list[dict]): @@ -95,16 +108,17 @@ def send_supervisor_chat( max_tokens=min(768, CHAT_MAX_OUTPUT_TOKENS), max_continuations=1, ) - if not reply or reply.strip().startswith("AI "): - return {"ok": False, "msg": reply or "AI 生成失败", "session_id": sid} + reply = str(reply or "").strip() + if not reply or is_ai_error_reply(reply): + return {"ok": False, "msg": "AI 暂时不可用,请稍后再试", "session_id": sid} append_chat_message(sid, "user", text) - session = append_supervisor_ai_message(sid, reply.strip()) + session = append_supervisor_ai_message(sid, reply) state = get_supervisor_session_state(day) return { "ok": True, "trading_day": day, "session": session, - "reply": reply.strip(), + "reply": reply, "model": model_label(), "message_count": state.get("message_count"), "unread_system": state.get("unread_system"), diff --git a/manual_trading_hub/hub_supervisor_lib.py b/manual_trading_hub/hub_supervisor_lib.py index 7ac2e9a..52c4919 100644 --- a/manual_trading_hub/hub_supervisor_lib.py +++ b/manual_trading_hub/hub_supervisor_lib.py @@ -388,6 +388,56 @@ def event_tag(event_type: str) -> str: }.get(event_type, "监管") +def _fmt_pnl_u(pnl: Any) -> str: + try: + v = float(pnl) + sign = "+" if v > 0 else "" + return f"{sign}{v:.4f}".rstrip("0").rstrip(".") + "U" + except (TypeError, ValueError): + return "" + + +def build_supervisor_fallback_reply(event: dict, warnings: list[dict] | None = None) -> str: + """AI 不可用或返回空时的短评语(不展示错误文案)。""" + et = str(event.get("event_type") or "") + sym = str(event.get("symbol") or "—") + ex = str(event.get("exchange_name") or event.get("account_name") or "").strip() + pnl_txt = _fmt_pnl_u(event.get("pnl_amount")) + warn = (warnings or [])[:1] + warn_txt = str(warn[0].get("message") or "").strip() if warn else "" + + if et == EVENT_PROGRAM_SL: + base = f"{sym} 程序止损" + if pnl_txt: + base += f"({pnl_txt})" + base += ",按计划出场是纪律。先歇一会儿,别急着马上再开。" + elif et == EVENT_PROGRAM_TP: + base = f"{sym} 程序止盈" + if pnl_txt: + base += f"({pnl_txt})" + base += ",执行不错。保持节奏,别立刻反手再开一单。" + elif et == EVENT_OPEN: + who = f"{ex} " if ex else "" + base = f"看到 {who}新开 {sym}。动手前确认是不是计划内,别因为上一笔情绪再开。" + elif et == EVENT_HUB_CLOSE: + base = f"中控平了 {sym}" + if pnl_txt: + base += f"({pnl_txt})" + base += "。" + base += f" {warn_txt}" if warn_txt else " 停一停,别连着手痒。" + elif et == EVENT_MANUAL_CLOSE: + base = f"手动平了 {sym}" + if pnl_txt: + base += f"({pnl_txt})" + base += "。" + base += f" {warn_txt}" if warn_txt else " 想好再开下一单。" + elif et == EVENT_FREQ_WARN: + base = warn_txt or "今日操作偏频繁,先休息一会儿。" + else: + base = "收到。确认是否按计划执行,别连续加码。" + return base.strip()[:320] + + def build_system_message(event: dict, *, trading_day: str, warnings: list[dict] | None = None) -> str: tag = event_tag(str(event.get("event_type") or "")) ex = event.get("exchange_name") or event.get("account_name") or "—" @@ -611,11 +661,22 @@ def process_supervisor_tick( trading_day=trading_day, session_id=session_id, ) - if reply and reply.strip(): - append_supervisor_ai_message(session_id, reply.strip()) + from hub_ai.text_util import is_ai_error_reply + + text = str(reply or "").strip() + if not text or is_ai_error_reply(text): + text = build_supervisor_fallback_reply(evt_snapshot, evt_warnings) + if text: + append_supervisor_ai_message(session_id, text) _fire_notify() except Exception: - pass + try: + fb = build_supervisor_fallback_reply(evt_snapshot, evt_warnings) + if fb: + append_supervisor_ai_message(session_id, fb) + _fire_notify() + except Exception: + pass threading.Thread(target=_ai_bg, daemon=True).start() diff --git a/tests/test_hub_supervisor_lib.py b/tests/test_hub_supervisor_lib.py index 07ee81d..4caefae 100644 --- a/tests/test_hub_supervisor_lib.py +++ b/tests/test_hub_supervisor_lib.py @@ -136,3 +136,25 @@ def test_normalize_supervisor_settings_env(monkeypatch): cfg = sup.normalize_supervisor_settings({}) assert cfg["wechat_webhook"] == "https://example.com/hook" assert cfg["wechat_link_base"] == "https://hub.example/ai?mode=supervisor" + + +def test_supervisor_fallback_reply_program_sl(): + text = sup.build_supervisor_fallback_reply( + { + "event_type": sup.EVENT_PROGRAM_SL, + "symbol": "ZEC/USDT", + "pnl_amount": -0.9557, + } + ) + assert "程序止损" in text + assert "AI 生成失败" not in text + + +def test_supervisor_fallback_not_error_reply(): + from hub_ai.text_util import is_ai_error_reply + + text = sup.build_supervisor_fallback_reply( + {"event_type": sup.EVENT_MANUAL_CLOSE, "symbol": "ETH/USDT", "pnl_amount": -1} + ) + assert text + assert not is_ai_error_reply(text)