From 091144854888dbeb653466c41fc7482a263f3c0b Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Sun, 26 Apr 2026 16:36:03 -0300 Subject: [PATCH] feat: Add switcher toggle functionality - Added SWITCHER_TOGGLE message type (8) for reprocessing last user message with active switchers - Backend: Handler fetches last user question from DB, mutates message in-place, injects switcher prompts into system_prompt - Backend: Switcher replays skip message_history save to avoid duplication - Frontend: toggleSwitcher() sends SWITCHER_TOGGLE when input empty, sendMessage() when text present - Frontend: Added TOOL_EXEC and SWITCHER_TOGGLE to MessageType constants - Fixed session_id shadowing bug in DB query (used session_id_for_query) - Preserves conversation history for LLM context when reprocessing with switchers --- botlib/src/message_types.rs | 6 +- botserver/src/core/bot/mod.rs | 80 +++++++++++++++++++++------ botui/ui/suite/chat/chat-state.js | 2 + botui/ui/suite/chat/chat-switchers.js | 18 ++++++ 4 files changed, 88 insertions(+), 18 deletions(-) diff --git a/botlib/src/message_types.rs b/botlib/src/message_types.rs index 84416062..61719447 100644 --- a/botlib/src/message_types.rs +++ b/botlib/src/message_types.rs @@ -17,8 +17,9 @@ impl MessageType { pub const CONTEXT_CHANGE: Self = Self(5); - pub const TOOL_EXEC: Self = Self(6); - pub const SYSTEM: Self = Self(7); + pub const TOOL_EXEC: Self = Self(6); + pub const SYSTEM: Self = Self(7); + pub const SWITCHER_TOGGLE: Self = Self(8); } impl From for MessageType { @@ -50,6 +51,7 @@ impl std::fmt::Display for MessageType { 5 => "CONTEXT_CHANGE", 6 => "TOOL_EXEC", 7 => "SYSTEM", + 8 => "SWITCHER_TOGGLE", _ => "UNKNOWN", }; write!(f, "{name}") diff --git a/botserver/src/core/bot/mod.rs b/botserver/src/core/bot/mod.rs index 24af19db..d3d93bde 100644 --- a/botserver/src/core/bot/mod.rs +++ b/botserver/src/core/bot/mod.rs @@ -362,11 +362,11 @@ impl BotOrchestrator { } - #[cfg(feature = "llm")] - pub async fn stream_response( - &self, - message: UserMessage, - response_tx: mpsc::Sender, +#[cfg(feature = "llm")] +pub async fn stream_response( + &self, + mut message: UserMessage, + response_tx: mpsc::Sender, ) -> Result<(), Box> { trace!( "Streaming response for user: {}, session: {}", @@ -374,13 +374,12 @@ impl BotOrchestrator { message.session_id ); - let user_id = Uuid::parse_str(&message.user_id)?; - let session_id = Uuid::parse_str(&message.session_id)?; - let message_content = message.content.clone(); + let user_id = Uuid::parse_str(&message.user_id)?; + let session_id = Uuid::parse_str(&message.session_id)?; - // Handle direct tool execution via TOOL_EXEC message type (invisible to user) - if message.message_type == MessageType::TOOL_EXEC { - let tool_name = message_content.trim(); +// Handle direct tool execution via TOOL_EXEC message type (invisible to user) + if message.message_type == MessageType::TOOL_EXEC { + let tool_name = message.content.trim(); if !tool_name.is_empty() { info!("tool_exec: Direct tool execution: {}", tool_name); @@ -451,7 +450,56 @@ impl BotOrchestrator { return Ok(()); } - // Legacy: Handle direct tool invocation via __TOOL__: prefix + // Handle SWITCHER_TOGGLE (type 8) - user clicked a switcher chip + // Re-process last user message with the active switchers injected into system prompt + // Mutates message in-place to avoid recursive async call + // Replays are NOT saved to message_history, so the DB always has the last original user question + // When user types a new message (e.g. "faz azul"), it IS saved and becomes the new base for switchers + let mut is_switcher_replay = false; + if message.message_type == MessageType::SWITCHER_TOGGLE { + let last_user_content: Option = { + let conn = self.state.conn.get().ok(); + let session_id_for_query = session_id; + conn.and_then(|mut db_conn| { + use crate::core::shared::models::schema::message_history::dsl::*; + message_history + .filter(session_id.eq(session_id_for_query)) + .filter(role.eq(1)) + .order(created_at.desc()) + .select(content_encrypted) + .first::(&mut db_conn) + .ok() + }) + }; + + if let Some(last_content) = last_user_content { + message.content = last_content; + message.message_type = MessageType::USER; + is_switcher_replay = true; + } else { + let empty_response = BotResponse { + bot_id: message.bot_id.clone(), + user_id: message.user_id.clone(), + session_id: message.session_id.clone(), + channel: message.channel.clone(), + content: String::new(), + message_type: MessageType::BOT_RESPONSE, + stream_token: None, + is_complete: true, + suggestions: Vec::new(), + switchers: Vec::new(), + context_name: None, + context_length: 0, + context_max_length: 0, + }; + let _ = response_tx.send(empty_response).await; + return Ok(()); + } + } + + let message_content = message.content.clone(); + + // Legacy: Handle direct tool invocation via __TOOL__: prefix if message_content.starts_with("__TOOL__:") { let tool_name = message_content.trim_start_matches("__TOOL__:").trim(); if !tool_name.is_empty() { @@ -525,10 +573,10 @@ impl BotOrchestrator { session.context_data = serde_json::Value::Object(map); } - if !message.content.trim().is_empty() { - let mut sm = state_clone.session_manager.blocking_lock(); - sm.save_message(session.id, user_id, 1, &message.content, 1)?; - } + if !message.content.trim().is_empty() && !is_switcher_replay { + let mut sm = state_clone.session_manager.blocking_lock(); + sm.save_message(session.id, user_id, 1, &message.content, 1)?; + } let context_data = { let sm = state_clone.session_manager.blocking_lock(); diff --git a/botui/ui/suite/chat/chat-state.js b/botui/ui/suite/chat/chat-state.js index a4518d56..9f2a0343 100644 --- a/botui/ui/suite/chat/chat-state.js +++ b/botui/ui/suite/chat/chat-state.js @@ -36,6 +36,8 @@ var MessageType = { CONTINUE: 3, SUGGESTION: 4, CONTEXT_CHANGE: 5, + TOOL_EXEC: 6, + SWITCHER_TOGGLE: 8, }; var EntityTypes = { diff --git a/botui/ui/suite/chat/chat-switchers.js b/botui/ui/suite/chat/chat-switchers.js index b57bb03f..ed82f3e7 100644 --- a/botui/ui/suite/chat/chat-switchers.js +++ b/botui/ui/suite/chat/chat-switchers.js @@ -19,6 +19,24 @@ function toggleSwitcher(switcherId) { ChatState.activeSwitchers.add(switcherId); } renderSwitcherChips(); + + var input = document.getElementById("messageInput"); + var inputText = input ? input.value.trim() : ""; + + if (inputText) { + window.sendMessage(inputText); + } else if (ChatState.ws && ChatState.ws.readyState === WebSocket.OPEN) { + ChatState.ws.send(JSON.stringify({ + bot_id: ChatState.currentBotId, + user_id: ChatState.currentUserId, + session_id: ChatState.currentSessionId, + channel: "web", + content: "", + message_type: MessageType.SWITCHER_TOGGLE, + active_switchers: Array.from(ChatState.activeSwitchers), + timestamp: new Date().toISOString(), + })); + } } function renderBotSwitchers(switchers) {