refactor: 将共用代码迁入 lib/ 模块化目录
统一 strategy、key_monitor、trade、hub 等共用库到 lib/ 子包,并补充 lib-structure 文档,便于四所与中控维护。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,161 +1,161 @@
|
||||
"""内照明心复盘语录 → 交易教练点评。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from hub_ai.client import generate_text, model_label
|
||||
from hub_ai.rolling_summary import refresh_session_rolling_summary
|
||||
from hub_ai.text_util import clip_text, is_ai_error_reply
|
||||
from hub_ai.config import (
|
||||
CHAT_MAX_CONTINUATIONS,
|
||||
CHAT_MAX_OUTPUT_TOKENS,
|
||||
CHAT_TEMPERATURE,
|
||||
CHAT_USER_MESSAGE_MAX_CHARS,
|
||||
)
|
||||
from hub_ai.prompts import CHAT_SYSTEM, build_archive_quote_review_prompt
|
||||
from hub_ai.store import (
|
||||
CHAT_BOT_TRADING,
|
||||
append_chat_message,
|
||||
create_new_session,
|
||||
delete_chat_session,
|
||||
get_active_session,
|
||||
list_chat_sessions,
|
||||
)
|
||||
from hub_symbol_archive_lib import list_daily_trades
|
||||
|
||||
|
||||
def _tag_label(tag: str) -> str:
|
||||
t = (tag or "").strip().lower()
|
||||
if t == "sick":
|
||||
return "犯病"
|
||||
if t == "emotion":
|
||||
return "情绪化"
|
||||
return t or "—"
|
||||
|
||||
|
||||
def _fmt_pnl(v: Any) -> str:
|
||||
try:
|
||||
n = float(v or 0)
|
||||
except (TypeError, ValueError):
|
||||
return "—"
|
||||
sign = "+" if n > 0 else ""
|
||||
return f"{sign}{n:.2f}U"
|
||||
|
||||
|
||||
def _fmt_pct(v: Any) -> str:
|
||||
try:
|
||||
n = float(v)
|
||||
except (TypeError, ValueError):
|
||||
return "—"
|
||||
return f"{n:.1f}%"
|
||||
|
||||
|
||||
def _fmt_rr(v: Any) -> str:
|
||||
try:
|
||||
n = float(v)
|
||||
except (TypeError, ValueError):
|
||||
return "—"
|
||||
return f"{n:.2f}:1"
|
||||
|
||||
|
||||
def format_archive_trades_for_ai(payload: dict[str, Any]) -> str:
|
||||
trades = payload.get("trades") or []
|
||||
stats = payload.get("stats") or {}
|
||||
lines = [
|
||||
(
|
||||
f"统计:开仓 {int(stats.get('open_count') or 0)} 笔,"
|
||||
f"盈利 {int(stats.get('win_count') or 0)} / 亏损 {int(stats.get('loss_count') or 0)},"
|
||||
f"平均盈利 {_fmt_pnl(stats.get('avg_win'))},平均亏损 {_fmt_pnl(stats.get('avg_loss'))},"
|
||||
f"胜率 {_fmt_pct(stats.get('win_rate'))},盈亏比 {_fmt_rr(stats.get('profit_loss_ratio'))},"
|
||||
f"最大盈利 {_fmt_pnl(stats.get('max_win'))},最大亏损 {_fmt_pnl(stats.get('max_loss'))},"
|
||||
f"犯病 {int(stats.get('sick_count') or 0)} 笔,"
|
||||
f"盈亏合计 {_fmt_pnl(stats.get('pnl_total'))},"
|
||||
f"剔除犯病盈亏 {_fmt_pnl(stats.get('pnl_ex_sick'))}"
|
||||
)
|
||||
]
|
||||
if not trades:
|
||||
lines.append("(该日无交易记录)")
|
||||
return "\n".join(lines)
|
||||
max_rows = 50
|
||||
if len(trades) > max_rows:
|
||||
lines.append(f"(共 {len(trades)} 笔,以下展示最近 {max_rows} 笔)")
|
||||
for i, t in enumerate(trades[:max_rows], 1):
|
||||
ex = str(t.get("exchange_key") or t.get("account_exchange_key") or "—")
|
||||
sym = str(t.get("symbol") or "—")
|
||||
direction = str(t.get("direction") or "—")
|
||||
opened = str(t.get("opened_at") or "—")
|
||||
closed = str(t.get("closed_at") or "—")
|
||||
hold = str(t.get("hold_minutes_text") or t.get("hold_minutes") or "—")
|
||||
result = str(t.get("result") or "—")
|
||||
pnl = _fmt_pnl(t.get("pnl_amount"))
|
||||
entry = str(t.get("entry_type") or t.get("entry_reason") or t.get("monitor_type") or "—")
|
||||
tag = _tag_label(str(t.get("behavior_tag") or ""))
|
||||
note = clip_text(str(t.get("note") or "").strip(), 80)
|
||||
line = (
|
||||
f"{i}. {ex} | {sym} | {direction} | 开仓类型 {entry} | "
|
||||
f"开 {opened} | 平 {closed} | 持仓 {hold} | 结果 {result} | "
|
||||
f"盈亏 {pnl} | 标签 {tag}"
|
||||
)
|
||||
if note:
|
||||
line += f" | 备注 {note}"
|
||||
lines.append(line)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def send_archive_quote_review(
|
||||
*,
|
||||
quote_date: str,
|
||||
content: str,
|
||||
) -> dict[str, Any]:
|
||||
text = (content or "").strip()
|
||||
if not text:
|
||||
return {"ok": False, "msg": "语录内容不能为空"}
|
||||
day = (quote_date or "").strip()[:10]
|
||||
if not day:
|
||||
return {"ok": False, "msg": "语录日期无效"}
|
||||
|
||||
session = create_new_session(
|
||||
trading_day=day,
|
||||
title=f"复盘 {day}",
|
||||
bot_mode=CHAT_BOT_TRADING,
|
||||
)
|
||||
sid = session["id"]
|
||||
|
||||
archive_payload = list_daily_trades(trading_day=day, period="today")
|
||||
archive_trades_text = format_archive_trades_for_ai(archive_payload)
|
||||
user_for_prompt = clip_text(text, CHAT_USER_MESSAGE_MAX_CHARS)
|
||||
|
||||
user_prompt = build_archive_quote_review_prompt(
|
||||
quote_date=day,
|
||||
archive_trades_text=archive_trades_text,
|
||||
user_message=user_for_prompt,
|
||||
)
|
||||
reply = generate_text(
|
||||
system=CHAT_SYSTEM,
|
||||
user=user_prompt,
|
||||
temperature=CHAT_TEMPERATURE,
|
||||
max_tokens=CHAT_MAX_OUTPUT_TOKENS,
|
||||
max_continuations=CHAT_MAX_CONTINUATIONS,
|
||||
)
|
||||
if is_ai_error_reply(reply):
|
||||
delete_chat_session(sid)
|
||||
return {"ok": False, "msg": reply}
|
||||
|
||||
append_chat_message(sid, "user", text)
|
||||
session = append_chat_message(sid, "assistant", reply)
|
||||
refresh_session_rolling_summary(
|
||||
sid,
|
||||
prior_summary="",
|
||||
user_text=text,
|
||||
assistant_text=reply,
|
||||
bot_mode=CHAT_BOT_TRADING,
|
||||
)
|
||||
session = get_active_session() or session
|
||||
return {
|
||||
"ok": True,
|
||||
"trading_day": day,
|
||||
"session": session,
|
||||
"sessions": list_chat_sessions(),
|
||||
"reply": reply,
|
||||
"model": model_label(),
|
||||
}
|
||||
"""内照明心复盘语录 → 交易教练点评。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from hub_ai.client import generate_text, model_label
|
||||
from hub_ai.rolling_summary import refresh_session_rolling_summary
|
||||
from hub_ai.text_util import clip_text, is_ai_error_reply
|
||||
from hub_ai.config import (
|
||||
CHAT_MAX_CONTINUATIONS,
|
||||
CHAT_MAX_OUTPUT_TOKENS,
|
||||
CHAT_TEMPERATURE,
|
||||
CHAT_USER_MESSAGE_MAX_CHARS,
|
||||
)
|
||||
from hub_ai.prompts import CHAT_SYSTEM, build_archive_quote_review_prompt
|
||||
from hub_ai.store import (
|
||||
CHAT_BOT_TRADING,
|
||||
append_chat_message,
|
||||
create_new_session,
|
||||
delete_chat_session,
|
||||
get_active_session,
|
||||
list_chat_sessions,
|
||||
)
|
||||
from lib.hub.hub_symbol_archive_lib import list_daily_trades
|
||||
|
||||
|
||||
def _tag_label(tag: str) -> str:
|
||||
t = (tag or "").strip().lower()
|
||||
if t == "sick":
|
||||
return "犯病"
|
||||
if t == "emotion":
|
||||
return "情绪化"
|
||||
return t or "—"
|
||||
|
||||
|
||||
def _fmt_pnl(v: Any) -> str:
|
||||
try:
|
||||
n = float(v or 0)
|
||||
except (TypeError, ValueError):
|
||||
return "—"
|
||||
sign = "+" if n > 0 else ""
|
||||
return f"{sign}{n:.2f}U"
|
||||
|
||||
|
||||
def _fmt_pct(v: Any) -> str:
|
||||
try:
|
||||
n = float(v)
|
||||
except (TypeError, ValueError):
|
||||
return "—"
|
||||
return f"{n:.1f}%"
|
||||
|
||||
|
||||
def _fmt_rr(v: Any) -> str:
|
||||
try:
|
||||
n = float(v)
|
||||
except (TypeError, ValueError):
|
||||
return "—"
|
||||
return f"{n:.2f}:1"
|
||||
|
||||
|
||||
def format_archive_trades_for_ai(payload: dict[str, Any]) -> str:
|
||||
trades = payload.get("trades") or []
|
||||
stats = payload.get("stats") or {}
|
||||
lines = [
|
||||
(
|
||||
f"统计:开仓 {int(stats.get('open_count') or 0)} 笔,"
|
||||
f"盈利 {int(stats.get('win_count') or 0)} / 亏损 {int(stats.get('loss_count') or 0)},"
|
||||
f"平均盈利 {_fmt_pnl(stats.get('avg_win'))},平均亏损 {_fmt_pnl(stats.get('avg_loss'))},"
|
||||
f"胜率 {_fmt_pct(stats.get('win_rate'))},盈亏比 {_fmt_rr(stats.get('profit_loss_ratio'))},"
|
||||
f"最大盈利 {_fmt_pnl(stats.get('max_win'))},最大亏损 {_fmt_pnl(stats.get('max_loss'))},"
|
||||
f"犯病 {int(stats.get('sick_count') or 0)} 笔,"
|
||||
f"盈亏合计 {_fmt_pnl(stats.get('pnl_total'))},"
|
||||
f"剔除犯病盈亏 {_fmt_pnl(stats.get('pnl_ex_sick'))}"
|
||||
)
|
||||
]
|
||||
if not trades:
|
||||
lines.append("(该日无交易记录)")
|
||||
return "\n".join(lines)
|
||||
max_rows = 50
|
||||
if len(trades) > max_rows:
|
||||
lines.append(f"(共 {len(trades)} 笔,以下展示最近 {max_rows} 笔)")
|
||||
for i, t in enumerate(trades[:max_rows], 1):
|
||||
ex = str(t.get("exchange_key") or t.get("account_exchange_key") or "—")
|
||||
sym = str(t.get("symbol") or "—")
|
||||
direction = str(t.get("direction") or "—")
|
||||
opened = str(t.get("opened_at") or "—")
|
||||
closed = str(t.get("closed_at") or "—")
|
||||
hold = str(t.get("hold_minutes_text") or t.get("hold_minutes") or "—")
|
||||
result = str(t.get("result") or "—")
|
||||
pnl = _fmt_pnl(t.get("pnl_amount"))
|
||||
entry = str(t.get("entry_type") or t.get("entry_reason") or t.get("monitor_type") or "—")
|
||||
tag = _tag_label(str(t.get("behavior_tag") or ""))
|
||||
note = clip_text(str(t.get("note") or "").strip(), 80)
|
||||
line = (
|
||||
f"{i}. {ex} | {sym} | {direction} | 开仓类型 {entry} | "
|
||||
f"开 {opened} | 平 {closed} | 持仓 {hold} | 结果 {result} | "
|
||||
f"盈亏 {pnl} | 标签 {tag}"
|
||||
)
|
||||
if note:
|
||||
line += f" | 备注 {note}"
|
||||
lines.append(line)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def send_archive_quote_review(
|
||||
*,
|
||||
quote_date: str,
|
||||
content: str,
|
||||
) -> dict[str, Any]:
|
||||
text = (content or "").strip()
|
||||
if not text:
|
||||
return {"ok": False, "msg": "语录内容不能为空"}
|
||||
day = (quote_date or "").strip()[:10]
|
||||
if not day:
|
||||
return {"ok": False, "msg": "语录日期无效"}
|
||||
|
||||
session = create_new_session(
|
||||
trading_day=day,
|
||||
title=f"复盘 {day}",
|
||||
bot_mode=CHAT_BOT_TRADING,
|
||||
)
|
||||
sid = session["id"]
|
||||
|
||||
archive_payload = list_daily_trades(trading_day=day, period="today")
|
||||
archive_trades_text = format_archive_trades_for_ai(archive_payload)
|
||||
user_for_prompt = clip_text(text, CHAT_USER_MESSAGE_MAX_CHARS)
|
||||
|
||||
user_prompt = build_archive_quote_review_prompt(
|
||||
quote_date=day,
|
||||
archive_trades_text=archive_trades_text,
|
||||
user_message=user_for_prompt,
|
||||
)
|
||||
reply = generate_text(
|
||||
system=CHAT_SYSTEM,
|
||||
user=user_prompt,
|
||||
temperature=CHAT_TEMPERATURE,
|
||||
max_tokens=CHAT_MAX_OUTPUT_TOKENS,
|
||||
max_continuations=CHAT_MAX_CONTINUATIONS,
|
||||
)
|
||||
if is_ai_error_reply(reply):
|
||||
delete_chat_session(sid)
|
||||
return {"ok": False, "msg": reply}
|
||||
|
||||
append_chat_message(sid, "user", text)
|
||||
session = append_chat_message(sid, "assistant", reply)
|
||||
refresh_session_rolling_summary(
|
||||
sid,
|
||||
prior_summary="",
|
||||
user_text=text,
|
||||
assistant_text=reply,
|
||||
bot_mode=CHAT_BOT_TRADING,
|
||||
)
|
||||
session = get_active_session() or session
|
||||
return {
|
||||
"ok": True,
|
||||
"trading_day": day,
|
||||
"session": session,
|
||||
"sessions": list_chat_sessions(),
|
||||
"reply": reply,
|
||||
"model": model_label(),
|
||||
}
|
||||
|
||||
+275
-275
@@ -1,275 +1,275 @@
|
||||
"""中控 AI:单会话聊天(直到用户点击新开)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from typing import Any, Optional
|
||||
|
||||
from hub_ai.attachments import parse_chat_attachments
|
||||
from hub_ai.client import generate_text, model_label
|
||||
from hub_ai.config import (
|
||||
CHAT_CONTEXT_MAX_CHARS,
|
||||
CHAT_FOLLOWUP_CONTEXT_MAX_CHARS,
|
||||
CHAT_HISTORY_MAX_CHARS_PER_MSG,
|
||||
CHAT_MAX_CONTINUATIONS,
|
||||
CHAT_MAX_HISTORY_TURNS,
|
||||
CHAT_MAX_OUTPUT_TOKENS,
|
||||
CHAT_PROMPT_MAX_CHARS,
|
||||
CHAT_SUMMARY_EXCERPT_MAX_CHARS,
|
||||
CHAT_TEMPERATURE,
|
||||
CHAT_USER_MESSAGE_MAX_CHARS,
|
||||
trading_day_reset_hour,
|
||||
)
|
||||
from hub_trades_lib import current_trading_day
|
||||
from hub_ai.context import (
|
||||
build_chat_context,
|
||||
format_chat_context_for_chat,
|
||||
format_chat_position_overview,
|
||||
)
|
||||
from hub_ai.prompts import (
|
||||
CHAT_GENERAL_SYSTEM,
|
||||
CHAT_SYSTEM,
|
||||
build_chat_user_prompt,
|
||||
build_general_chat_user_prompt,
|
||||
)
|
||||
from hub_ai.rolling_summary import refresh_session_rolling_summary
|
||||
from hub_ai.store import (
|
||||
CHAT_BOT_GENERAL,
|
||||
CHAT_BOT_TRADING,
|
||||
append_chat_message,
|
||||
create_new_session,
|
||||
delete_chat_session,
|
||||
ensure_active_session,
|
||||
get_active_session,
|
||||
list_chat_sessions,
|
||||
load_chat_store,
|
||||
set_active_session,
|
||||
summary_excerpt_for_chat,
|
||||
)
|
||||
from hub_ai.text_util import clip_text, is_ai_error_reply
|
||||
|
||||
|
||||
def _is_ai_error_reply(text: str) -> bool:
|
||||
return is_ai_error_reply(text)
|
||||
|
||||
|
||||
def _clip_text(text: str, max_chars: int) -> str:
|
||||
return clip_text(text, max_chars)
|
||||
|
||||
|
||||
def _history_lines(
|
||||
messages: list[dict],
|
||||
max_turns: int = CHAT_MAX_HISTORY_TURNS,
|
||||
*,
|
||||
max_chars_per_msg: int = CHAT_HISTORY_MAX_CHARS_PER_MSG,
|
||||
total_max_chars: int | None = None,
|
||||
) -> str:
|
||||
rows = [m for m in (messages or []) if m.get("role") in ("user", "assistant")]
|
||||
rows = rows[-max_turns * 2 :]
|
||||
lines = []
|
||||
for m in rows:
|
||||
role = "用户" if m.get("role") == "user" else "搭档"
|
||||
content = str(m.get("content") or "").strip()
|
||||
if m.get("role") == "assistant" and _is_ai_error_reply(content):
|
||||
continue
|
||||
att = m.get("attachments") or []
|
||||
if att:
|
||||
names = "、".join(str(a.get("name") or "附件") for a in att[:3])
|
||||
content = f"{content} [附件: {names}]".strip()
|
||||
content = _clip_text(content, max_chars_per_msg)
|
||||
if content:
|
||||
lines.append(f"{role}:{content}")
|
||||
if total_max_chars and total_max_chars > 0:
|
||||
while lines and len("\n".join(lines)) > total_max_chars:
|
||||
lines.pop(0)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _trading_context_bundle(ctx: dict[str, Any], *, prior_count: int) -> tuple[str, str]:
|
||||
day = str(ctx.get("trading_day") or (ctx.get("totals") or {}).get("trading_day") or "")
|
||||
if prior_count <= 0:
|
||||
brief = format_chat_context_for_chat(ctx, max_chars=CHAT_CONTEXT_MAX_CHARS)
|
||||
excerpt = summary_excerpt_for_chat(day, max_chars=CHAT_SUMMARY_EXCERPT_MAX_CHARS)
|
||||
return brief, excerpt
|
||||
totals = ctx.get("totals") or {}
|
||||
overview = format_chat_position_overview(ctx)
|
||||
slim = (
|
||||
f"【续聊快照 {day}】平仓盈亏 {totals.get('total_pnl_u')}U | "
|
||||
f"笔数 {totals.get('closed_count')} | "
|
||||
f"持仓 {totals.get('open_position_count', 0)} 仓 | "
|
||||
f"浮盈亏 {totals.get('float_pnl_u')}U"
|
||||
)
|
||||
brief = _clip_text(overview + "\n" + slim, CHAT_FOLLOWUP_CONTEXT_MAX_CHARS)
|
||||
return brief, ""
|
||||
|
||||
|
||||
def _history_budget(*sizes: int) -> int:
|
||||
used = sum(int(s or 0) for s in sizes) + 2200
|
||||
return max(1200, CHAT_PROMPT_MAX_CHARS - used)
|
||||
|
||||
|
||||
def _prompt_memory(session: dict, prior_msgs: list[dict]) -> tuple[str, str]:
|
||||
"""续聊优先用滚动摘要;旧会话无摘要时仅带最近 1 轮兜底。"""
|
||||
rolling = str(session.get("rolling_summary") or "").strip()
|
||||
if rolling:
|
||||
return rolling, ""
|
||||
prior_count = len([m for m in prior_msgs if m.get("role") in ("user", "assistant")])
|
||||
if prior_count <= 0:
|
||||
return "", ""
|
||||
tail = _history_lines(
|
||||
prior_msgs,
|
||||
max_turns=1,
|
||||
max_chars_per_msg=CHAT_HISTORY_MAX_CHARS_PER_MSG,
|
||||
)
|
||||
return "", tail
|
||||
|
||||
|
||||
def get_chat_state() -> dict[str, Any]:
|
||||
store = load_chat_store()
|
||||
session = get_active_session()
|
||||
if session:
|
||||
session.setdefault("bot_mode", CHAT_BOT_TRADING)
|
||||
session.setdefault("rolling_summary", "")
|
||||
return {
|
||||
"active_session_id": store.get("active_session_id"),
|
||||
"session": session,
|
||||
"sessions": list_chat_sessions(),
|
||||
"model": model_label(),
|
||||
}
|
||||
|
||||
|
||||
def start_new_chat(*, trading_day: str, bot_mode: str = CHAT_BOT_TRADING) -> dict:
|
||||
session = create_new_session(trading_day=trading_day, bot_mode=bot_mode)
|
||||
return {
|
||||
"ok": True,
|
||||
"session": session,
|
||||
"sessions": list_chat_sessions(),
|
||||
"model": model_label(),
|
||||
}
|
||||
|
||||
|
||||
def switch_chat_session(session_id: str) -> dict[str, Any]:
|
||||
session = set_active_session(session_id)
|
||||
return {
|
||||
"ok": True,
|
||||
"session": session,
|
||||
"sessions": list_chat_sessions(),
|
||||
"model": model_label(),
|
||||
}
|
||||
|
||||
|
||||
def remove_chat_session(session_id: str) -> dict[str, Any]:
|
||||
deleted, new_active = delete_chat_session(session_id)
|
||||
if not deleted:
|
||||
return {"ok": False, "msg": "session_not_found"}
|
||||
session = get_active_session()
|
||||
return {
|
||||
"ok": True,
|
||||
"active_session_id": new_active,
|
||||
"session": session,
|
||||
"sessions": list_chat_sessions(),
|
||||
"model": model_label(),
|
||||
}
|
||||
|
||||
|
||||
def send_chat_message(
|
||||
exchanges: list[dict],
|
||||
message: str,
|
||||
*,
|
||||
trading_day: str | None = None,
|
||||
raw_attachments: Optional[list[dict]] = None,
|
||||
) -> dict[str, Any]:
|
||||
text = (message or "").strip()
|
||||
parsed = parse_chat_attachments(raw_attachments or [])
|
||||
if parsed.get("errors") and not text and not parsed.get("images_b64"):
|
||||
return {"ok": False, "msg": ";".join(parsed["errors"])}
|
||||
if not text and not parsed.get("images_b64") and not parsed.get("text_append"):
|
||||
return {"ok": False, "msg": "消息不能为空"}
|
||||
|
||||
user_visible = text
|
||||
if parsed.get("text_append"):
|
||||
user_visible = (user_visible + "\n\n" + parsed["text_append"]).strip()
|
||||
if not user_visible and parsed.get("attachment_note"):
|
||||
user_visible = f"(上传了 {parsed['attachment_note']})"
|
||||
|
||||
day = (trading_day or "").strip()[:10] or current_trading_day(
|
||||
reset_hour=trading_day_reset_hour()
|
||||
)
|
||||
session = ensure_active_session(trading_day=day)
|
||||
sid = session["id"]
|
||||
prior_rolling = str(session.get("rolling_summary") or "")
|
||||
prior_msgs = session.get("messages") or []
|
||||
prior_count = len([m for m in prior_msgs if m.get("role") in ("user", "assistant")])
|
||||
user_for_prompt = _clip_text(text or user_visible, CHAT_USER_MESSAGE_MAX_CHARS)
|
||||
rolling_summary, history_tail = _prompt_memory(session, prior_msgs)
|
||||
|
||||
bot_mode = (session.get("bot_mode") or CHAT_BOT_TRADING).strip().lower()
|
||||
if bot_mode == CHAT_BOT_GENERAL:
|
||||
user_prompt = build_general_chat_user_prompt(
|
||||
rolling_summary=rolling_summary,
|
||||
history_lines=history_tail,
|
||||
user_message=user_for_prompt,
|
||||
attachment_note=str(parsed.get("attachment_note") or ""),
|
||||
)
|
||||
if parsed.get("text_append"):
|
||||
user_prompt += "\n\n【附件正文】\n" + _clip_text(parsed["text_append"], 3000)
|
||||
system_prompt = CHAT_GENERAL_SYSTEM
|
||||
else:
|
||||
ctx = build_chat_context(exchanges, trading_day=day)
|
||||
day = ctx["trading_day"]
|
||||
brief_ctx, excerpt = _trading_context_bundle(ctx, prior_count=prior_count)
|
||||
user_prompt = build_chat_user_prompt(
|
||||
context_text=brief_ctx,
|
||||
trading_day=day,
|
||||
summary_excerpt=excerpt,
|
||||
rolling_summary=rolling_summary,
|
||||
history_lines=history_tail,
|
||||
user_message=user_for_prompt,
|
||||
attachment_note=str(parsed.get("attachment_note") or ""),
|
||||
)
|
||||
if parsed.get("text_append"):
|
||||
user_prompt += "\n\n【附件正文】\n" + _clip_text(parsed["text_append"], 3000)
|
||||
system_prompt = CHAT_SYSTEM
|
||||
|
||||
reply = generate_text(
|
||||
system=system_prompt,
|
||||
user=user_prompt,
|
||||
temperature=CHAT_TEMPERATURE,
|
||||
images_b64=parsed.get("images_b64") or None,
|
||||
max_tokens=CHAT_MAX_OUTPUT_TOKENS,
|
||||
max_continuations=CHAT_MAX_CONTINUATIONS,
|
||||
)
|
||||
if _is_ai_error_reply(reply):
|
||||
return {"ok": False, "msg": reply, "session_id": sid}
|
||||
|
||||
append_chat_message(
|
||||
sid,
|
||||
"user",
|
||||
user_visible,
|
||||
attachments=parsed.get("attachment_meta") or [],
|
||||
)
|
||||
session = append_chat_message(sid, "assistant", reply)
|
||||
summary_kwargs = {
|
||||
"session_id": sid,
|
||||
"prior_summary": prior_rolling,
|
||||
"user_text": user_visible,
|
||||
"assistant_text": reply,
|
||||
"bot_mode": bot_mode,
|
||||
}
|
||||
|
||||
def _refresh_summary_bg() -> None:
|
||||
try:
|
||||
refresh_session_rolling_summary(**summary_kwargs)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
threading.Thread(target=_refresh_summary_bg, daemon=True).start()
|
||||
session = get_active_session() or session
|
||||
return {
|
||||
"ok": True,
|
||||
"trading_day": day,
|
||||
"session": session,
|
||||
"sessions": list_chat_sessions(),
|
||||
"reply": reply,
|
||||
"model": model_label(),
|
||||
"attachment_warnings": parsed.get("errors") or [],
|
||||
}
|
||||
"""中控 AI:单会话聊天(直到用户点击新开)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from typing import Any, Optional
|
||||
|
||||
from hub_ai.attachments import parse_chat_attachments
|
||||
from hub_ai.client import generate_text, model_label
|
||||
from hub_ai.config import (
|
||||
CHAT_CONTEXT_MAX_CHARS,
|
||||
CHAT_FOLLOWUP_CONTEXT_MAX_CHARS,
|
||||
CHAT_HISTORY_MAX_CHARS_PER_MSG,
|
||||
CHAT_MAX_CONTINUATIONS,
|
||||
CHAT_MAX_HISTORY_TURNS,
|
||||
CHAT_MAX_OUTPUT_TOKENS,
|
||||
CHAT_PROMPT_MAX_CHARS,
|
||||
CHAT_SUMMARY_EXCERPT_MAX_CHARS,
|
||||
CHAT_TEMPERATURE,
|
||||
CHAT_USER_MESSAGE_MAX_CHARS,
|
||||
trading_day_reset_hour,
|
||||
)
|
||||
from lib.hub.hub_trades_lib import current_trading_day
|
||||
from hub_ai.context import (
|
||||
build_chat_context,
|
||||
format_chat_context_for_chat,
|
||||
format_chat_position_overview,
|
||||
)
|
||||
from hub_ai.prompts import (
|
||||
CHAT_GENERAL_SYSTEM,
|
||||
CHAT_SYSTEM,
|
||||
build_chat_user_prompt,
|
||||
build_general_chat_user_prompt,
|
||||
)
|
||||
from hub_ai.rolling_summary import refresh_session_rolling_summary
|
||||
from hub_ai.store import (
|
||||
CHAT_BOT_GENERAL,
|
||||
CHAT_BOT_TRADING,
|
||||
append_chat_message,
|
||||
create_new_session,
|
||||
delete_chat_session,
|
||||
ensure_active_session,
|
||||
get_active_session,
|
||||
list_chat_sessions,
|
||||
load_chat_store,
|
||||
set_active_session,
|
||||
summary_excerpt_for_chat,
|
||||
)
|
||||
from hub_ai.text_util import clip_text, is_ai_error_reply
|
||||
|
||||
|
||||
def _is_ai_error_reply(text: str) -> bool:
|
||||
return is_ai_error_reply(text)
|
||||
|
||||
|
||||
def _clip_text(text: str, max_chars: int) -> str:
|
||||
return clip_text(text, max_chars)
|
||||
|
||||
|
||||
def _history_lines(
|
||||
messages: list[dict],
|
||||
max_turns: int = CHAT_MAX_HISTORY_TURNS,
|
||||
*,
|
||||
max_chars_per_msg: int = CHAT_HISTORY_MAX_CHARS_PER_MSG,
|
||||
total_max_chars: int | None = None,
|
||||
) -> str:
|
||||
rows = [m for m in (messages or []) if m.get("role") in ("user", "assistant")]
|
||||
rows = rows[-max_turns * 2 :]
|
||||
lines = []
|
||||
for m in rows:
|
||||
role = "用户" if m.get("role") == "user" else "搭档"
|
||||
content = str(m.get("content") or "").strip()
|
||||
if m.get("role") == "assistant" and _is_ai_error_reply(content):
|
||||
continue
|
||||
att = m.get("attachments") or []
|
||||
if att:
|
||||
names = "、".join(str(a.get("name") or "附件") for a in att[:3])
|
||||
content = f"{content} [附件: {names}]".strip()
|
||||
content = _clip_text(content, max_chars_per_msg)
|
||||
if content:
|
||||
lines.append(f"{role}:{content}")
|
||||
if total_max_chars and total_max_chars > 0:
|
||||
while lines and len("\n".join(lines)) > total_max_chars:
|
||||
lines.pop(0)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _trading_context_bundle(ctx: dict[str, Any], *, prior_count: int) -> tuple[str, str]:
|
||||
day = str(ctx.get("trading_day") or (ctx.get("totals") or {}).get("trading_day") or "")
|
||||
if prior_count <= 0:
|
||||
brief = format_chat_context_for_chat(ctx, max_chars=CHAT_CONTEXT_MAX_CHARS)
|
||||
excerpt = summary_excerpt_for_chat(day, max_chars=CHAT_SUMMARY_EXCERPT_MAX_CHARS)
|
||||
return brief, excerpt
|
||||
totals = ctx.get("totals") or {}
|
||||
overview = format_chat_position_overview(ctx)
|
||||
slim = (
|
||||
f"【续聊快照 {day}】平仓盈亏 {totals.get('total_pnl_u')}U | "
|
||||
f"笔数 {totals.get('closed_count')} | "
|
||||
f"持仓 {totals.get('open_position_count', 0)} 仓 | "
|
||||
f"浮盈亏 {totals.get('float_pnl_u')}U"
|
||||
)
|
||||
brief = _clip_text(overview + "\n" + slim, CHAT_FOLLOWUP_CONTEXT_MAX_CHARS)
|
||||
return brief, ""
|
||||
|
||||
|
||||
def _history_budget(*sizes: int) -> int:
|
||||
used = sum(int(s or 0) for s in sizes) + 2200
|
||||
return max(1200, CHAT_PROMPT_MAX_CHARS - used)
|
||||
|
||||
|
||||
def _prompt_memory(session: dict, prior_msgs: list[dict]) -> tuple[str, str]:
|
||||
"""续聊优先用滚动摘要;旧会话无摘要时仅带最近 1 轮兜底。"""
|
||||
rolling = str(session.get("rolling_summary") or "").strip()
|
||||
if rolling:
|
||||
return rolling, ""
|
||||
prior_count = len([m for m in prior_msgs if m.get("role") in ("user", "assistant")])
|
||||
if prior_count <= 0:
|
||||
return "", ""
|
||||
tail = _history_lines(
|
||||
prior_msgs,
|
||||
max_turns=1,
|
||||
max_chars_per_msg=CHAT_HISTORY_MAX_CHARS_PER_MSG,
|
||||
)
|
||||
return "", tail
|
||||
|
||||
|
||||
def get_chat_state() -> dict[str, Any]:
|
||||
store = load_chat_store()
|
||||
session = get_active_session()
|
||||
if session:
|
||||
session.setdefault("bot_mode", CHAT_BOT_TRADING)
|
||||
session.setdefault("rolling_summary", "")
|
||||
return {
|
||||
"active_session_id": store.get("active_session_id"),
|
||||
"session": session,
|
||||
"sessions": list_chat_sessions(),
|
||||
"model": model_label(),
|
||||
}
|
||||
|
||||
|
||||
def start_new_chat(*, trading_day: str, bot_mode: str = CHAT_BOT_TRADING) -> dict:
|
||||
session = create_new_session(trading_day=trading_day, bot_mode=bot_mode)
|
||||
return {
|
||||
"ok": True,
|
||||
"session": session,
|
||||
"sessions": list_chat_sessions(),
|
||||
"model": model_label(),
|
||||
}
|
||||
|
||||
|
||||
def switch_chat_session(session_id: str) -> dict[str, Any]:
|
||||
session = set_active_session(session_id)
|
||||
return {
|
||||
"ok": True,
|
||||
"session": session,
|
||||
"sessions": list_chat_sessions(),
|
||||
"model": model_label(),
|
||||
}
|
||||
|
||||
|
||||
def remove_chat_session(session_id: str) -> dict[str, Any]:
|
||||
deleted, new_active = delete_chat_session(session_id)
|
||||
if not deleted:
|
||||
return {"ok": False, "msg": "session_not_found"}
|
||||
session = get_active_session()
|
||||
return {
|
||||
"ok": True,
|
||||
"active_session_id": new_active,
|
||||
"session": session,
|
||||
"sessions": list_chat_sessions(),
|
||||
"model": model_label(),
|
||||
}
|
||||
|
||||
|
||||
def send_chat_message(
|
||||
exchanges: list[dict],
|
||||
message: str,
|
||||
*,
|
||||
trading_day: str | None = None,
|
||||
raw_attachments: Optional[list[dict]] = None,
|
||||
) -> dict[str, Any]:
|
||||
text = (message or "").strip()
|
||||
parsed = parse_chat_attachments(raw_attachments or [])
|
||||
if parsed.get("errors") and not text and not parsed.get("images_b64"):
|
||||
return {"ok": False, "msg": ";".join(parsed["errors"])}
|
||||
if not text and not parsed.get("images_b64") and not parsed.get("text_append"):
|
||||
return {"ok": False, "msg": "消息不能为空"}
|
||||
|
||||
user_visible = text
|
||||
if parsed.get("text_append"):
|
||||
user_visible = (user_visible + "\n\n" + parsed["text_append"]).strip()
|
||||
if not user_visible and parsed.get("attachment_note"):
|
||||
user_visible = f"(上传了 {parsed['attachment_note']})"
|
||||
|
||||
day = (trading_day or "").strip()[:10] or current_trading_day(
|
||||
reset_hour=trading_day_reset_hour()
|
||||
)
|
||||
session = ensure_active_session(trading_day=day)
|
||||
sid = session["id"]
|
||||
prior_rolling = str(session.get("rolling_summary") or "")
|
||||
prior_msgs = session.get("messages") or []
|
||||
prior_count = len([m for m in prior_msgs if m.get("role") in ("user", "assistant")])
|
||||
user_for_prompt = _clip_text(text or user_visible, CHAT_USER_MESSAGE_MAX_CHARS)
|
||||
rolling_summary, history_tail = _prompt_memory(session, prior_msgs)
|
||||
|
||||
bot_mode = (session.get("bot_mode") or CHAT_BOT_TRADING).strip().lower()
|
||||
if bot_mode == CHAT_BOT_GENERAL:
|
||||
user_prompt = build_general_chat_user_prompt(
|
||||
rolling_summary=rolling_summary,
|
||||
history_lines=history_tail,
|
||||
user_message=user_for_prompt,
|
||||
attachment_note=str(parsed.get("attachment_note") or ""),
|
||||
)
|
||||
if parsed.get("text_append"):
|
||||
user_prompt += "\n\n【附件正文】\n" + _clip_text(parsed["text_append"], 3000)
|
||||
system_prompt = CHAT_GENERAL_SYSTEM
|
||||
else:
|
||||
ctx = build_chat_context(exchanges, trading_day=day)
|
||||
day = ctx["trading_day"]
|
||||
brief_ctx, excerpt = _trading_context_bundle(ctx, prior_count=prior_count)
|
||||
user_prompt = build_chat_user_prompt(
|
||||
context_text=brief_ctx,
|
||||
trading_day=day,
|
||||
summary_excerpt=excerpt,
|
||||
rolling_summary=rolling_summary,
|
||||
history_lines=history_tail,
|
||||
user_message=user_for_prompt,
|
||||
attachment_note=str(parsed.get("attachment_note") or ""),
|
||||
)
|
||||
if parsed.get("text_append"):
|
||||
user_prompt += "\n\n【附件正文】\n" + _clip_text(parsed["text_append"], 3000)
|
||||
system_prompt = CHAT_SYSTEM
|
||||
|
||||
reply = generate_text(
|
||||
system=system_prompt,
|
||||
user=user_prompt,
|
||||
temperature=CHAT_TEMPERATURE,
|
||||
images_b64=parsed.get("images_b64") or None,
|
||||
max_tokens=CHAT_MAX_OUTPUT_TOKENS,
|
||||
max_continuations=CHAT_MAX_CONTINUATIONS,
|
||||
)
|
||||
if _is_ai_error_reply(reply):
|
||||
return {"ok": False, "msg": reply, "session_id": sid}
|
||||
|
||||
append_chat_message(
|
||||
sid,
|
||||
"user",
|
||||
user_visible,
|
||||
attachments=parsed.get("attachment_meta") or [],
|
||||
)
|
||||
session = append_chat_message(sid, "assistant", reply)
|
||||
summary_kwargs = {
|
||||
"session_id": sid,
|
||||
"prior_summary": prior_rolling,
|
||||
"user_text": user_visible,
|
||||
"assistant_text": reply,
|
||||
"bot_mode": bot_mode,
|
||||
}
|
||||
|
||||
def _refresh_summary_bg() -> None:
|
||||
try:
|
||||
refresh_session_rolling_summary(**summary_kwargs)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
threading.Thread(target=_refresh_summary_bg, daemon=True).start()
|
||||
session = get_active_session() or session
|
||||
return {
|
||||
"ok": True,
|
||||
"trading_day": day,
|
||||
"session": session,
|
||||
"sessions": list_chat_sessions(),
|
||||
"reply": reply,
|
||||
"model": model_label(),
|
||||
"attachment_warnings": parsed.get("errors") or [],
|
||||
}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
"""中控 AI 模型调用(共用 ai_client 配置,逻辑独立)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional, Sequence
|
||||
|
||||
_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, ai_generate_chat, ai_provider_label # noqa: E402
|
||||
|
||||
|
||||
def model_label() -> str:
|
||||
return ai_provider_label()
|
||||
|
||||
|
||||
def generate_text(
|
||||
*,
|
||||
system: str,
|
||||
user: str,
|
||||
temperature: float,
|
||||
images_b64: Optional[Sequence[str]] = None,
|
||||
max_tokens: int | None = None,
|
||||
max_continuations: int = 3,
|
||||
) -> str:
|
||||
if max_tokens is not None and max_tokens > 0:
|
||||
return ai_generate_chat(
|
||||
system=system,
|
||||
user=user,
|
||||
temperature=temperature,
|
||||
images_b64=images_b64,
|
||||
max_tokens=int(max_tokens),
|
||||
max_continuations=max_continuations,
|
||||
)
|
||||
prompt = f"{system.strip()}\n\n---\n\n{user.strip()}"
|
||||
return ai_generate(
|
||||
prompt,
|
||||
temperature=temperature,
|
||||
images_b64=images_b64,
|
||||
)
|
||||
"""中控 AI 模型调用(共用 ai_client 配置,逻辑独立)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional, Sequence
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(_REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_REPO_ROOT))
|
||||
|
||||
from lib.ai.ai_client import ai_generate, ai_generate_chat, ai_provider_label # noqa: E402
|
||||
|
||||
|
||||
def model_label() -> str:
|
||||
return ai_provider_label()
|
||||
|
||||
|
||||
def generate_text(
|
||||
*,
|
||||
system: str,
|
||||
user: str,
|
||||
temperature: float,
|
||||
images_b64: Optional[Sequence[str]] = None,
|
||||
max_tokens: int | None = None,
|
||||
max_continuations: int = 3,
|
||||
) -> str:
|
||||
if max_tokens is not None and max_tokens > 0:
|
||||
return ai_generate_chat(
|
||||
system=system,
|
||||
user=user,
|
||||
temperature=temperature,
|
||||
images_b64=images_b64,
|
||||
max_tokens=int(max_tokens),
|
||||
max_continuations=max_continuations,
|
||||
)
|
||||
prompt = f"{system.strip()}\n\n---\n\n{user.strip()}"
|
||||
return ai_generate(
|
||||
prompt,
|
||||
temperature=temperature,
|
||||
images_b64=images_b64,
|
||||
)
|
||||
|
||||
+1070
-1070
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,18 @@
|
||||
"""中控 AI:分户资金快照(委托 hub_fund_history_lib,保留 180 交易日)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from hub_fund_history_lib import (
|
||||
FUND_HISTORY_DAYS,
|
||||
format_fund_history_text,
|
||||
get_fund_history,
|
||||
record_fund_snapshot,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"FUND_HISTORY_DAYS",
|
||||
"format_fund_history_text",
|
||||
"get_fund_history",
|
||||
"record_fund_snapshot",
|
||||
]
|
||||
"""中控 AI:分户资金快照(委托 hub_fund_history_lib,保留 180 交易日)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from lib.hub.hub_fund_history_lib import (
|
||||
FUND_HISTORY_DAYS,
|
||||
format_fund_history_text,
|
||||
get_fund_history,
|
||||
record_fund_snapshot,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"FUND_HISTORY_DAYS",
|
||||
"format_fund_history_text",
|
||||
"get_fund_history",
|
||||
"record_fund_snapshot",
|
||||
]
|
||||
|
||||
+200
-200
@@ -1,200 +1,200 @@
|
||||
"""中控 AI FastAPI 路由。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Callable
|
||||
|
||||
from fastapi import APIRouter, Body, File, Form, HTTPException, UploadFile
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from hub_ai.archive_quote import send_archive_quote_review
|
||||
from hub_ai.chat import (
|
||||
get_chat_state,
|
||||
remove_chat_session,
|
||||
send_chat_message,
|
||||
start_new_chat,
|
||||
switch_chat_session,
|
||||
)
|
||||
from hub_ai.client import model_label
|
||||
from hub_ai.config import trading_day_reset_hour
|
||||
from hub_ai.context import build_daily_context
|
||||
from hub_ai.store import get_latest_summary, list_summaries
|
||||
from hub_ai.supervisor import send_supervisor_chat
|
||||
from hub_ai.supervisor_store import get_supervisor_session_state
|
||||
from hub_ai.summary import generate_daily_summary
|
||||
from hub_trades_lib import current_trading_day
|
||||
from settings_store import normalize_supervisor_settings
|
||||
|
||||
|
||||
class ChatSendBody(BaseModel):
|
||||
message: str = ""
|
||||
trading_day: str = ""
|
||||
|
||||
|
||||
class SummaryGenerateBody(BaseModel):
|
||||
trading_day: str = ""
|
||||
force: bool = False
|
||||
|
||||
|
||||
class ChatNewBody(BaseModel):
|
||||
trading_day: str = ""
|
||||
bot_mode: str = "trading"
|
||||
|
||||
|
||||
class ChatSwitchBody(BaseModel):
|
||||
session_id: str = Field(..., min_length=1)
|
||||
|
||||
|
||||
class ArchiveQuoteChatBody(BaseModel):
|
||||
quote_date: str = ""
|
||||
content: str = ""
|
||||
|
||||
|
||||
class SupervisorChatBody(BaseModel):
|
||||
message: str = ""
|
||||
trading_day: str = ""
|
||||
|
||||
|
||||
def create_hub_ai_router(*, load_all_exchanges: Callable[[], list]) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/ai", tags=["hub-ai"])
|
||||
|
||||
def _day(raw: str = "") -> str:
|
||||
d = (raw or "").strip()[:10]
|
||||
return d or current_trading_day(reset_hour=trading_day_reset_hour())
|
||||
|
||||
@router.get("/meta")
|
||||
def api_ai_meta():
|
||||
return {
|
||||
"ok": True,
|
||||
"model": model_label(),
|
||||
"trading_day_reset_hour": trading_day_reset_hour(),
|
||||
"trading_day": current_trading_day(reset_hour=trading_day_reset_hour()),
|
||||
"storage": {
|
||||
"summaries": "hub_ai_summaries.json",
|
||||
"chat": "hub_ai_chat.json",
|
||||
},
|
||||
}
|
||||
|
||||
@router.get("/context")
|
||||
def api_ai_context(trading_day: str = ""):
|
||||
exchanges = load_all_exchanges()
|
||||
ctx = build_daily_context(exchanges, trading_day=_day(trading_day))
|
||||
return {"ok": True, **ctx}
|
||||
|
||||
@router.get("/summary")
|
||||
def api_ai_summary_list(trading_day: str = ""):
|
||||
day = _day(trading_day) if trading_day.strip() else ""
|
||||
items = list_summaries(trading_day=day or None, limit=20)
|
||||
latest = get_latest_summary(_day(trading_day)) if trading_day.strip() else (
|
||||
items[0] if items else None
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"trading_day": _day(trading_day) if trading_day.strip() else None,
|
||||
"summaries": items,
|
||||
"latest": latest,
|
||||
"model": model_label(),
|
||||
}
|
||||
|
||||
@router.post("/summary/generate")
|
||||
def api_ai_summary_generate(body: SummaryGenerateBody = SummaryGenerateBody()):
|
||||
exchanges = load_all_exchanges()
|
||||
result = generate_daily_summary(
|
||||
exchanges,
|
||||
trading_day=_day(body.trading_day) if body.trading_day.strip() else None,
|
||||
force=bool(body.force),
|
||||
)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(status_code=502, detail=result.get("msg") or "生成失败")
|
||||
result.pop("context", None)
|
||||
return result
|
||||
|
||||
@router.get("/chat/session")
|
||||
def api_ai_chat_session():
|
||||
state = get_chat_state()
|
||||
return {"ok": True, **state, "model": model_label()}
|
||||
|
||||
@router.post("/chat/new")
|
||||
def api_ai_chat_new(body: ChatNewBody = ChatNewBody()):
|
||||
day = _day(body.trading_day)
|
||||
return start_new_chat(trading_day=day, bot_mode=body.bot_mode or "trading")
|
||||
|
||||
@router.post("/chat/switch")
|
||||
def api_ai_chat_switch(body: ChatSwitchBody):
|
||||
try:
|
||||
return switch_chat_session(body.session_id.strip())
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
|
||||
@router.delete("/chat/session/{session_id}")
|
||||
def api_ai_chat_delete(session_id: str):
|
||||
result = remove_chat_session(session_id.strip())
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
return result
|
||||
|
||||
@router.post("/chat/archive-quote")
|
||||
def api_ai_chat_archive_quote(body: ArchiveQuoteChatBody = Body(...)):
|
||||
result = send_archive_quote_review(
|
||||
quote_date=body.quote_date,
|
||||
content=body.content,
|
||||
)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(status_code=502, detail=result.get("msg") or "发送失败")
|
||||
return result
|
||||
|
||||
@router.post("/chat/send")
|
||||
async def api_ai_chat_send(
|
||||
message: str = Form(""),
|
||||
trading_day: str = Form(""),
|
||||
files: list[UploadFile] = File(default=[]),
|
||||
):
|
||||
exchanges = load_all_exchanges()
|
||||
raw_attachments = []
|
||||
for f in files or []:
|
||||
if not f or not f.filename:
|
||||
continue
|
||||
data = await f.read()
|
||||
raw_attachments.append(
|
||||
{
|
||||
"filename": f.filename,
|
||||
"content_type": f.content_type or "",
|
||||
"data": data,
|
||||
}
|
||||
)
|
||||
result = await asyncio.to_thread(
|
||||
send_chat_message,
|
||||
exchanges,
|
||||
message,
|
||||
trading_day=_day(trading_day) if trading_day.strip() else None,
|
||||
raw_attachments=raw_attachments,
|
||||
)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(status_code=502, detail=result.get("msg") or "发送失败")
|
||||
return result
|
||||
|
||||
@router.get("/supervisor/session")
|
||||
def api_ai_supervisor_session(trading_day: str = ""):
|
||||
day = _day(trading_day)
|
||||
return get_supervisor_session_state(day)
|
||||
|
||||
@router.get("/supervisor/rules")
|
||||
def api_ai_supervisor_rules():
|
||||
from settings_store import load_settings
|
||||
|
||||
cfg = normalize_supervisor_settings(load_settings().get("supervisor"))
|
||||
return {"ok": True, "supervisor": cfg}
|
||||
|
||||
@router.post("/supervisor/chat/send")
|
||||
def api_ai_supervisor_chat_send(body: SupervisorChatBody = SupervisorChatBody()):
|
||||
exchanges = load_all_exchanges()
|
||||
result = send_supervisor_chat(
|
||||
exchanges,
|
||||
body.message,
|
||||
trading_day=_day(body.trading_day) if body.trading_day.strip() else None,
|
||||
)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(status_code=502, detail=result.get("msg") or "发送失败")
|
||||
return result
|
||||
|
||||
return router
|
||||
"""中控 AI FastAPI 路由。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Callable
|
||||
|
||||
from fastapi import APIRouter, Body, File, Form, HTTPException, UploadFile
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from hub_ai.archive_quote import send_archive_quote_review
|
||||
from hub_ai.chat import (
|
||||
get_chat_state,
|
||||
remove_chat_session,
|
||||
send_chat_message,
|
||||
start_new_chat,
|
||||
switch_chat_session,
|
||||
)
|
||||
from hub_ai.client import model_label
|
||||
from hub_ai.config import trading_day_reset_hour
|
||||
from hub_ai.context import build_daily_context
|
||||
from hub_ai.store import get_latest_summary, list_summaries
|
||||
from hub_ai.supervisor import send_supervisor_chat
|
||||
from hub_ai.supervisor_store import get_supervisor_session_state
|
||||
from hub_ai.summary import generate_daily_summary
|
||||
from lib.hub.hub_trades_lib import current_trading_day
|
||||
from settings_store import normalize_supervisor_settings
|
||||
|
||||
|
||||
class ChatSendBody(BaseModel):
|
||||
message: str = ""
|
||||
trading_day: str = ""
|
||||
|
||||
|
||||
class SummaryGenerateBody(BaseModel):
|
||||
trading_day: str = ""
|
||||
force: bool = False
|
||||
|
||||
|
||||
class ChatNewBody(BaseModel):
|
||||
trading_day: str = ""
|
||||
bot_mode: str = "trading"
|
||||
|
||||
|
||||
class ChatSwitchBody(BaseModel):
|
||||
session_id: str = Field(..., min_length=1)
|
||||
|
||||
|
||||
class ArchiveQuoteChatBody(BaseModel):
|
||||
quote_date: str = ""
|
||||
content: str = ""
|
||||
|
||||
|
||||
class SupervisorChatBody(BaseModel):
|
||||
message: str = ""
|
||||
trading_day: str = ""
|
||||
|
||||
|
||||
def create_hub_ai_router(*, load_all_exchanges: Callable[[], list]) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/ai", tags=["hub-ai"])
|
||||
|
||||
def _day(raw: str = "") -> str:
|
||||
d = (raw or "").strip()[:10]
|
||||
return d or current_trading_day(reset_hour=trading_day_reset_hour())
|
||||
|
||||
@router.get("/meta")
|
||||
def api_ai_meta():
|
||||
return {
|
||||
"ok": True,
|
||||
"model": model_label(),
|
||||
"trading_day_reset_hour": trading_day_reset_hour(),
|
||||
"trading_day": current_trading_day(reset_hour=trading_day_reset_hour()),
|
||||
"storage": {
|
||||
"summaries": "hub_ai_summaries.json",
|
||||
"chat": "hub_ai_chat.json",
|
||||
},
|
||||
}
|
||||
|
||||
@router.get("/context")
|
||||
def api_ai_context(trading_day: str = ""):
|
||||
exchanges = load_all_exchanges()
|
||||
ctx = build_daily_context(exchanges, trading_day=_day(trading_day))
|
||||
return {"ok": True, **ctx}
|
||||
|
||||
@router.get("/summary")
|
||||
def api_ai_summary_list(trading_day: str = ""):
|
||||
day = _day(trading_day) if trading_day.strip() else ""
|
||||
items = list_summaries(trading_day=day or None, limit=20)
|
||||
latest = get_latest_summary(_day(trading_day)) if trading_day.strip() else (
|
||||
items[0] if items else None
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"trading_day": _day(trading_day) if trading_day.strip() else None,
|
||||
"summaries": items,
|
||||
"latest": latest,
|
||||
"model": model_label(),
|
||||
}
|
||||
|
||||
@router.post("/summary/generate")
|
||||
def api_ai_summary_generate(body: SummaryGenerateBody = SummaryGenerateBody()):
|
||||
exchanges = load_all_exchanges()
|
||||
result = generate_daily_summary(
|
||||
exchanges,
|
||||
trading_day=_day(body.trading_day) if body.trading_day.strip() else None,
|
||||
force=bool(body.force),
|
||||
)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(status_code=502, detail=result.get("msg") or "生成失败")
|
||||
result.pop("context", None)
|
||||
return result
|
||||
|
||||
@router.get("/chat/session")
|
||||
def api_ai_chat_session():
|
||||
state = get_chat_state()
|
||||
return {"ok": True, **state, "model": model_label()}
|
||||
|
||||
@router.post("/chat/new")
|
||||
def api_ai_chat_new(body: ChatNewBody = ChatNewBody()):
|
||||
day = _day(body.trading_day)
|
||||
return start_new_chat(trading_day=day, bot_mode=body.bot_mode or "trading")
|
||||
|
||||
@router.post("/chat/switch")
|
||||
def api_ai_chat_switch(body: ChatSwitchBody):
|
||||
try:
|
||||
return switch_chat_session(body.session_id.strip())
|
||||
except KeyError:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
|
||||
@router.delete("/chat/session/{session_id}")
|
||||
def api_ai_chat_delete(session_id: str):
|
||||
result = remove_chat_session(session_id.strip())
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
return result
|
||||
|
||||
@router.post("/chat/archive-quote")
|
||||
def api_ai_chat_archive_quote(body: ArchiveQuoteChatBody = Body(...)):
|
||||
result = send_archive_quote_review(
|
||||
quote_date=body.quote_date,
|
||||
content=body.content,
|
||||
)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(status_code=502, detail=result.get("msg") or "发送失败")
|
||||
return result
|
||||
|
||||
@router.post("/chat/send")
|
||||
async def api_ai_chat_send(
|
||||
message: str = Form(""),
|
||||
trading_day: str = Form(""),
|
||||
files: list[UploadFile] = File(default=[]),
|
||||
):
|
||||
exchanges = load_all_exchanges()
|
||||
raw_attachments = []
|
||||
for f in files or []:
|
||||
if not f or not f.filename:
|
||||
continue
|
||||
data = await f.read()
|
||||
raw_attachments.append(
|
||||
{
|
||||
"filename": f.filename,
|
||||
"content_type": f.content_type or "",
|
||||
"data": data,
|
||||
}
|
||||
)
|
||||
result = await asyncio.to_thread(
|
||||
send_chat_message,
|
||||
exchanges,
|
||||
message,
|
||||
trading_day=_day(trading_day) if trading_day.strip() else None,
|
||||
raw_attachments=raw_attachments,
|
||||
)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(status_code=502, detail=result.get("msg") or "发送失败")
|
||||
return result
|
||||
|
||||
@router.get("/supervisor/session")
|
||||
def api_ai_supervisor_session(trading_day: str = ""):
|
||||
day = _day(trading_day)
|
||||
return get_supervisor_session_state(day)
|
||||
|
||||
@router.get("/supervisor/rules")
|
||||
def api_ai_supervisor_rules():
|
||||
from settings_store import load_settings
|
||||
|
||||
cfg = normalize_supervisor_settings(load_settings().get("supervisor"))
|
||||
return {"ok": True, "supervisor": cfg}
|
||||
|
||||
@router.post("/supervisor/chat/send")
|
||||
def api_ai_supervisor_chat_send(body: SupervisorChatBody = SupervisorChatBody()):
|
||||
exchanges = load_all_exchanges()
|
||||
result = send_supervisor_chat(
|
||||
exchanges,
|
||||
body.message,
|
||||
trading_day=_day(body.trading_day) if body.trading_day.strip() else None,
|
||||
)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(status_code=502, detail=result.get("msg") or "发送失败")
|
||||
return result
|
||||
|
||||
return router
|
||||
|
||||
@@ -1,125 +1,125 @@
|
||||
"""交易监管: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, 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,
|
||||
ensure_supervisor_session,
|
||||
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(
|
||||
*,
|
||||
event: dict,
|
||||
warnings: list[dict],
|
||||
trading_day: str,
|
||||
session_id: str,
|
||||
exchanges: list[dict],
|
||||
) -> str:
|
||||
ctx = build_chat_context(exchanges, trading_day=trading_day)
|
||||
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,
|
||||
)
|
||||
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]):
|
||||
def _fn(*, event: dict, warnings: list[dict], trading_day: str, session_id: str) -> str:
|
||||
return generate_supervisor_ai_reply(
|
||||
event=event,
|
||||
warnings=warnings or [],
|
||||
trading_day=trading_day,
|
||||
session_id=session_id,
|
||||
exchanges=exchanges,
|
||||
)
|
||||
|
||||
return _fn
|
||||
|
||||
|
||||
def send_supervisor_chat(
|
||||
exchanges: list[dict],
|
||||
message: str,
|
||||
*,
|
||||
trading_day: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
text = (message or "").strip()
|
||||
if not text:
|
||||
return {"ok": False, "msg": "消息不能为空"}
|
||||
day = (trading_day or "").strip()[:10] or current_trading_day(
|
||||
reset_hour=trading_day_reset_hour()
|
||||
)
|
||||
session = ensure_supervisor_session(day)
|
||||
sid = str(session.get("id") or "")
|
||||
prior = session.get("messages") or []
|
||||
ctx = build_chat_context(exchanges, trading_day=day)
|
||||
brief = format_chat_context_for_chat(ctx, max_chars=6000)
|
||||
recent = []
|
||||
for m in prior[-8:]:
|
||||
role = m.get("role")
|
||||
if role not in ("user", "assistant", "system"):
|
||||
continue
|
||||
label = {"user": "用户", "assistant": "监管", "system": "系统"}.get(role, role)
|
||||
recent.append(f"{label}:{str(m.get('content') or '').strip()}")
|
||||
user_prompt = build_supervisor_chat_prompt(
|
||||
context_text=brief,
|
||||
trading_day=day,
|
||||
history_lines="\n".join(recent),
|
||||
user_message=text,
|
||||
)
|
||||
reply = generate_text(
|
||||
system=SUPERVISOR_SYSTEM,
|
||||
user=user_prompt,
|
||||
temperature=min(0.4, CHAT_TEMPERATURE),
|
||||
max_tokens=min(768, CHAT_MAX_OUTPUT_TOKENS),
|
||||
max_continuations=1,
|
||||
)
|
||||
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)
|
||||
state = get_supervisor_session_state(day)
|
||||
return {
|
||||
"ok": True,
|
||||
"trading_day": day,
|
||||
"session": session,
|
||||
"reply": reply,
|
||||
"model": model_label(),
|
||||
"message_count": state.get("message_count"),
|
||||
"unread_system": state.get("unread_system"),
|
||||
}
|
||||
"""交易监管: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 lib.ai.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, 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,
|
||||
ensure_supervisor_session,
|
||||
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 lib.hub.hub_trades_lib import current_trading_day
|
||||
|
||||
SUPERVISOR_AI_MAX_TOKENS = 320
|
||||
|
||||
|
||||
def generate_supervisor_ai_reply(
|
||||
*,
|
||||
event: dict,
|
||||
warnings: list[dict],
|
||||
trading_day: str,
|
||||
session_id: str,
|
||||
exchanges: list[dict],
|
||||
) -> str:
|
||||
ctx = build_chat_context(exchanges, trading_day=trading_day)
|
||||
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,
|
||||
)
|
||||
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]):
|
||||
def _fn(*, event: dict, warnings: list[dict], trading_day: str, session_id: str) -> str:
|
||||
return generate_supervisor_ai_reply(
|
||||
event=event,
|
||||
warnings=warnings or [],
|
||||
trading_day=trading_day,
|
||||
session_id=session_id,
|
||||
exchanges=exchanges,
|
||||
)
|
||||
|
||||
return _fn
|
||||
|
||||
|
||||
def send_supervisor_chat(
|
||||
exchanges: list[dict],
|
||||
message: str,
|
||||
*,
|
||||
trading_day: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
text = (message or "").strip()
|
||||
if not text:
|
||||
return {"ok": False, "msg": "消息不能为空"}
|
||||
day = (trading_day or "").strip()[:10] or current_trading_day(
|
||||
reset_hour=trading_day_reset_hour()
|
||||
)
|
||||
session = ensure_supervisor_session(day)
|
||||
sid = str(session.get("id") or "")
|
||||
prior = session.get("messages") or []
|
||||
ctx = build_chat_context(exchanges, trading_day=day)
|
||||
brief = format_chat_context_for_chat(ctx, max_chars=6000)
|
||||
recent = []
|
||||
for m in prior[-8:]:
|
||||
role = m.get("role")
|
||||
if role not in ("user", "assistant", "system"):
|
||||
continue
|
||||
label = {"user": "用户", "assistant": "监管", "system": "系统"}.get(role, role)
|
||||
recent.append(f"{label}:{str(m.get('content') or '').strip()}")
|
||||
user_prompt = build_supervisor_chat_prompt(
|
||||
context_text=brief,
|
||||
trading_day=day,
|
||||
history_lines="\n".join(recent),
|
||||
user_message=text,
|
||||
)
|
||||
reply = generate_text(
|
||||
system=SUPERVISOR_SYSTEM,
|
||||
user=user_prompt,
|
||||
temperature=min(0.4, CHAT_TEMPERATURE),
|
||||
max_tokens=min(768, CHAT_MAX_OUTPUT_TOKENS),
|
||||
max_continuations=1,
|
||||
)
|
||||
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)
|
||||
state = get_supervisor_session_state(day)
|
||||
return {
|
||||
"ok": True,
|
||||
"trading_day": day,
|
||||
"session": session,
|
||||
"reply": reply,
|
||||
"model": model_label(),
|
||||
"message_count": state.get("message_count"),
|
||||
"unread_system": state.get("unread_system"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user