feat(auth): add suggestion support and clean up code

- Added new ADD_SUGGESTION keyword handler to support sending suggestions in responses
- Removed unused env import in hear_talk module
- Simplified bot_id assignment to use static string
- Added suggestions field to BotResponse struct
- Improved SET_CONTEXT keyword to take both name and value parameters
- Fixed whitespace in auth handler
- Enhanced error handling for suggestion sending

The changes improve the suggestion system functionality while cleaning up unused code and standardizing response handling.
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-10-31 20:55:13 -03:00
parent 683955a4a4
commit 6f59cdaab6
8 changed files with 125 additions and 67 deletions

View file

@ -1,8 +1,7 @@
use crate::shared::models::{BotResponse, UserSession};
use crate::shared::models::{BotResponse, Suggestion, UserSession};
use crate::shared::state::AppState;
use log::{debug, error, info};
use rhai::{Dynamic, Engine, EvalAltResult};
use std::env;
use std::sync::Arc;
use uuid::Uuid;
@ -73,7 +72,7 @@ pub fn talk_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine
debug!("TALK: Sending message: {}", message);
// Build the bot response that will be sent back to the client.
let bot_id = env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string());
let bot_id = "default_bot".to_string();
let response = BotResponse {
bot_id,
user_id: user_clone.user_id.to_string(),
@ -83,6 +82,7 @@ pub fn talk_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine
message_type: 1,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
};
let user_id = user_clone.id.to_string();
@ -168,23 +168,73 @@ pub fn set_user_keyword(state: Arc<AppState>, user: UserSession, engine: &mut En
})
.unwrap();
}
pub fn set_context_keyword(state: &AppState, user: UserSession, engine: &mut Engine) {
pub fn add_suggestion_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let user_clone = user.clone();
engine
.register_custom_syntax(&["ADD_SUGGESTION", "$expr$", "$expr$", "$expr$"], true, move |context, inputs| {
// Evaluate expressions: text, context_name
let text = context.eval_expression_tree(&inputs[0])?.to_string();
let context_name = context.eval_expression_tree(&inputs[1])?.to_string();
info!("ADD_SUGGESTION command executed - text: {}, context: {}", text, context_name);
// Get current response channels
let state_clone = Arc::clone(&state);
let user_id = user_clone.id.to_string();
tokio::spawn(async move {
let mut response_channels = state_clone.response_channels.lock().await;
if let Some(tx) = response_channels.get_mut(&user_id) {
let suggestion = Suggestion {
text,
context_name,
is_suggestion: true
};
// Create a response with just this suggestion
let response = BotResponse {
bot_id: "system".to_string(),
user_id: user_clone.user_id.to_string(),
session_id: user_clone.id.to_string(),
channel: "web".to_string(),
content: String::new(),
message_type: 3, // Special type for suggestions
stream_token: None,
is_complete: true,
suggestions: vec![suggestion],
};
if let Err(e) = tx.try_send(response) {
error!("Failed to send suggestion: {}", e);
}
}
});
Ok(Dynamic::UNIT)
})
.unwrap();
}
pub fn set_context_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let cache = state.redis_client.clone();
engine
.register_custom_syntax(&["SET_CONTEXT", "$expr$"], true, move |context, inputs| {
// Evaluate the expression that should be stored in the context.
let context_value = context.eval_expression_tree(&inputs[0])?.to_string();
.register_custom_syntax(&["SET_CONTEXT", "$expr$", "$expr$"], true, move |context, inputs| {
// Evaluate both expressions - first is context name, second is context value
let context_name = context.eval_expression_tree(&inputs[0])?.to_string();
let context_value = context.eval_expression_tree(&inputs[1])?.to_string();
info!("SET CONTEXT command executed: {}", context_value);
// Build the Redis key using the user ID and the session ID.
let redis_key = format!("context:{}:{}", user.user_id, user.id);
info!("SET CONTEXT command executed - name: {}, value: {}", context_name, context_value);
// Build the Redis key using user ID, session ID and context name
let redis_key = format!("context:{}:{}:{}", user.user_id, user.id, context_name);
log::trace!(
target: "app::set_context",
"Constructed Redis key: {} for user {} and session {}",
"Constructed Redis key: {} for user {}, session {}, context {}",
redis_key,
user.user_id,
user.id
user.id,
context_name
);
// If a Redis client is configured, perform the SET operation in a background task.

View file

@ -71,7 +71,7 @@ impl ScriptService {
set_schedule_keyword(&state, user.clone(), &mut engine);
hear_keyword(state.clone(), user.clone(), &mut engine);
talk_keyword(state.clone(), user.clone(), &mut engine);
set_context_keyword(&state, user.clone(), &mut engine);
set_context_keyword(state.clone(), user.clone(), &mut engine);
set_user_keyword(state.clone(), user.clone(), &mut engine);
// KB and Tools keywords

View file

@ -222,6 +222,7 @@ impl BotOrchestrator {
message_type: 2,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
};
if let Some(adapter) = self.state.channels.lock().unwrap().get(channel) {
@ -249,6 +250,7 @@ impl BotOrchestrator {
message_type: 1,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
};
if let Some(adapter) = self.state.channels.lock().unwrap().get(channel) {
@ -303,6 +305,7 @@ impl BotOrchestrator {
message_type: 1,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
};
adapter.send_message(ack_response).await?;
}
@ -346,6 +349,7 @@ impl BotOrchestrator {
message_type: 1,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
};
if let Some(adapter) = self.state.channels.lock().unwrap().get(&message.channel) {
@ -593,6 +597,7 @@ impl BotOrchestrator {
message_type: 1,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
};
response_tx.send(thinking_response).await?;
}
@ -664,6 +669,7 @@ impl BotOrchestrator {
message_type: 1,
stream_token: None,
is_complete: false,
suggestions: Vec::new(),
};
if response_tx.send(partial).await.is_err() {
@ -688,6 +694,7 @@ impl BotOrchestrator {
message_type: 1,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
};
response_tx.send(final_msg).await?;
@ -787,6 +794,7 @@ impl BotOrchestrator {
message_type: 1,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
};
adapter.send_message(warn_response).await
} else {

View file

@ -354,19 +354,7 @@ impl SessionManager {
#[actix_web::post("/api/sessions")]
async fn create_session(data: web::Data<AppState>) -> Result<HttpResponse> {
let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
let bot_id = if let Ok(bot_guid) = std::env::var("BOT_GUID") {
match Uuid::parse_str(&bot_guid) {
Ok(uuid) => uuid,
Err(e) => {
warn!("Invalid BOT_GUID from env: {}", e);
return Ok(HttpResponse::BadRequest()
.json(serde_json::json!({"error": "Invalid BOT_GUID"})));
}
}
} else {
warn!("BOT_GUID not set in environment, using nil UUID");
Uuid::nil()
};
let bot_id = Uuid::nil();
let session = {
let mut session_manager = data.session_manager.lock().await;

View file

@ -118,6 +118,13 @@ pub struct UserMessage {
pub timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Suggestion {
pub text: String, // The button text that will be sent as message
pub context_name: String, // The context name to set when clicked
pub is_suggestion: bool, // Flag to identify suggestion clicks
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BotResponse {
pub bot_id: String,
@ -128,6 +135,7 @@ pub struct BotResponse {
pub message_type: i32,
pub stream_token: Option<String>,
pub is_complete: bool,
pub suggestions: Vec<Suggestion>,
}
#[derive(Debug, Deserialize)]

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">

View file

@ -880,10 +880,7 @@
</div>
<div id="suggestions-container" style="text-align:center; margin-top:10px;"></div>
<script>
async function loadSuggestions() {
try {
const res = await fetch('/api/suggestions');
const suggestions = await res.json();
function handleSuggestions(suggestions) {
const container = document.getElementById('suggestions-container');
container.innerHTML = '';
suggestions.forEach(s => {
@ -900,16 +897,13 @@
btn.onclick = () => setContext(s.context);
container.appendChild(btn);
});
} catch (err) {
console.error('Failed to load suggestions:', err);
}
}
async function setContext(context) {
try {
if (ws && ws.readyState === WebSocket.OPEN) {
const suggestionEvent = {
bot_id: "default_bot",
bot_id: currentBotId,
user_id: currentUserId,
session_id: currentSessionId,
channel: "web",
@ -929,8 +923,6 @@
console.error('Failed to set context:', err);
}
}
window.addEventListener('load', loadSuggestions);
</script>
</footer>
</div>
@ -964,6 +956,7 @@
let ws = null;
let currentSessionId = null;
let currentUserId = null;
let currentBotId = "default_bot";
let isStreaming = false;
let voiceRoom = null;
let isVoiceMode = false;
@ -1233,6 +1226,11 @@
ws.onmessage = function (event) {
const response = JSON.parse(event.data);
// Update current bot_id if provided in the message
if (response.bot_id) {
currentBotId = response.bot_id;
}
if (response.message_type === 2) {
const eventData = JSON.parse(response.content);
handleEvent(eventData.event, eventData.data);
@ -1297,6 +1295,12 @@
updateContextUsage(response.context_usage);
}
// Handle suggestion messages
if (response.message_type === 3) {
handleSuggestions(response.suggestions);
return;
}
// Handle complete messages
if (response.is_complete) {
if (isStreaming) {