mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-09-04 02:01:48 +03:00
fix(webui): project generated titles onto per-chat sessions under unifiedSession
With unifiedSession enabled, all turns are routed to the shared unified:default session, so title generation, persistence, and change notifications all happen on that shared session. The WebUI sidebar, however, renders per-chat websocket:<id> sessions, so generated titles never reach the session the user sees and every chat stays "Untitled". Pass the origin chat_id through maybe_generate_webui_title_after_turn and add a target_session_key to maybe_generate_webui_title so the title is written to the per-chat session while conversation content still comes from the shared (routed) session. When the routed session differs from the per-chat session, the latest user/assistant exchange is used as the title prompt since the shared session mixes content from every channel. The shared session's title metadata is no longer read or written, so stale shared titles can no longer leak into per-chat sessions.
This commit is contained in:
@@ -168,30 +168,76 @@ def _title_inputs(session: Session) -> tuple[str, str]:
|
||||
return user_text, assistant_text
|
||||
|
||||
|
||||
def _latest_title_inputs(session: Session) -> tuple[str, str]:
|
||||
"""Latest user/assistant texts, for turns executed on a shared session."""
|
||||
user_text = ""
|
||||
assistant_text = ""
|
||||
for message in reversed(session.messages):
|
||||
if message.get("_command") is True:
|
||||
continue
|
||||
if is_hidden_history_message(message):
|
||||
continue
|
||||
message = public_history_message(message)
|
||||
role = message.get("role")
|
||||
content = message.get("content")
|
||||
if not isinstance(content, str) or not content.strip():
|
||||
continue
|
||||
content = strip_think(content)
|
||||
if not content:
|
||||
continue
|
||||
if role == "user" and not user_text:
|
||||
user_text = content.strip()
|
||||
elif role == "assistant" and not assistant_text:
|
||||
assistant_text = content.strip()
|
||||
if user_text and assistant_text:
|
||||
break
|
||||
return user_text, assistant_text
|
||||
|
||||
|
||||
async def maybe_generate_webui_title(
|
||||
*,
|
||||
sessions: SessionManager,
|
||||
session_key: str,
|
||||
provider: LLMProvider,
|
||||
model: str,
|
||||
target_session_key: str | None = None,
|
||||
) -> bool:
|
||||
"""Generate and persist a short title for WebUI-owned sessions only."""
|
||||
session = sessions.get_or_create(session_key)
|
||||
if session.metadata.get(WEBUI_SESSION_METADATA_KEY) is not True:
|
||||
"""Generate and persist a short title for WebUI-owned sessions.
|
||||
|
||||
``session_key`` owns the conversation content. Under unified-session
|
||||
routing this is the shared session while WebUI renders per-chat sessions,
|
||||
so pass ``target_session_key`` to project the title onto that per-chat
|
||||
session instead of storing it on the shared one.
|
||||
"""
|
||||
routed_session = sessions.get_or_create(session_key)
|
||||
target_is_routed = target_session_key is None or target_session_key == session_key
|
||||
if target_is_routed or target_session_key is None:
|
||||
target_session = routed_session
|
||||
else:
|
||||
target_session = sessions.get_or_create(target_session_key)
|
||||
if (
|
||||
routed_session.metadata.get(WEBUI_SESSION_METADATA_KEY) is not True
|
||||
and target_session.metadata.get(WEBUI_SESSION_METADATA_KEY) is not True
|
||||
):
|
||||
return False
|
||||
if session.metadata.get(WEBUI_TITLE_USER_EDITED_METADATA_KEY) is True:
|
||||
if target_session.metadata.get(WEBUI_TITLE_USER_EDITED_METADATA_KEY) is True:
|
||||
return False
|
||||
current_title = session.metadata.get(WEBUI_TITLE_METADATA_KEY)
|
||||
current_title = target_session.metadata.get(WEBUI_TITLE_METADATA_KEY)
|
||||
if isinstance(current_title, str) and current_title.strip():
|
||||
cleaned_current_title = clean_generated_title(current_title)
|
||||
if cleaned_current_title:
|
||||
if cleaned_current_title != current_title:
|
||||
session.metadata[WEBUI_TITLE_METADATA_KEY] = cleaned_current_title
|
||||
sessions.save(session)
|
||||
target_session.metadata[WEBUI_TITLE_METADATA_KEY] = cleaned_current_title
|
||||
sessions.save(target_session)
|
||||
return False
|
||||
session.metadata.pop(WEBUI_TITLE_METADATA_KEY, None)
|
||||
target_session.metadata.pop(WEBUI_TITLE_METADATA_KEY, None)
|
||||
|
||||
user_text, assistant_text = _title_inputs(session)
|
||||
if target_is_routed:
|
||||
user_text, assistant_text = _title_inputs(routed_session)
|
||||
else:
|
||||
# Shared-session content mixes every channel; generation runs right
|
||||
# after this turn, so its exchange is the latest pair.
|
||||
user_text, assistant_text = _latest_title_inputs(routed_session)
|
||||
if not user_text:
|
||||
return False
|
||||
|
||||
@@ -240,14 +286,15 @@ async def maybe_generate_webui_title(
|
||||
response.finish_reason,
|
||||
)
|
||||
return False
|
||||
session.metadata[WEBUI_TITLE_METADATA_KEY] = title
|
||||
sessions.save(session)
|
||||
target_session.metadata[WEBUI_TITLE_METADATA_KEY] = title
|
||||
sessions.save(target_session)
|
||||
return True
|
||||
|
||||
|
||||
async def maybe_generate_webui_title_after_turn(
|
||||
*,
|
||||
channel: str,
|
||||
chat_id: str,
|
||||
metadata: dict[str, Any],
|
||||
sessions: SessionManager,
|
||||
session_key: str,
|
||||
@@ -256,11 +303,15 @@ async def maybe_generate_webui_title_after_turn(
|
||||
) -> bool:
|
||||
if channel != "websocket" or metadata.get(WEBUI_SESSION_METADATA_KEY) is not True:
|
||||
return False
|
||||
origin_session_key = f"{channel}:{chat_id}"
|
||||
return await maybe_generate_webui_title(
|
||||
sessions=sessions,
|
||||
session_key=session_key,
|
||||
provider=provider,
|
||||
model=model,
|
||||
target_session_key=(
|
||||
origin_session_key if origin_session_key != session_key else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -731,6 +782,7 @@ class WebuiTurnCoordinator:
|
||||
) -> None:
|
||||
generated = await maybe_generate_webui_title_after_turn(
|
||||
channel=event.context.channel,
|
||||
chat_id=event.context.chat_id,
|
||||
metadata=event.context.metadata,
|
||||
sessions=self.sessions,
|
||||
session_key=event.context.session_key,
|
||||
|
||||
Reference in New Issue
Block a user