From 6dd316547f7ba140931b09a244f4437945700363 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Sun, 19 Apr 2026 18:43:48 +0800 Subject: [PATCH] fix(web): fix session title generation fallback and reset Bridge on config change --- agent/chat/session_service.py | 35 ++++++++++++++++++++++--- agent/memory/conversation_store.py | 13 +++++++++- bridge/agent_bridge.py | 41 ++++++++++++++++++++++++++++-- channel/web/web_channel.py | 13 ++++++++++ 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/agent/chat/session_service.py b/agent/chat/session_service.py index d2c9d2c5..86bf1535 100644 --- a/agent/chat/session_service.py +++ b/agent/chat/session_service.py @@ -12,12 +12,30 @@ from typing import Optional from common.log import logger +def _truncate_fallback_title(user_message: str, max_len: int = 30) -> str: + """Pick the first non-empty line of the user message and truncate it.""" + if not user_message: + return "New Chat" + first_line = "" + for line in user_message.splitlines(): + line = line.strip() + if line: + first_line = line + break + if not first_line: + return "New Chat" + if len(first_line) > max_len: + first_line = first_line[:max_len].rstrip() + "..." + return first_line + + def generate_session_title(user_message: str, assistant_reply: str = "") -> str: """ Generate a short session title by calling the current bot's reply_text. - Falls back to a truncated user message if the LLM call fails. + Falls back to the first line of the user message if the LLM call fails + or returns an obvious error sentinel. """ - fallback = user_message[:50].split("\n")[0].strip() or "New Chat" + fallback = _truncate_fallback_title(user_message) try: from bridge.bridge import Bridge from models.session_manager import Session @@ -36,8 +54,19 @@ def generate_session_title(user_message: str, assistant_reply: str = "") -> str: )} ] - result = bot.reply_text(session) + result = bot.reply_text(session) or {} + # When bots fail (network error, auth error, rate limit, etc.) they + # typically return completion_tokens=0 with a sentinel content like + # "请再问我一次吧" / "我现在有点累了". Treat that as failure. + completion_tokens = result.get("completion_tokens", 0) or 0 raw = (result.get("content") or "").strip() + if completion_tokens <= 0: + logger.warning( + f"[SessionService] Title generation got empty completion " + f"(completion_tokens={completion_tokens}, content='{raw[:50]}'), " + f"using fallback") + return fallback + title = re.sub(r'.*?', '', raw, flags=re.DOTALL).strip().strip('"\'') logger.info(f"[SessionService] Title generation result: '{title}' (len={len(title)})") if title and len(title) <= 50: diff --git a/agent/memory/conversation_store.py b/agent/memory/conversation_store.py index 2462e563..b4b21a74 100644 --- a/agent/memory/conversation_store.py +++ b/agent/memory/conversation_store.py @@ -139,6 +139,7 @@ def _extract_tool_results(content: Any) -> Dict[str, str]: def _group_into_display_turns( rows: List[tuple], + include_thinking: bool = True, ) -> List[Dict[str, Any]]: """ Convert raw (role, content_json, created_at) DB rows into display turns. @@ -216,6 +217,8 @@ def _group_into_display_turns( continue btype = block.get("type") if btype == "thinking": + if not include_thinking: + continue txt = block.get("thinking", "").strip() if txt: steps.append({"type": "thinking", "content": txt}) @@ -601,9 +604,17 @@ class ConversationStore: finally: conn.close() + # Honour the current enable_thinking switch when building display turns + # so that toggling it off hides previously-saved thinking blocks too. + try: + from config import conf + include_thinking = bool(conf().get("enable_thinking", False)) + except Exception: + include_thinking = False + # Strip seq for display grouping, but record max seq per visible user group plain_rows = [(role, content, created_at) for _seq, role, content, created_at in rows] - visible = _group_into_display_turns(plain_rows) + visible = _group_into_display_turns(plain_rows, include_thinking=include_thinking) # Build a mapping: find the seq of each visible user message to annotate context boundary. # Walk through rows to find visible user message seqs in order. diff --git a/bridge/agent_bridge.py b/bridge/agent_bridge.py index 8f6f6316..c72e30a7 100644 --- a/bridge/agent_bridge.py +++ b/bridge/agent_bridge.py @@ -608,18 +608,55 @@ class AgentBridge: from config import conf if not conf().get("conversation_persistence", True): return + # When deep-thinking display is disabled, strip "thinking" content + # blocks before persisting so they don't resurface on history reload. + # The in-memory message list keeps them intact for this run's + # multi-turn LLM context. + thinking_enabled = bool(conf().get("enable_thinking", False)) except Exception: - pass + thinking_enabled = False + + messages_to_store = new_messages + if not thinking_enabled: + messages_to_store = self._strip_thinking_blocks(new_messages) + try: from agent.memory import get_conversation_store get_conversation_store().append_messages( - session_id, new_messages, channel_type=channel_type + session_id, messages_to_store, channel_type=channel_type ) except Exception as e: logger.warning( f"[AgentBridge] Failed to persist messages for session={session_id}: {e}" ) + @staticmethod + def _strip_thinking_blocks(messages: list) -> list: + """Return a shallow copy of messages with assistant "thinking" blocks removed.""" + cleaned = [] + for msg in messages: + if not isinstance(msg, dict): + cleaned.append(msg) + continue + if msg.get("role") != "assistant": + cleaned.append(msg) + continue + content = msg.get("content") + if not isinstance(content, list): + cleaned.append(msg) + continue + filtered_blocks = [ + b for b in content + if not (isinstance(b, dict) and b.get("type") == "thinking") + ] + if len(filtered_blocks) == len(content): + cleaned.append(msg) + else: + new_msg = dict(msg) + new_msg["content"] = filtered_blocks + cleaned.append(new_msg) + return cleaned + def clear_session(self, session_id: str): """ Clear a specific session's agent and conversation history diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py index 637cd7f6..59102994 100644 --- a/channel/web/web_channel.py +++ b/channel/web/web_channel.py @@ -936,6 +936,19 @@ class ConfigHandler: json.dump(file_cfg, f, indent=4, ensure_ascii=False) logger.info(f"[WebChannel] Config updated: {list(applied.keys())}") + + # Reset Bridge so that bot routing reflects the new config. + # Without this, Bridge keeps its cached bot instance (e.g. LinkAIBot) + # even after the user switches bot_type / use_linkai / model in UI. + bridge_routing_keys = {"bot_type", "use_linkai", "model"} + if any(k in applied for k in bridge_routing_keys): + try: + from bridge.bridge import Bridge + Bridge().reset_bot() + logger.info("[WebChannel] Bridge bot routing reset due to config change") + except Exception as reset_err: + logger.warning(f"[WebChannel] Failed to reset bridge: {reset_err}") + return json.dumps({"status": "success", "applied": applied}, ensure_ascii=False) except Exception as e: logger.error(f"Error updating config: {e}")