fix(web): fix session title generation fallback and reset Bridge on config change

This commit is contained in:
zhayujie
2026-04-19 18:43:48 +08:00
parent 54c7676a44
commit 6dd316547f
4 changed files with 96 additions and 6 deletions

View File

@@ -12,12 +12,30 @@ from typing import Optional
from common.log import logger 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: def generate_session_title(user_message: str, assistant_reply: str = "") -> str:
""" """
Generate a short session title by calling the current bot's reply_text. 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: try:
from bridge.bridge import Bridge from bridge.bridge import Bridge
from models.session_manager import Session 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() 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'<think>.*?</think>', '', raw, flags=re.DOTALL).strip().strip('"\'') title = re.sub(r'<think>.*?</think>', '', raw, flags=re.DOTALL).strip().strip('"\'')
logger.info(f"[SessionService] Title generation result: '{title}' (len={len(title)})") logger.info(f"[SessionService] Title generation result: '{title}' (len={len(title)})")
if title and len(title) <= 50: if title and len(title) <= 50:

View File

@@ -139,6 +139,7 @@ def _extract_tool_results(content: Any) -> Dict[str, str]:
def _group_into_display_turns( def _group_into_display_turns(
rows: List[tuple], rows: List[tuple],
include_thinking: bool = True,
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
""" """
Convert raw (role, content_json, created_at) DB rows into display turns. Convert raw (role, content_json, created_at) DB rows into display turns.
@@ -216,6 +217,8 @@ def _group_into_display_turns(
continue continue
btype = block.get("type") btype = block.get("type")
if btype == "thinking": if btype == "thinking":
if not include_thinking:
continue
txt = block.get("thinking", "").strip() txt = block.get("thinking", "").strip()
if txt: if txt:
steps.append({"type": "thinking", "content": txt}) steps.append({"type": "thinking", "content": txt})
@@ -601,9 +604,17 @@ class ConversationStore:
finally: finally:
conn.close() 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 # 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] 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. # 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. # Walk through rows to find visible user message seqs in order.

View File

@@ -608,18 +608,55 @@ class AgentBridge:
from config import conf from config import conf
if not conf().get("conversation_persistence", True): if not conf().get("conversation_persistence", True):
return 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: 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: try:
from agent.memory import get_conversation_store from agent.memory import get_conversation_store
get_conversation_store().append_messages( 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: except Exception as e:
logger.warning( logger.warning(
f"[AgentBridge] Failed to persist messages for session={session_id}: {e}" 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): def clear_session(self, session_id: str):
""" """
Clear a specific session's agent and conversation history Clear a specific session's agent and conversation history

View File

@@ -936,6 +936,19 @@ class ConfigHandler:
json.dump(file_cfg, f, indent=4, ensure_ascii=False) json.dump(file_cfg, f, indent=4, ensure_ascii=False)
logger.info(f"[WebChannel] Config updated: {list(applied.keys())}") 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) return json.dumps({"status": "success", "applied": applied}, ensure_ascii=False)
except Exception as e: except Exception as e:
logger.error(f"Error updating config: {e}") logger.error(f"Error updating config: {e}")