Fix supervisor AI empty replies with fallback templates
Skip appending AI error strings to the session and use event-specific fallback commentary when the model returns empty content. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,15 +1,23 @@
|
|||||||
"""交易监管:AI 评语与用户回聊。"""
|
"""交易监管:AI 评语与用户回聊。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
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.client import generate_text, model_label
|
||||||
from hub_ai.config import (
|
from hub_ai.config import (
|
||||||
CHAT_MAX_OUTPUT_TOKENS,
|
CHAT_MAX_OUTPUT_TOKENS,
|
||||||
CHAT_TEMPERATURE,
|
CHAT_TEMPERATURE,
|
||||||
trading_day_reset_hour,
|
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.prompts import SUPERVISOR_SYSTEM, build_supervisor_ai_prompt, build_supervisor_chat_prompt
|
||||||
from hub_ai.supervisor_store import (
|
from hub_ai.supervisor_store import (
|
||||||
append_supervisor_ai_message,
|
append_supervisor_ai_message,
|
||||||
@@ -17,8 +25,12 @@ from hub_ai.supervisor_store import (
|
|||||||
get_supervisor_session_state,
|
get_supervisor_session_state,
|
||||||
)
|
)
|
||||||
from hub_ai.store import append_chat_message
|
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
|
from hub_trades_lib import current_trading_day
|
||||||
|
|
||||||
|
SUPERVISOR_AI_MAX_TOKENS = 320
|
||||||
|
|
||||||
|
|
||||||
def generate_supervisor_ai_reply(
|
def generate_supervisor_ai_reply(
|
||||||
*,
|
*,
|
||||||
@@ -29,20 +41,21 @@ def generate_supervisor_ai_reply(
|
|||||||
exchanges: list[dict],
|
exchanges: list[dict],
|
||||||
) -> str:
|
) -> str:
|
||||||
ctx = build_chat_context(exchanges, trading_day=trading_day)
|
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(
|
user_prompt = build_supervisor_ai_prompt(
|
||||||
context_text=brief,
|
context_text=brief,
|
||||||
trading_day=trading_day,
|
trading_day=trading_day,
|
||||||
event=event,
|
event=event,
|
||||||
warnings=warnings,
|
warnings=warnings,
|
||||||
)
|
)
|
||||||
return generate_text(
|
prompt = f"{SUPERVISOR_SYSTEM.strip()}\n\n---\n\n{user_prompt.strip()}"
|
||||||
system=SUPERVISOR_SYSTEM,
|
text = ai_generate(prompt, temperature=0.35, max_tokens=SUPERVISOR_AI_MAX_TOKENS)
|
||||||
user=user_prompt,
|
text = str(text or "").strip()
|
||||||
temperature=min(0.35, CHAT_TEMPERATURE),
|
if not text or is_ai_error_reply(text):
|
||||||
max_tokens=min(512, CHAT_MAX_OUTPUT_TOKENS),
|
return build_supervisor_fallback_reply(event, warnings)
|
||||||
max_continuations=1,
|
return text
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def make_supervisor_ai_reply_fn(exchanges: list[dict]):
|
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_tokens=min(768, CHAT_MAX_OUTPUT_TOKENS),
|
||||||
max_continuations=1,
|
max_continuations=1,
|
||||||
)
|
)
|
||||||
if not reply or reply.strip().startswith("AI "):
|
reply = str(reply or "").strip()
|
||||||
return {"ok": False, "msg": reply or "AI 生成失败", "session_id": sid}
|
if not reply or is_ai_error_reply(reply):
|
||||||
|
return {"ok": False, "msg": "AI 暂时不可用,请稍后再试", "session_id": sid}
|
||||||
append_chat_message(sid, "user", text)
|
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)
|
state = get_supervisor_session_state(day)
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"trading_day": day,
|
"trading_day": day,
|
||||||
"session": session,
|
"session": session,
|
||||||
"reply": reply.strip(),
|
"reply": reply,
|
||||||
"model": model_label(),
|
"model": model_label(),
|
||||||
"message_count": state.get("message_count"),
|
"message_count": state.get("message_count"),
|
||||||
"unread_system": state.get("unread_system"),
|
"unread_system": state.get("unread_system"),
|
||||||
|
|||||||
@@ -388,6 +388,56 @@ def event_tag(event_type: str) -> str:
|
|||||||
}.get(event_type, "监管")
|
}.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:
|
def build_system_message(event: dict, *, trading_day: str, warnings: list[dict] | None = None) -> str:
|
||||||
tag = event_tag(str(event.get("event_type") or ""))
|
tag = event_tag(str(event.get("event_type") or ""))
|
||||||
ex = event.get("exchange_name") or event.get("account_name") or "—"
|
ex = event.get("exchange_name") or event.get("account_name") or "—"
|
||||||
@@ -611,11 +661,22 @@ def process_supervisor_tick(
|
|||||||
trading_day=trading_day,
|
trading_day=trading_day,
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
)
|
)
|
||||||
if reply and reply.strip():
|
from hub_ai.text_util import is_ai_error_reply
|
||||||
append_supervisor_ai_message(session_id, reply.strip())
|
|
||||||
|
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()
|
_fire_notify()
|
||||||
except Exception:
|
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()
|
threading.Thread(target=_ai_bg, daemon=True).start()
|
||||||
|
|
||||||
|
|||||||
@@ -136,3 +136,25 @@ def test_normalize_supervisor_settings_env(monkeypatch):
|
|||||||
cfg = sup.normalize_supervisor_settings({})
|
cfg = sup.normalize_supervisor_settings({})
|
||||||
assert cfg["wechat_webhook"] == "https://example.com/hook"
|
assert cfg["wechat_webhook"] == "https://example.com/hook"
|
||||||
assert cfg["wechat_link_base"] == "https://hub.example/ai?mode=supervisor"
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user