feat(auth): add bot name lookup and suggestion support in bot responses

Implemented a new `bot_from_name` method in `AuthService` to retrieve a bot's UUID by its name, enabling explicit bot selection via a `bot_name` query parameter in the authentication endpoint. Updated `auth_handler` to prioritize this parameter, fall back to the first active bot when necessary, and handle cases where no active bots exist.

Extended `BotOrchestrator` to import the `Suggestion` model and fetch suggestion data from Redis for each user session. Integrated these suggestions into the `BotResponse` payload, ensuring clients receive relevant suggestions alongside bot messages.

These changes improve bot selection flexibility and enrich the response data with contextual suggestions.
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-11-01 17:56:53 -03:00
parent d6fcc346fc
commit fda0b0c9e8
3 changed files with 96 additions and 9 deletions

View file

@ -144,6 +144,22 @@ impl AuthService {
Ok(user)
}
pub fn bot_from_name(
&mut self,
bot_name: &str,
) -> Result<Option<Uuid>, Box<dyn std::error::Error + Send + Sync>> {
use crate::shared::models::bots;
let bot = bots::table
.filter(bots::name.eq(bot_name))
.filter(bots::is_active.eq(true))
.select(bots::id)
.first::<Uuid>(&mut self.conn)
.optional()?;
Ok(bot)
}
}
#[actix_web::get("/api/auth")]
@ -152,6 +168,7 @@ async fn auth_handler(
data: web::Data<AppState>,
web::Query(params): web::Query<HashMap<String, String>>,
) -> Result<HttpResponse> {
let bot_name = params.get("bot_name").cloned().unwrap_or_default();
let _token = params.get("token").cloned().unwrap_or_default();
// Create or get anonymous user with proper UUID
@ -168,9 +185,41 @@ async fn auth_handler(
};
let mut db_conn = data.conn.lock().unwrap();
let (bot_id, bot_name) = match crate::bot::bot_from_url(&mut *db_conn, req.path()) {
Ok((id, name)) => (id, name),
Err(res) => return Ok(res),
// Use bot_name query parameter if provided, otherwise fallback to path-based lookup
let bot_name_param = bot_name.clone();
let (bot_id, bot_name) = {
use crate::shared::models::schema::bots::dsl::*;
use diesel::prelude::*;
use actix_web::error::ErrorInternalServerError;
// Try to find bot by the provided name
match bots
.filter(name.eq(&bot_name_param))
.filter(is_active.eq(true))
.select((id, name))
.first::<(Uuid, String)>(&mut *db_conn)
.optional()
.map_err(|e| ErrorInternalServerError(e))?
{
Some((id_val, name_val)) => (id_val, name_val),
None => {
// Fallback to first active bot if not found
match bots
.filter(is_active.eq(true))
.select((id, name))
.first::<(Uuid, String)>(&mut *db_conn)
.optional()
.map_err(|e| ErrorInternalServerError(e))?
{
Some((id_val, name_val)) => (id_val, name_val),
None => {
error!("No active bots found");
return Ok(HttpResponse::ServiceUnavailable()
.json(serde_json::json!({"error": "No bots available"})));
}
}
}
}
};
let session = {

View file

@ -3,7 +3,7 @@ use crate::context::langcache::get_langcache_client;
use crate::drive_monitor::DriveMonitor;
use crate::kb::embeddings::generate_embeddings;
use crate::kb::qdrant_client::{ensure_collection_exists, get_qdrant_client, QdrantPoint};
use crate::shared::models::{BotResponse, UserMessage, UserSession};
use crate::shared::models::{BotResponse, Suggestion, UserMessage, UserSession};
use crate::shared::state::AppState;
use actix_web::{web, HttpRequest, HttpResponse, Result};
use actix_ws::Message as WsMessage;
@ -550,6 +550,25 @@ impl BotOrchestrator {
message.user_id, message.session_id
);
// Get suggestions from Redis
let suggestions = if let Some(redis) = &self.state.redis_client {
let mut conn = redis.get_multiplexed_async_connection().await?;
let redis_key = format!("suggestions:{}:{}", message.user_id, message.session_id);
let suggestions: Vec<String> = redis::cmd("LRANGE")
.arg(&redis_key)
.arg(0)
.arg(-1)
.query_async(&mut conn)
.await?;
suggestions
.into_iter()
.filter_map(|s| serde_json::from_str::<Suggestion>(&s).ok())
.collect()
} else {
Vec::new()
};
let user_id = Uuid::parse_str(&message.user_id).map_err(|e| {
error!("Invalid user ID: {}", e);
e
@ -733,7 +752,7 @@ impl BotOrchestrator {
message_type: 1,
stream_token: None,
is_complete: false,
suggestions: Vec::new(),
suggestions: suggestions.clone(),
};
if response_tx.send(partial).await.is_err() {
@ -761,7 +780,7 @@ impl BotOrchestrator {
message_type: 1,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
suggestions,
};
response_tx.send(final_msg).await?;
@ -801,8 +820,23 @@ impl BotOrchestrator {
session.id, token
);
let bot_guid = std::env::var("BOT_GUID").unwrap_or_else(|_| String::from("default_bot"));
let start_script_path = format!("./{}.gbai/.gbdialog/start.bas", bot_guid);
use crate::shared::models::schema::bots::dsl::*;
use diesel::prelude::*;
let bot_id = session.bot_id;
let bot_name: String = {
let mut db_conn = state.conn.lock().unwrap();
bots.filter(id.eq(Uuid::parse_str(&bot_id.to_string())?))
.select(name)
.first(&mut *db_conn)
.map_err(|e| {
error!("Failed to query bot name for {}: {}", bot_id, e);
e
})?
};
let start_script_path = format!("./work/{}.gbai/{}.gbdialog/start.ast", bot_name, bot_name);
let start_script = match std::fs::read_to_string(&start_script_path) {
Ok(content) => content,

View file

@ -1093,7 +1093,11 @@
async function initializeAuth() {
try {
updateConnectionStatus("connecting");
const response = await fetch("/api/auth");
// Extract bot name from URL path (first segment after /)
const pathSegments = window.location.pathname.split('/').filter(s => s);
const botName = pathSegments.length > 0 ? pathSegments[0] : 'default';
const response = await fetch(`/api/auth?bot_name=${encodeURIComponent(botName)}`);
const authData = await response.json();
currentUserId = authData.user_id;
currentSessionId = authData.session_id;