feat(hub): rolling chat summary to cap AI context and prevent mid-session failures
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,11 +3,24 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from hub_ai.chat import _is_ai_error_reply
|
||||
from hub_ai.client import generate_text, model_label
|
||||
from hub_ai.config import CHAT_MAX_CONTINUATIONS, CHAT_MAX_OUTPUT_TOKENS, CHAT_TEMPERATURE
|
||||
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, list_chat_sessions
|
||||
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
|
||||
|
||||
|
||||
@@ -43,7 +56,10 @@ def format_archive_trades_for_ai(payload: dict[str, Any]) -> str:
|
||||
if not trades:
|
||||
lines.append("(该日无交易记录)")
|
||||
return "\n".join(lines)
|
||||
for i, t in enumerate(trades, 1):
|
||||
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 "—")
|
||||
@@ -54,7 +70,7 @@ def format_archive_trades_for_ai(payload: dict[str, Any]) -> str:
|
||||
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 = str(t.get("note") or "").strip()
|
||||
note = clip_text(str(t.get("note") or "").strip(), 80)
|
||||
line = (
|
||||
f"{i}. {ex} | {sym} | {direction} | 开仓类型 {entry} | "
|
||||
f"开 {opened} | 平 {closed} | 持仓 {hold} | 结果 {result} | "
|
||||
@@ -87,13 +103,12 @@ def send_archive_quote_review(
|
||||
|
||||
archive_payload = list_daily_trades(trading_day=day, period="today")
|
||||
archive_trades_text = format_archive_trades_for_ai(archive_payload)
|
||||
|
||||
append_chat_message(sid, "user", text)
|
||||
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=text,
|
||||
user_message=user_for_prompt,
|
||||
)
|
||||
reply = generate_text(
|
||||
system=CHAT_SYSTEM,
|
||||
@@ -102,10 +117,20 @@ def send_archive_quote_review(
|
||||
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}
|
||||
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,
|
||||
|
||||
@@ -7,22 +7,30 @@ 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_daily_context, format_chat_context_for_chat
|
||||
from hub_ai.context import (
|
||||
build_daily_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,
|
||||
@@ -36,18 +44,23 @@ from hub_ai.store import (
|
||||
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:
|
||||
t = (text or "").strip()
|
||||
return t.startswith("AI 调用失败") or t.startswith("AI 生成失败")
|
||||
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 = 1500,
|
||||
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 :]
|
||||
@@ -61,17 +74,60 @@ def _history_lines(
|
||||
if att:
|
||||
names = "、".join(str(a.get("name") or "附件") for a in att[:3])
|
||||
content = f"{content} [附件: {names}]".strip()
|
||||
if len(content) > max_chars_per_msg:
|
||||
content = content[: max_chars_per_msg - 1].rstrip() + "…"
|
||||
lines.append(f"{role}:{content}")
|
||||
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,
|
||||
@@ -139,43 +195,38 @@ def send_chat_message(
|
||||
)
|
||||
session = ensure_active_session(trading_day=day)
|
||||
sid = session["id"]
|
||||
history = _history_lines(
|
||||
session.get("messages") or [],
|
||||
max_chars_per_msg=CHAT_HISTORY_MAX_CHARS_PER_MSG,
|
||||
)
|
||||
|
||||
append_chat_message(
|
||||
sid,
|
||||
"user",
|
||||
user_visible,
|
||||
attachments=parsed.get("attachment_meta") or [],
|
||||
)
|
||||
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(
|
||||
history_lines=history,
|
||||
user_message=text or user_visible,
|
||||
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" + parsed["text_append"]
|
||||
user_prompt += "\n\n【附件正文】\n" + _clip_text(parsed["text_append"], 3000)
|
||||
system_prompt = CHAT_GENERAL_SYSTEM
|
||||
else:
|
||||
ctx = build_daily_context(exchanges, trading_day=day)
|
||||
day = ctx["trading_day"]
|
||||
brief_ctx = 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)
|
||||
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,
|
||||
history_lines=history,
|
||||
user_message=text or user_visible,
|
||||
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" + parsed["text_append"]
|
||||
user_prompt += "\n\n【附件正文】\n" + _clip_text(parsed["text_append"], 3000)
|
||||
system_prompt = CHAT_SYSTEM
|
||||
|
||||
reply = generate_text(
|
||||
@@ -189,7 +240,21 @@ def send_chat_message(
|
||||
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)
|
||||
refresh_session_rolling_summary(
|
||||
sid,
|
||||
prior_summary=prior_rolling,
|
||||
user_text=user_visible,
|
||||
assistant_text=reply,
|
||||
bot_mode=bot_mode,
|
||||
)
|
||||
session = get_active_session() or session
|
||||
return {
|
||||
"ok": True,
|
||||
"trading_day": day,
|
||||
|
||||
@@ -15,12 +15,18 @@ def _int_env(key: str, default: int) -> int:
|
||||
|
||||
SUMMARY_TEMPERATURE = 0.15
|
||||
CHAT_TEMPERATURE = 0.5
|
||||
CHAT_MAX_HISTORY_TURNS = 40
|
||||
CHAT_MAX_HISTORY_TURNS = _int_env("CHAT_MAX_HISTORY_TURNS", 16)
|
||||
CHAT_MAX_OUTPUT_TOKENS = _int_env("CHAT_MAX_OUTPUT_TOKENS", 8192)
|
||||
CHAT_MAX_CONTINUATIONS = _int_env("CHAT_MAX_CONTINUATIONS", 4)
|
||||
CHAT_CONTEXT_MAX_CHARS = _int_env("CHAT_CONTEXT_MAX_CHARS", 20_000)
|
||||
CHAT_SUMMARY_EXCERPT_MAX_CHARS = _int_env("CHAT_SUMMARY_EXCERPT_MAX_CHARS", 2000)
|
||||
CHAT_HISTORY_MAX_CHARS_PER_MSG = _int_env("CHAT_HISTORY_MAX_CHARS_PER_MSG", 1500)
|
||||
CHAT_CONTEXT_MAX_CHARS = _int_env("CHAT_CONTEXT_MAX_CHARS", 12_000)
|
||||
CHAT_FOLLOWUP_CONTEXT_MAX_CHARS = _int_env("CHAT_FOLLOWUP_CONTEXT_MAX_CHARS", 4500)
|
||||
CHAT_PROMPT_MAX_CHARS = _int_env("CHAT_PROMPT_MAX_CHARS", 28_000)
|
||||
CHAT_USER_MESSAGE_MAX_CHARS = _int_env("CHAT_USER_MESSAGE_MAX_CHARS", 3500)
|
||||
CHAT_SUMMARY_EXCERPT_MAX_CHARS = _int_env("CHAT_SUMMARY_EXCERPT_MAX_CHARS", 1200)
|
||||
CHAT_HISTORY_MAX_CHARS_PER_MSG = _int_env("CHAT_HISTORY_MAX_CHARS_PER_MSG", 900)
|
||||
CHAT_ROLLING_SUMMARY_MAX_CHARS = _int_env("CHAT_ROLLING_SUMMARY_MAX_CHARS", 900)
|
||||
CHAT_ROLLING_SUMMARY_GEN_MAX_TOKENS = _int_env("CHAT_ROLLING_SUMMARY_GEN_MAX_TOKENS", 512)
|
||||
CHAT_ROLLING_SUMMARY_TEMPERATURE = 0.2
|
||||
SUMMARY_RETENTION_DAYS = 90
|
||||
CHAT_SESSION_RETENTION_DAYS = 60
|
||||
FUND_HISTORY_DAYS = 180
|
||||
|
||||
@@ -49,8 +49,8 @@ CHAT_SYSTEM = """
|
||||
- 用户口述与快照冲突时,以快照为准并口语说明「我这边看到是空仓/有N仓」。
|
||||
- 若附带「今日总结摘要」,那是较早生成的缓存,**实盘持仓以【当前多账户快照】里的「实盘持仓总览」为准**,摘要里若提到持仓可能已过时。
|
||||
- 若用户上传图片,可结合图中可见信息讨论,看不清的明确说看不清。
|
||||
- **优先接住【用户现在说】和【此前对话】**:用户聊心态、悔单、某笔操作时,先顺着这个话题回应,不要每句都复述账户资金数字。
|
||||
- **接续对话**:有【此前对话】时须接着聊,不要重复开场白;整段回复必须写完,以句号/问号/感叹号收尾,不得停在半句话;编号列表每条单独一行。
|
||||
- **优先接住【用户现在说】和【对话核心摘要】**:用户聊心态、悔单、某笔操作时,先顺着这个话题回应,不要每句都复述账户资金数字。
|
||||
- **接续对话**:有【对话核心摘要】时须接着聊,不要重复开场白;整段回复必须写完,以句号/问号/感叹号收尾,不得停在半句话;编号列表每条单独一行。
|
||||
- 快照里的盈亏/资金仅在需要核对事实时引用;用户口述与快照冲突时,以快照为准并口语说明。
|
||||
""".strip()
|
||||
|
||||
@@ -73,19 +73,60 @@ CHAT_GENERAL_SYSTEM = """
|
||||
- 用户未主动聊交易时,不要主动扯合约、仓位、盈亏、盯盘。
|
||||
- 你没有接入用户的交易账户数据;不要编造持仓、资金或监控状态。若被问到交易事实,说明这边看不到实盘,建议去中控监控区或实例页查看。
|
||||
- 若用户上传图片或文档,结合可见内容回应;看不清的直说。
|
||||
- 接续【此前对话】,不要重复开场白;回复须写完整,以句号/问号/感叹号收尾。
|
||||
- 接续【对话核心摘要】,不要重复开场白;回复须写完整,以句号/问号/感叹号收尾。
|
||||
""".strip()
|
||||
|
||||
|
||||
ROLLING_SUMMARY_TRADING_SYSTEM = """
|
||||
你是交易教练的对话记录员。把「此前摘要」与「本轮用户+教练回复」压成一条极短中文摘要。
|
||||
|
||||
要求:
|
||||
- 120~280 字,纯文本一段,不要标题、不要列表、不要寒暄。
|
||||
- 只保留:用户情绪/困扰、涉及的交易事实、教练核心建议、已达成的共识、待跟进事项。
|
||||
- 禁止编造未出现的信息;数字与账户名须来自原文。
|
||||
""".strip()
|
||||
|
||||
|
||||
ROLLING_SUMMARY_GENERAL_SYSTEM = """
|
||||
你是对话记录员。把「此前摘要」与「本轮用户+助手回复」压成一条极短中文摘要。
|
||||
|
||||
要求:
|
||||
- 100~240 字,纯文本一段,不要标题、不要列表。
|
||||
- 只保留:话题、用户诉求、助手给出的关键信息、待跟进事项。
|
||||
""".strip()
|
||||
|
||||
|
||||
def build_rolling_summary_user_prompt(
|
||||
*,
|
||||
prior_summary: str,
|
||||
user_text: str,
|
||||
assistant_text: str,
|
||||
) -> str:
|
||||
parts: list[str] = []
|
||||
if prior_summary.strip():
|
||||
parts.extend(["【此前摘要】", prior_summary.strip()])
|
||||
parts.extend([
|
||||
"【本轮用户】",
|
||||
user_text.strip() or "(空)",
|
||||
"【本轮教练/助手】",
|
||||
assistant_text.strip() or "(空)",
|
||||
"请输出更新后的对话核心摘要:",
|
||||
])
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
def build_general_chat_user_prompt(
|
||||
*,
|
||||
history_lines: str,
|
||||
rolling_summary: str = "",
|
||||
history_lines: str = "",
|
||||
user_message: str,
|
||||
attachment_note: str = "",
|
||||
) -> str:
|
||||
parts: list[str] = []
|
||||
if history_lines.strip():
|
||||
parts.extend(["【此前对话(须接续,勿重复开场)】", history_lines.strip()])
|
||||
if rolling_summary.strip():
|
||||
parts.extend(["【对话核心摘要(须接续,勿重复开场)】", rolling_summary.strip()])
|
||||
elif history_lines.strip():
|
||||
parts.extend(["【最近对话】", history_lines.strip()])
|
||||
if attachment_note.strip():
|
||||
parts.extend(["【用户附件说明】", attachment_note.strip()])
|
||||
parts.extend(["【用户现在说(优先回应这一条)】", user_message.strip()])
|
||||
@@ -97,13 +138,16 @@ def build_chat_user_prompt(
|
||||
context_text: str,
|
||||
trading_day: str,
|
||||
summary_excerpt: str,
|
||||
history_lines: str,
|
||||
rolling_summary: str = "",
|
||||
history_lines: str = "",
|
||||
user_message: str,
|
||||
attachment_note: str = "",
|
||||
) -> str:
|
||||
parts = [f"【交易日】{trading_day}"]
|
||||
if history_lines.strip():
|
||||
parts.extend(["【此前对话(须接续,勿重复开场)】", history_lines.strip()])
|
||||
if rolling_summary.strip():
|
||||
parts.extend(["【对话核心摘要(须接续,勿重复开场)】", rolling_summary.strip()])
|
||||
elif history_lines.strip():
|
||||
parts.extend(["【最近对话】", history_lines.strip()])
|
||||
parts.extend([
|
||||
"【当前多账户快照(事实参考;持仓以「实盘持仓总览」为准)】",
|
||||
context_text.strip() or "(无监控数据)",
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""聊天滚动摘要:每轮后压缩历史,续聊只带摘要 + 当前消息。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from hub_ai.text_util import clip_text, is_ai_error_reply
|
||||
from hub_ai.client import generate_text
|
||||
from hub_ai.config import (
|
||||
CHAT_ROLLING_SUMMARY_GEN_MAX_TOKENS,
|
||||
CHAT_ROLLING_SUMMARY_MAX_CHARS,
|
||||
CHAT_ROLLING_SUMMARY_TEMPERATURE,
|
||||
)
|
||||
from hub_ai.prompts import (
|
||||
ROLLING_SUMMARY_GENERAL_SYSTEM,
|
||||
ROLLING_SUMMARY_TRADING_SYSTEM,
|
||||
build_rolling_summary_user_prompt,
|
||||
)
|
||||
from hub_ai.store import CHAT_BOT_GENERAL, update_session_rolling_summary
|
||||
|
||||
|
||||
def refresh_session_rolling_summary(
|
||||
session_id: str,
|
||||
*,
|
||||
prior_summary: str,
|
||||
user_text: str,
|
||||
assistant_text: str,
|
||||
bot_mode: str,
|
||||
) -> str:
|
||||
"""合并旧摘要与本轮对话,生成新的短摘要并写入会话。"""
|
||||
user_clip = clip_text(user_text, 1200)
|
||||
assistant_clip = clip_text(assistant_text, 1800)
|
||||
if not user_clip and not assistant_clip:
|
||||
summary = clip_text(prior_summary, CHAT_ROLLING_SUMMARY_MAX_CHARS)
|
||||
update_session_rolling_summary(session_id, summary)
|
||||
return summary
|
||||
|
||||
system = (
|
||||
ROLLING_SUMMARY_GENERAL_SYSTEM
|
||||
if (bot_mode or "").strip().lower() == CHAT_BOT_GENERAL
|
||||
else ROLLING_SUMMARY_TRADING_SYSTEM
|
||||
)
|
||||
raw = generate_text(
|
||||
system=system,
|
||||
user=build_rolling_summary_user_prompt(
|
||||
prior_summary=prior_summary,
|
||||
user_text=user_clip,
|
||||
assistant_text=assistant_clip,
|
||||
),
|
||||
temperature=CHAT_ROLLING_SUMMARY_TEMPERATURE,
|
||||
max_tokens=CHAT_ROLLING_SUMMARY_GEN_MAX_TOKENS,
|
||||
max_continuations=1,
|
||||
)
|
||||
if is_ai_error_reply(raw):
|
||||
fallback = _fallback_summary(prior_summary, user_clip, assistant_clip)
|
||||
update_session_rolling_summary(session_id, fallback)
|
||||
return fallback
|
||||
|
||||
summary = clip_text(raw, CHAT_ROLLING_SUMMARY_MAX_CHARS)
|
||||
update_session_rolling_summary(session_id, summary)
|
||||
return summary
|
||||
|
||||
|
||||
def _fallback_summary(prior: str, user_text: str, assistant_text: str) -> str:
|
||||
parts: list[str] = []
|
||||
if prior.strip():
|
||||
parts.append(prior.strip())
|
||||
if user_text.strip():
|
||||
parts.append(f"用户:{clip_text(user_text, 200)}")
|
||||
if assistant_text.strip():
|
||||
parts.append(f"教练:{clip_text(assistant_text, 280)}")
|
||||
return clip_text("\n".join(parts), CHAT_ROLLING_SUMMARY_MAX_CHARS)
|
||||
@@ -165,6 +165,7 @@ def create_new_session(
|
||||
"created_at": _now_str(),
|
||||
"updated_at": _now_str(),
|
||||
"messages": [],
|
||||
"rolling_summary": "",
|
||||
}
|
||||
store.setdefault("sessions", []).append(session)
|
||||
store["active_session_id"] = session["id"]
|
||||
@@ -179,6 +180,22 @@ def ensure_active_session(*, trading_day: str) -> dict:
|
||||
return create_new_session(trading_day=trading_day)
|
||||
|
||||
|
||||
def update_session_rolling_summary(session_id: str, summary: str) -> dict:
|
||||
store = load_chat_store()
|
||||
target = None
|
||||
for s in store.get("sessions") or []:
|
||||
if str(s.get("id")) == str(session_id):
|
||||
target = s
|
||||
break
|
||||
if not target:
|
||||
raise KeyError("session_not_found")
|
||||
target["rolling_summary"] = str(summary or "").strip()
|
||||
target["updated_at"] = _now_str()
|
||||
store["active_session_id"] = target["id"]
|
||||
save_chat_store(store)
|
||||
return target
|
||||
|
||||
|
||||
def append_chat_message(
|
||||
session_id: str,
|
||||
role: str,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
"""中控 AI 文本小工具。"""
|
||||
|
||||
|
||||
def is_ai_error_reply(text: str) -> bool:
|
||||
t = (text or "").strip()
|
||||
return t.startswith("AI 调用失败") or t.startswith("AI 生成失败")
|
||||
|
||||
|
||||
def clip_text(text: str, max_chars: int) -> str:
|
||||
s = str(text or "").strip()
|
||||
limit = max(200, int(max_chars or 0))
|
||||
if len(s) <= limit:
|
||||
return s
|
||||
return s[: limit - 1].rstrip() + "…"
|
||||
@@ -3498,8 +3498,10 @@
|
||||
}
|
||||
|
||||
const ARCHIVE_QUOTE_AI_KEY = "hub_archive_quote_ai";
|
||||
let archiveQuoteAiPending = false;
|
||||
|
||||
async function consumeArchiveQuoteAiPending() {
|
||||
if (archiveQuoteAiPending || aiChatLoading) return;
|
||||
let raw = "";
|
||||
try {
|
||||
raw = sessionStorage.getItem(ARCHIVE_QUOTE_AI_KEY) || "";
|
||||
@@ -3526,6 +3528,7 @@
|
||||
applyAiMobileTab("trading");
|
||||
}
|
||||
|
||||
archiveQuoteAiPending = true;
|
||||
setAiChatBusy(true);
|
||||
renderAiChatMessages(aiChatSessionCache, {
|
||||
pendingUser: content,
|
||||
@@ -3547,12 +3550,14 @@
|
||||
showToast("复盘语录已发送给交易教练");
|
||||
} catch (e) {
|
||||
showToast(String(e), true);
|
||||
if (input) input.value = content;
|
||||
try {
|
||||
await loadAiChatSession();
|
||||
} catch (_) {
|
||||
renderAiChatMessages(aiChatSessionCache);
|
||||
}
|
||||
} finally {
|
||||
archiveQuoteAiPending = false;
|
||||
setAiChatBusy(false);
|
||||
}
|
||||
}
|
||||
@@ -3604,6 +3609,7 @@
|
||||
const files = fileInput && fileInput.files ? Array.from(fileInput.files) : [];
|
||||
if (!text && !files.length) return;
|
||||
const pendingAttachments = files.map((f) => ({ name: f.name, kind: f.type.startsWith("image/") ? "image" : "text" }));
|
||||
const savedText = text;
|
||||
if (input) input.value = "";
|
||||
setAiChatBusy(true);
|
||||
renderAiChatMessages(aiChatSessionCache, {
|
||||
@@ -3629,6 +3635,7 @@
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(String(e), true);
|
||||
if (input && savedText) input.value = savedText;
|
||||
try {
|
||||
await loadAiChatSession();
|
||||
} catch (_) {
|
||||
|
||||
@@ -594,6 +594,6 @@
|
||||
<script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>
|
||||
<script src="/assets/ai_review_render.js?v=3"></script>
|
||||
<script src="/assets/time_close_ui.js?v=2"></script>
|
||||
<script src="/assets/app.js?v=20260612-archive-ai-chat"></script>
|
||||
<script src="/assets/app.js?v=20260612-ai-chat-budget"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user