mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-09-04 02:01:48 +03:00
feat(webui): add lightweight session messaging via mentions
This commit is contained in:
+112
-14
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
import time
|
||||
from collections.abc import Awaitable, Callable
|
||||
from collections.abc import Awaitable, Callable, Mapping
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
from uuid import uuid4
|
||||
|
||||
from loguru import logger
|
||||
@@ -19,6 +19,7 @@ from nanobot.bus.outbound_events import (
|
||||
GoalStateSyncEvent,
|
||||
GoalStatusEvent,
|
||||
RuntimeModelUpdatedEvent,
|
||||
SessionMessageInputEvent,
|
||||
SessionUpdatedEvent,
|
||||
TurnEndEvent,
|
||||
TurnModelUpdatedEvent,
|
||||
@@ -41,12 +42,20 @@ from nanobot.runtime_context import public_history_message
|
||||
from nanobot.session.goal_state import goal_state_ws_blob
|
||||
from nanobot.session.history_visibility import is_hidden_history_message
|
||||
from nanobot.session.manager import Session, SessionManager
|
||||
from nanobot.session.session_messages import (
|
||||
SESSION_MESSAGE_METADATA_KEY,
|
||||
session_message_inbound,
|
||||
session_message_public_metadata,
|
||||
session_reply_timeout_inbound,
|
||||
)
|
||||
from nanobot.utils.helpers import strip_think, truncate_text
|
||||
from nanobot.utils.llm_runtime import LLMRuntime
|
||||
from nanobot.webui.metadata import (
|
||||
WEBSOCKET_TURN_OWNER_METADATA_KEY,
|
||||
WEBUI_MESSAGE_SOURCE_METADATA_KEY,
|
||||
WEBUI_TURN_METADATA_KEY,
|
||||
)
|
||||
from nanobot.webui.transcript import append_session_message_input
|
||||
|
||||
WEBUI_SESSION_METADATA_KEY = "webui"
|
||||
WEBUI_TITLE_METADATA_KEY = "title"
|
||||
@@ -106,6 +115,20 @@ def mark_webui_session(session: Session, metadata: dict[str, Any]) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _session_for_webui_lifecycle(
|
||||
sessions: SessionManager,
|
||||
msg: InboundMessage,
|
||||
session_key: str,
|
||||
) -> Session | None:
|
||||
"""Resolve lifecycle state without reviving deleted internal-message targets."""
|
||||
if (
|
||||
session_message_inbound(msg) is not None
|
||||
or session_reply_timeout_inbound(msg) is not None
|
||||
):
|
||||
return sessions.get_existing(session_key)
|
||||
return sessions.get_or_create(session_key)
|
||||
|
||||
|
||||
def clean_generated_title(raw: str | None) -> str:
|
||||
text = (raw or "").strip()
|
||||
if not text:
|
||||
@@ -153,7 +176,9 @@ async def maybe_generate_webui_title(
|
||||
model: str,
|
||||
) -> bool:
|
||||
"""Generate and persist a short title for WebUI-owned sessions only."""
|
||||
session = sessions.get_or_create(session_key)
|
||||
session = sessions.get_existing(session_key)
|
||||
if session is None:
|
||||
return False
|
||||
if session.metadata.get(WEBUI_SESSION_METADATA_KEY) is not True:
|
||||
return False
|
||||
if session.metadata.get(WEBUI_TITLE_USER_EDITED_METADATA_KEY) is True:
|
||||
@@ -389,7 +414,7 @@ async def publish_turn_run_status(
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WebuiTurnRoutePolicy:
|
||||
"""Expose independently dispatched late subagent turns to WebUI sessions."""
|
||||
"""Expose independently dispatched agent turns to WebUI sessions."""
|
||||
|
||||
sessions: SessionManager
|
||||
|
||||
@@ -399,22 +424,52 @@ class WebuiTurnRoutePolicy:
|
||||
session_key: str,
|
||||
route: TurnRoute,
|
||||
) -> TurnRoute:
|
||||
"""Make an independently dispatched late subagent result visible in WebUI."""
|
||||
"""Make an independently dispatched agent turn visible in WebUI."""
|
||||
routed = route
|
||||
session_message = session_message_inbound(msg)
|
||||
reply_timeout = session_reply_timeout_inbound(msg)
|
||||
if (
|
||||
msg.channel == "system"
|
||||
and msg.sender_id == "subagent"
|
||||
and msg.metadata.get("injected_event") == "subagent_result"
|
||||
(
|
||||
(
|
||||
msg.channel == "system"
|
||||
and msg.sender_id == "subagent"
|
||||
and msg.metadata.get("injected_event") == "subagent_result"
|
||||
)
|
||||
or session_message is not None
|
||||
or reply_timeout is not None
|
||||
)
|
||||
and route.channel == "websocket"
|
||||
):
|
||||
session = self.sessions.get_or_create(session_key)
|
||||
if session.metadata.get(WEBUI_SESSION_METADATA_KEY) is True:
|
||||
if session_message is not None or reply_timeout is not None:
|
||||
persisted = self.sessions.read_session_metadata(session_key)
|
||||
raw_session_metadata = (
|
||||
persisted.get("metadata") if persisted is not None else None
|
||||
)
|
||||
session_metadata: Mapping[str, Any] = (
|
||||
cast(Mapping[str, Any], raw_session_metadata)
|
||||
if isinstance(raw_session_metadata, Mapping)
|
||||
else {}
|
||||
)
|
||||
else:
|
||||
session_metadata = self.sessions.get_or_create(session_key).metadata
|
||||
if session_metadata.get(WEBUI_SESSION_METADATA_KEY) is True:
|
||||
metadata = dict(route.metadata)
|
||||
turn_prefix = "subagent"
|
||||
if session_message is not None:
|
||||
turn_prefix = "session-message"
|
||||
elif reply_timeout is not None:
|
||||
turn_prefix = "session-reply-timeout"
|
||||
metadata.update({
|
||||
WEBUI_SESSION_METADATA_KEY: True,
|
||||
"_wants_stream": True,
|
||||
WEBUI_TURN_METADATA_KEY: f"subagent:{uuid4().hex}",
|
||||
WEBUI_TURN_METADATA_KEY: f"{turn_prefix}:{uuid4().hex}",
|
||||
})
|
||||
if session_message is not None:
|
||||
metadata[SESSION_MESSAGE_METADATA_KEY] = session_message
|
||||
metadata[WEBUI_MESSAGE_SOURCE_METADATA_KEY] = {
|
||||
"kind": "session",
|
||||
"label": f"@{session_message['source']['name']}",
|
||||
}
|
||||
routed = replace(route, metadata=metadata, publish_lifecycle=True)
|
||||
|
||||
if routed.channel == "websocket" and routed.publish_lifecycle:
|
||||
@@ -446,6 +501,40 @@ class WebuiTurnRoutePolicy:
|
||||
return routed
|
||||
|
||||
|
||||
async def project_session_message_input(
|
||||
bus: MessageBus,
|
||||
msg: InboundMessage,
|
||||
session_key: str,
|
||||
) -> None:
|
||||
"""Persist and publish an incoming session message for WebUI clients."""
|
||||
envelope = session_message_inbound(msg)
|
||||
if envelope is None or msg.channel != "websocket":
|
||||
return
|
||||
public_metadata = session_message_public_metadata(envelope)
|
||||
try:
|
||||
append_session_message_input(
|
||||
session_key,
|
||||
content=msg.content,
|
||||
created_at_ms=envelope["created_at_ms"],
|
||||
session_message=public_metadata,
|
||||
)
|
||||
except (OSError, TypeError, ValueError):
|
||||
logger.warning(
|
||||
"Failed to persist session input {}",
|
||||
envelope["message_id"],
|
||||
exc_info=True,
|
||||
)
|
||||
await bus.publish_outbound(outbound_message_for_event(
|
||||
channel="websocket",
|
||||
chat_id=str(msg.chat_id),
|
||||
event=SessionMessageInputEvent(
|
||||
content=msg.content,
|
||||
created_at_ms=envelope["created_at_ms"],
|
||||
session_message=public_metadata,
|
||||
),
|
||||
))
|
||||
|
||||
|
||||
def build_webui_fallback_model_observer(bus: MessageBus) -> FallbackModelObserver:
|
||||
"""Translate provider fallback choices into chat-scoped WebUI events."""
|
||||
|
||||
@@ -533,10 +622,17 @@ class WebuiTurnCoordinator:
|
||||
def _is_websocket_event(ctx: RuntimeEventContext) -> bool:
|
||||
return ctx.channel == "websocket"
|
||||
|
||||
def _handle_session_turn_started(self, event: SessionTurnStarted) -> None:
|
||||
async def _handle_session_turn_started(self, event: SessionTurnStarted) -> None:
|
||||
if not self._is_websocket_event(event.context):
|
||||
return
|
||||
session = self.sessions.get_or_create(event.context.session_key)
|
||||
msg = self._ctx_msg(event.context)
|
||||
session = _session_for_webui_lifecycle(
|
||||
self.sessions,
|
||||
msg,
|
||||
event.context.session_key,
|
||||
)
|
||||
if session is None:
|
||||
return
|
||||
mark_webui_session(session, event.context.metadata)
|
||||
|
||||
async def _handle_run_status_changed(self, event: TurnRunStatusChanged) -> None:
|
||||
@@ -630,7 +726,9 @@ class WebuiTurnCoordinator:
|
||||
if msg.channel != "websocket":
|
||||
return
|
||||
|
||||
session = self.sessions.get_or_create(session_key)
|
||||
session = _session_for_webui_lifecycle(self.sessions, msg, session_key)
|
||||
if session is None:
|
||||
return
|
||||
await self.bus.publish_outbound(
|
||||
outbound_message_for_event(
|
||||
channel=msg.channel,
|
||||
|
||||
Reference in New Issue
Block a user