869 lines
28 KiB
Rust
869 lines
28 KiB
Rust
//! ADD BOT keyword for multi-agent conversations
|
|
//!
|
|
//! Enables multiple bots to participate in a conversation based on triggers.
|
|
//!
|
|
//! Syntax:
|
|
//! - ADD BOT "name" WITH TRIGGER "keyword1, keyword2"
|
|
//! - ADD BOT "name" WITH TOOLS "tool1, tool2"
|
|
//! - ADD BOT "name" WITH SCHEDULE "cron_expression"
|
|
//! - REMOVE BOT "name"
|
|
//! - LIST BOTS
|
|
//! - SET BOT PRIORITY "name", priority
|
|
|
|
use crate::shared::models::UserSession;
|
|
use crate::shared::state::AppState;
|
|
use diesel::prelude::*;
|
|
use log::{info, trace};
|
|
use rhai::{Dynamic, Engine};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::fmt;
|
|
use std::sync::Arc;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub enum TriggerType {
|
|
Keyword,
|
|
Tool,
|
|
Schedule,
|
|
Event,
|
|
Always,
|
|
}
|
|
|
|
impl From<String> for TriggerType {
|
|
fn from(s: String) -> Self {
|
|
match s.to_lowercase().as_str() {
|
|
"keyword" => Self::Keyword,
|
|
"tool" => Self::Tool,
|
|
"schedule" => Self::Schedule,
|
|
"event" => Self::Event,
|
|
"always" => Self::Always,
|
|
_ => Self::Keyword,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for TriggerType {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Keyword => write!(f, "keyword"),
|
|
Self::Tool => write!(f, "tool"),
|
|
Self::Schedule => write!(f, "schedule"),
|
|
Self::Event => write!(f, "event"),
|
|
Self::Always => write!(f, "always"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct BotTrigger {
|
|
pub trigger_type: TriggerType,
|
|
pub keywords: Option<Vec<String>>,
|
|
pub tools: Option<Vec<String>>,
|
|
pub schedule: Option<String>,
|
|
pub event_name: Option<String>,
|
|
}
|
|
|
|
impl BotTrigger {
|
|
#[must_use]
|
|
pub fn from_keywords(keywords: Vec<String>) -> Self {
|
|
Self {
|
|
trigger_type: TriggerType::Keyword,
|
|
keywords: Some(keywords),
|
|
tools: None,
|
|
schedule: None,
|
|
event_name: None,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn from_tools(tools: Vec<String>) -> Self {
|
|
Self {
|
|
trigger_type: TriggerType::Tool,
|
|
keywords: None,
|
|
tools: Some(tools),
|
|
schedule: None,
|
|
event_name: None,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn from_schedule(cron: String) -> Self {
|
|
Self {
|
|
trigger_type: TriggerType::Schedule,
|
|
keywords: None,
|
|
tools: None,
|
|
schedule: Some(cron),
|
|
event_name: None,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn always() -> Self {
|
|
Self {
|
|
trigger_type: TriggerType::Always,
|
|
keywords: None,
|
|
tools: None,
|
|
schedule: None,
|
|
event_name: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SessionBot {
|
|
pub id: Uuid,
|
|
pub session_id: Uuid,
|
|
pub bot_id: Uuid,
|
|
pub bot_name: String,
|
|
pub trigger: BotTrigger,
|
|
pub priority: i32,
|
|
pub is_active: bool,
|
|
}
|
|
|
|
pub fn register_bot_keywords(state: &Arc<AppState>, user: &UserSession, engine: &mut Engine) {
|
|
if let Err(e) = add_bot_with_trigger_keyword(Arc::clone(state), user.clone(), engine) {
|
|
log::error!("Failed to register ADD BOT WITH TRIGGER keyword: {e}");
|
|
}
|
|
if let Err(e) = add_bot_with_tools_keyword(Arc::clone(state), user.clone(), engine) {
|
|
log::error!("Failed to register ADD BOT WITH TOOLS keyword: {e}");
|
|
}
|
|
if let Err(e) = add_bot_with_schedule_keyword(Arc::clone(state), user.clone(), engine) {
|
|
log::error!("Failed to register ADD BOT WITH SCHEDULE keyword: {e}");
|
|
}
|
|
if let Err(e) = remove_bot_keyword(Arc::clone(state), user.clone(), engine) {
|
|
log::error!("Failed to register REMOVE BOT keyword: {e}");
|
|
}
|
|
if let Err(e) = list_bots_keyword(Arc::clone(state), user.clone(), engine) {
|
|
log::error!("Failed to register LIST BOTS keyword: {e}");
|
|
}
|
|
if let Err(e) = set_bot_priority_keyword(Arc::clone(state), user.clone(), engine) {
|
|
log::error!("Failed to register SET BOT PRIORITY keyword: {e}");
|
|
}
|
|
if let Err(e) = delegate_to_keyword(Arc::clone(state), user.clone(), engine) {
|
|
log::error!("Failed to register DELEGATE TO keyword: {e}");
|
|
}
|
|
}
|
|
|
|
fn add_bot_with_trigger_keyword(
|
|
state: Arc<AppState>,
|
|
user: UserSession,
|
|
engine: &mut Engine,
|
|
) -> Result<(), rhai::ParseError> {
|
|
let state_clone = Arc::clone(&state);
|
|
let user_clone = user.clone();
|
|
|
|
engine.register_custom_syntax(
|
|
&["ADD", "BOT", "$expr$", "WITH", "TRIGGER", "$expr$"],
|
|
false,
|
|
move |context, inputs| {
|
|
let bot_name = context
|
|
.eval_expression_tree(&inputs[0])?
|
|
.to_string()
|
|
.trim_matches('"')
|
|
.to_string();
|
|
let trigger_str = context
|
|
.eval_expression_tree(&inputs[1])?
|
|
.to_string()
|
|
.trim_matches('"')
|
|
.to_string();
|
|
|
|
trace!(
|
|
"ADD BOT '{bot_name}' WITH TRIGGER '{trigger_str}' for session: {}",
|
|
user_clone.id
|
|
);
|
|
|
|
let keywords: Vec<String> = trigger_str
|
|
.split(',')
|
|
.map(|s| s.trim().to_lowercase())
|
|
.filter(|s| !s.is_empty())
|
|
.collect();
|
|
|
|
let trigger = BotTrigger::from_keywords(keywords);
|
|
let state_for_task = Arc::clone(&state_clone);
|
|
let session_id = user_clone.id;
|
|
let bot_id = user_clone.bot_id;
|
|
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
|
|
std::thread::spawn(move || {
|
|
let rt = match tokio::runtime::Runtime::new() {
|
|
Ok(rt) => rt,
|
|
Err(e) => {
|
|
let _ = tx.send(Err(format!("Failed to create runtime: {e}")));
|
|
return;
|
|
}
|
|
};
|
|
let result = rt.block_on(async {
|
|
add_bot_to_session(&state_for_task, session_id, bot_id, &bot_name, trigger)
|
|
.await
|
|
});
|
|
let _ = tx.send(result);
|
|
});
|
|
|
|
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
|
|
Ok(Ok(msg)) => Ok(Dynamic::from(msg)),
|
|
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
|
e.into(),
|
|
rhai::Position::NONE,
|
|
))),
|
|
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
|
"ADD BOT timed out".into(),
|
|
rhai::Position::NONE,
|
|
))),
|
|
}
|
|
},
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn add_bot_with_tools_keyword(
|
|
state: Arc<AppState>,
|
|
user: UserSession,
|
|
engine: &mut Engine,
|
|
) -> Result<(), rhai::ParseError> {
|
|
let state_clone = Arc::clone(&state);
|
|
let user_clone = user.clone();
|
|
|
|
engine.register_custom_syntax(
|
|
&["ADD", "BOT", "$expr$", "WITH", "TOOLS", "$expr$"],
|
|
false,
|
|
move |context, inputs| {
|
|
let bot_name = context
|
|
.eval_expression_tree(&inputs[0])?
|
|
.to_string()
|
|
.trim_matches('"')
|
|
.to_string();
|
|
let tools_str = context
|
|
.eval_expression_tree(&inputs[1])?
|
|
.to_string()
|
|
.trim_matches('"')
|
|
.to_string();
|
|
|
|
trace!(
|
|
"ADD BOT '{bot_name}' WITH TOOLS '{tools_str}' for session: {}",
|
|
user_clone.id
|
|
);
|
|
|
|
let tools: Vec<String> = tools_str
|
|
.split(',')
|
|
.map(|s| s.trim().to_uppercase())
|
|
.filter(|s| !s.is_empty())
|
|
.collect();
|
|
|
|
let trigger = BotTrigger::from_tools(tools);
|
|
let state_for_task = Arc::clone(&state_clone);
|
|
let session_id = user_clone.id;
|
|
let bot_id = user_clone.bot_id;
|
|
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
|
|
std::thread::spawn(move || {
|
|
let rt = match tokio::runtime::Runtime::new() {
|
|
Ok(rt) => rt,
|
|
Err(e) => {
|
|
let _ = tx.send(Err(format!("Failed to create runtime: {e}")));
|
|
return;
|
|
}
|
|
};
|
|
let result = rt.block_on(async {
|
|
add_bot_to_session(&state_for_task, session_id, bot_id, &bot_name, trigger)
|
|
.await
|
|
});
|
|
let _ = tx.send(result);
|
|
});
|
|
|
|
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
|
|
Ok(Ok(msg)) => Ok(Dynamic::from(msg)),
|
|
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
|
e.into(),
|
|
rhai::Position::NONE,
|
|
))),
|
|
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
|
"ADD BOT timed out".into(),
|
|
rhai::Position::NONE,
|
|
))),
|
|
}
|
|
},
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn add_bot_with_schedule_keyword(
|
|
state: Arc<AppState>,
|
|
user: UserSession,
|
|
engine: &mut Engine,
|
|
) -> Result<(), rhai::ParseError> {
|
|
let state_clone = Arc::clone(&state);
|
|
let user_clone = user.clone();
|
|
|
|
engine.register_custom_syntax(
|
|
&["ADD", "BOT", "$expr$", "WITH", "SCHEDULE", "$expr$"],
|
|
false,
|
|
move |context, inputs| {
|
|
let bot_name = context
|
|
.eval_expression_tree(&inputs[0])?
|
|
.to_string()
|
|
.trim_matches('"')
|
|
.to_string();
|
|
let schedule = context
|
|
.eval_expression_tree(&inputs[1])?
|
|
.to_string()
|
|
.trim_matches('"')
|
|
.to_string();
|
|
|
|
trace!(
|
|
"ADD BOT '{bot_name}' WITH SCHEDULE '{schedule}' for session: {}",
|
|
user_clone.id
|
|
);
|
|
|
|
let trigger = BotTrigger::from_schedule(schedule);
|
|
let state_for_task = Arc::clone(&state_clone);
|
|
let session_id = user_clone.id;
|
|
let bot_id = user_clone.bot_id;
|
|
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
|
|
std::thread::spawn(move || {
|
|
let rt = match tokio::runtime::Runtime::new() {
|
|
Ok(rt) => rt,
|
|
Err(e) => {
|
|
let _ = tx.send(Err(format!("Failed to create runtime: {e}")));
|
|
return;
|
|
}
|
|
};
|
|
let result = rt.block_on(async {
|
|
add_bot_to_session(&state_for_task, session_id, bot_id, &bot_name, trigger)
|
|
.await
|
|
});
|
|
let _ = tx.send(result);
|
|
});
|
|
|
|
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
|
|
Ok(Ok(msg)) => Ok(Dynamic::from(msg)),
|
|
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
|
e.into(),
|
|
rhai::Position::NONE,
|
|
))),
|
|
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
|
"ADD BOT timed out".into(),
|
|
rhai::Position::NONE,
|
|
))),
|
|
}
|
|
},
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn remove_bot_keyword(
|
|
state: Arc<AppState>,
|
|
user: UserSession,
|
|
engine: &mut Engine,
|
|
) -> Result<(), rhai::ParseError> {
|
|
let state_clone = Arc::clone(&state);
|
|
let user_clone = user.clone();
|
|
|
|
engine.register_custom_syntax(
|
|
&["REMOVE", "BOT", "$expr$"],
|
|
false,
|
|
move |context, inputs| {
|
|
let bot_name = context
|
|
.eval_expression_tree(&inputs[0])?
|
|
.to_string()
|
|
.trim_matches('"')
|
|
.to_string();
|
|
|
|
trace!("REMOVE BOT '{bot_name}' from session: {}", user_clone.id);
|
|
|
|
let state_for_task = Arc::clone(&state_clone);
|
|
let session_id = user_clone.id;
|
|
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
|
|
std::thread::spawn(move || {
|
|
let rt = match tokio::runtime::Runtime::new() {
|
|
Ok(rt) => rt,
|
|
Err(e) => {
|
|
let _ = tx.send(Err(format!("Failed to create runtime: {e}")));
|
|
return;
|
|
}
|
|
};
|
|
let result = rt.block_on(async {
|
|
remove_bot_from_session(&state_for_task, session_id, &bot_name).await
|
|
});
|
|
let _ = tx.send(result);
|
|
});
|
|
|
|
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
|
|
Ok(Ok(msg)) => Ok(Dynamic::from(msg)),
|
|
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
|
e.into(),
|
|
rhai::Position::NONE,
|
|
))),
|
|
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
|
"REMOVE BOT timed out".into(),
|
|
rhai::Position::NONE,
|
|
))),
|
|
}
|
|
},
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn list_bots_keyword(
|
|
state: Arc<AppState>,
|
|
user: UserSession,
|
|
engine: &mut Engine,
|
|
) -> Result<(), rhai::ParseError> {
|
|
let state_clone = Arc::clone(&state);
|
|
let user_clone = user.clone();
|
|
|
|
engine.register_custom_syntax(&["LIST", "BOTS"], false, move |_context, _inputs| {
|
|
trace!("LIST BOTS for session: {}", user_clone.id);
|
|
|
|
let state_for_task = Arc::clone(&state_clone);
|
|
let session_id = user_clone.id;
|
|
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
|
|
std::thread::spawn(move || {
|
|
let rt = match tokio::runtime::Runtime::new() {
|
|
Ok(rt) => rt,
|
|
Err(e) => {
|
|
let _ = tx.send(Err(format!("Failed to create runtime: {e}")));
|
|
return;
|
|
}
|
|
};
|
|
let result = rt.block_on(async { get_session_bots(&state_for_task, session_id).await });
|
|
let _ = tx.send(result);
|
|
});
|
|
|
|
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
|
|
Ok(Ok(bots)) => {
|
|
let bot_list: Vec<Dynamic> = bots
|
|
.into_iter()
|
|
.map(|b| {
|
|
let mut map = rhai::Map::new();
|
|
map.insert("name".into(), Dynamic::from(b.bot_name));
|
|
map.insert("priority".into(), Dynamic::from(b.priority));
|
|
map.insert(
|
|
"trigger_type".into(),
|
|
Dynamic::from(b.trigger.trigger_type.to_string()),
|
|
);
|
|
map.insert("is_active".into(), Dynamic::from(b.is_active));
|
|
Dynamic::from(map)
|
|
})
|
|
.collect();
|
|
Ok(Dynamic::from(bot_list))
|
|
}
|
|
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
|
e.into(),
|
|
rhai::Position::NONE,
|
|
))),
|
|
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
|
"LIST BOTS timed out".into(),
|
|
rhai::Position::NONE,
|
|
))),
|
|
}
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
fn set_bot_priority_keyword(
|
|
state: Arc<AppState>,
|
|
user: UserSession,
|
|
engine: &mut Engine,
|
|
) -> Result<(), rhai::ParseError> {
|
|
let state_clone = Arc::clone(&state);
|
|
let user_clone = user.clone();
|
|
|
|
engine.register_custom_syntax(
|
|
&["SET", "BOT", "PRIORITY", "$expr$", ",", "$expr$"],
|
|
false,
|
|
move |context, inputs| {
|
|
let bot_name = context
|
|
.eval_expression_tree(&inputs[0])?
|
|
.to_string()
|
|
.trim_matches('"')
|
|
.to_string();
|
|
#[allow(clippy::cast_possible_truncation)]
|
|
let priority = context
|
|
.eval_expression_tree(&inputs[1])?
|
|
.as_int()
|
|
.unwrap_or(0) as i32;
|
|
|
|
trace!(
|
|
"SET BOT PRIORITY '{bot_name}' to {priority} for session: {}",
|
|
user_clone.id
|
|
);
|
|
|
|
let state_for_task = Arc::clone(&state_clone);
|
|
let session_id = user_clone.id;
|
|
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
|
|
std::thread::spawn(move || {
|
|
let rt = match tokio::runtime::Runtime::new() {
|
|
Ok(rt) => rt,
|
|
Err(e) => {
|
|
let _ = tx.send(Err(format!("Failed to create runtime: {e}")));
|
|
return;
|
|
}
|
|
};
|
|
let result = rt.block_on(async {
|
|
set_bot_priority(&state_for_task, session_id, &bot_name, priority).await
|
|
});
|
|
let _ = tx.send(result);
|
|
});
|
|
|
|
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
|
|
Ok(Ok(msg)) => Ok(Dynamic::from(msg)),
|
|
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
|
e.into(),
|
|
rhai::Position::NONE,
|
|
))),
|
|
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
|
"SET BOT PRIORITY timed out".into(),
|
|
rhai::Position::NONE,
|
|
))),
|
|
}
|
|
},
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn delegate_to_keyword(
|
|
state: Arc<AppState>,
|
|
user: UserSession,
|
|
engine: &mut Engine,
|
|
) -> Result<(), rhai::ParseError> {
|
|
let state_clone = Arc::clone(&state);
|
|
let user_clone = user.clone();
|
|
|
|
engine.register_custom_syntax(
|
|
&["DELEGATE", "TO", "$expr$"],
|
|
false,
|
|
move |context, inputs| {
|
|
let bot_name = context
|
|
.eval_expression_tree(&inputs[0])?
|
|
.to_string()
|
|
.trim_matches('"')
|
|
.to_string();
|
|
|
|
trace!("DELEGATE TO '{bot_name}' for session: {}", user_clone.id);
|
|
|
|
let state_for_task = Arc::clone(&state_clone);
|
|
let session_id = user_clone.id;
|
|
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
|
|
std::thread::spawn(move || {
|
|
let rt = match tokio::runtime::Runtime::new() {
|
|
Ok(rt) => rt,
|
|
Err(e) => {
|
|
let _ = tx.send(Err(format!("Failed to create runtime: {e}")));
|
|
return;
|
|
}
|
|
};
|
|
let result = rt.block_on(async {
|
|
delegate_to_bot(&state_for_task, session_id, &bot_name).await
|
|
});
|
|
let _ = tx.send(result);
|
|
});
|
|
|
|
match rx.recv_timeout(std::time::Duration::from_secs(60)) {
|
|
Ok(Ok(response)) => Ok(Dynamic::from(response)),
|
|
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
|
e.into(),
|
|
rhai::Position::NONE,
|
|
))),
|
|
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
|
"DELEGATE TO timed out".into(),
|
|
rhai::Position::NONE,
|
|
))),
|
|
}
|
|
},
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn add_bot_to_session(
|
|
state: &AppState,
|
|
session_id: Uuid,
|
|
_parent_bot_id: Uuid,
|
|
bot_name: &str,
|
|
trigger: BotTrigger,
|
|
) -> Result<String, String> {
|
|
let mut conn = state.conn.get().map_err(|e| format!("DB error: {e}"))?;
|
|
|
|
let bot_exists: bool = diesel::sql_query(
|
|
"SELECT EXISTS(SELECT 1 FROM bots WHERE name = $1 AND is_active = true) as exists",
|
|
)
|
|
.bind::<diesel::sql_types::Text, _>(bot_name)
|
|
.get_result::<BoolResult>(&mut *conn)
|
|
.map(|r| r.exists)
|
|
.unwrap_or(false);
|
|
|
|
let bot_id: String = if bot_exists {
|
|
diesel::sql_query("SELECT id FROM bots WHERE name = $1 AND is_active = true")
|
|
.bind::<diesel::sql_types::Text, _>(bot_name)
|
|
.get_result::<UuidResult>(&mut *conn)
|
|
.map(|r| r.id)
|
|
.map_err(|e| format!("Failed to get bot ID: {e}"))?
|
|
} else {
|
|
let new_bot_id = Uuid::new_v4();
|
|
diesel::sql_query(
|
|
"INSERT INTO bots (id, name, description, is_active, created_at)
|
|
VALUES ($1, $2, $3, true, NOW())
|
|
ON CONFLICT (name) DO UPDATE SET is_active = true
|
|
RETURNING id",
|
|
)
|
|
.bind::<diesel::sql_types::Text, _>(new_bot_id.to_string())
|
|
.bind::<diesel::sql_types::Text, _>(bot_name)
|
|
.bind::<diesel::sql_types::Text, _>(format!("Bot agent: {bot_name}"))
|
|
.execute(&mut *conn)
|
|
.map_err(|e| format!("Failed to create bot: {e}"))?;
|
|
|
|
new_bot_id.to_string()
|
|
};
|
|
|
|
let trigger_json =
|
|
serde_json::to_string(&trigger).map_err(|e| format!("Failed to serialize trigger: {e}"))?;
|
|
|
|
let association_id = Uuid::new_v4();
|
|
diesel::sql_query(
|
|
"INSERT INTO session_bots (id, session_id, bot_id, bot_name, trigger_config, priority, is_active, joined_at)
|
|
VALUES ($1, $2, $3, $4, $5, 0, true, NOW())
|
|
ON CONFLICT (session_id, bot_name)
|
|
DO UPDATE SET trigger_config = $5, is_active = true, joined_at = NOW()",
|
|
)
|
|
.bind::<diesel::sql_types::Text, _>(association_id.to_string())
|
|
.bind::<diesel::sql_types::Text, _>(session_id.to_string())
|
|
.bind::<diesel::sql_types::Text, _>(bot_id)
|
|
.bind::<diesel::sql_types::Text, _>(bot_name)
|
|
.bind::<diesel::sql_types::Text, _>(&trigger_json)
|
|
.execute(&mut *conn)
|
|
.map_err(|e| format!("Failed to add bot to session: {e}"))?;
|
|
|
|
info!(
|
|
"Bot '{bot_name}' added to session {session_id} with trigger type: {:?}",
|
|
trigger.trigger_type
|
|
);
|
|
|
|
Ok(format!("Bot '{bot_name}' added to conversation"))
|
|
}
|
|
|
|
async fn remove_bot_from_session(
|
|
state: &AppState,
|
|
session_id: Uuid,
|
|
bot_name: &str,
|
|
) -> Result<String, String> {
|
|
let mut conn = state.conn.get().map_err(|e| format!("DB error: {e}"))?;
|
|
|
|
let affected = diesel::sql_query(
|
|
"UPDATE session_bots SET is_active = false WHERE session_id = $1 AND bot_name = $2",
|
|
)
|
|
.bind::<diesel::sql_types::Text, _>(session_id.to_string())
|
|
.bind::<diesel::sql_types::Text, _>(bot_name)
|
|
.execute(&mut *conn)
|
|
.map_err(|e| format!("Failed to remove bot: {e}"))?;
|
|
|
|
if affected > 0 {
|
|
info!("Bot '{bot_name}' removed from session {session_id}");
|
|
Ok(format!("Bot '{bot_name}' removed from conversation"))
|
|
} else {
|
|
Ok(format!("Bot '{bot_name}' was not in the conversation"))
|
|
}
|
|
}
|
|
|
|
async fn get_session_bots(state: &AppState, session_id: Uuid) -> Result<Vec<SessionBot>, String> {
|
|
let mut conn = state.conn.get().map_err(|e| format!("DB error: {e}"))?;
|
|
|
|
let results: Vec<SessionBotRow> = diesel::sql_query(
|
|
"SELECT id, session_id, bot_id, bot_name, trigger_config, priority, is_active
|
|
FROM session_bots
|
|
WHERE session_id = $1 AND is_active = true
|
|
ORDER BY priority DESC, joined_at ASC",
|
|
)
|
|
.bind::<diesel::sql_types::Text, _>(session_id.to_string())
|
|
.load(&mut *conn)
|
|
.map_err(|e| format!("Failed to get session bots: {e}"))?;
|
|
|
|
let bots = results
|
|
.into_iter()
|
|
.filter_map(|row| {
|
|
let trigger: BotTrigger =
|
|
serde_json::from_str(&row.trigger_config).unwrap_or_else(|_| BotTrigger::always());
|
|
Some(SessionBot {
|
|
id: Uuid::parse_str(&row.id).ok()?,
|
|
session_id: Uuid::parse_str(&row.session_id).ok()?,
|
|
bot_id: Uuid::parse_str(&row.bot_id).ok()?,
|
|
bot_name: row.bot_name,
|
|
trigger,
|
|
priority: row.priority,
|
|
is_active: row.is_active,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
Ok(bots)
|
|
}
|
|
|
|
async fn set_bot_priority(
|
|
state: &AppState,
|
|
session_id: Uuid,
|
|
bot_name: &str,
|
|
priority: i32,
|
|
) -> Result<String, String> {
|
|
let mut conn = state.conn.get().map_err(|e| format!("DB error: {e}"))?;
|
|
|
|
diesel::sql_query(
|
|
"UPDATE session_bots SET priority = $1 WHERE session_id = $2 AND bot_name = $3",
|
|
)
|
|
.bind::<diesel::sql_types::Integer, _>(priority)
|
|
.bind::<diesel::sql_types::Text, _>(session_id.to_string())
|
|
.bind::<diesel::sql_types::Text, _>(bot_name)
|
|
.execute(&mut *conn)
|
|
.map_err(|e| format!("Failed to set priority: {e}"))?;
|
|
|
|
Ok(format!("Bot '{bot_name}' priority set to {priority}"))
|
|
}
|
|
|
|
async fn delegate_to_bot(
|
|
state: &AppState,
|
|
session_id: Uuid,
|
|
bot_name: &str,
|
|
) -> Result<String, String> {
|
|
let mut conn = state.conn.get().map_err(|e| format!("DB error: {e}"))?;
|
|
|
|
let bot_config: Option<BotConfigRow> = diesel::sql_query(
|
|
"SELECT id, name, system_prompt, model_config FROM bots WHERE name = $1 AND is_active = true",
|
|
)
|
|
.bind::<diesel::sql_types::Text, _>(bot_name)
|
|
.get_result(&mut *conn)
|
|
.ok();
|
|
|
|
let Some(config) = bot_config else {
|
|
return Err(format!("Bot '{bot_name}' not found"));
|
|
};
|
|
|
|
trace!(
|
|
"Delegating to bot: id={}, name={}, has_system_prompt={}, has_model_config={}",
|
|
config.id,
|
|
config.name,
|
|
config.system_prompt.is_some(),
|
|
config.model_config.is_some()
|
|
);
|
|
|
|
diesel::sql_query("UPDATE sessions SET delegated_to = $1, delegated_at = NOW() WHERE id = $2")
|
|
.bind::<diesel::sql_types::Text, _>(&config.id)
|
|
.bind::<diesel::sql_types::Text, _>(session_id.to_string())
|
|
.execute(&mut *conn)
|
|
.map_err(|e| format!("Failed to delegate: {e}"))?;
|
|
|
|
let response = config.system_prompt.as_ref().map_or_else(
|
|
|| format!("Conversation delegated to '{}'", config.name),
|
|
|prompt| {
|
|
format!(
|
|
"Conversation delegated to '{}' (specialized: {})",
|
|
config.name,
|
|
prompt.chars().take(50).collect::<String>()
|
|
)
|
|
},
|
|
);
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn match_bot_triggers(message: &str, bots: &[SessionBot]) -> Vec<SessionBot> {
|
|
let message_lower = message.to_lowercase();
|
|
let mut matching_bots = Vec::new();
|
|
|
|
for bot in bots {
|
|
if !bot.is_active {
|
|
continue;
|
|
}
|
|
|
|
let matches = match bot.trigger.trigger_type {
|
|
TriggerType::Keyword => bot.trigger.keywords.as_ref().map_or(false, |keywords| {
|
|
keywords
|
|
.iter()
|
|
.any(|kw| message_lower.contains(&kw.to_lowercase()))
|
|
}),
|
|
TriggerType::Tool | TriggerType::Schedule | TriggerType::Event => false,
|
|
TriggerType::Always => true,
|
|
};
|
|
|
|
if matches {
|
|
matching_bots.push(bot.clone());
|
|
}
|
|
}
|
|
|
|
matching_bots.sort_by(|a, b| b.priority.cmp(&a.priority));
|
|
matching_bots
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn match_tool_triggers(tool_name: &str, bots: &[SessionBot]) -> Vec<SessionBot> {
|
|
let tool_upper = tool_name.to_uppercase();
|
|
let mut matching_bots = Vec::new();
|
|
|
|
for bot in bots {
|
|
if !bot.is_active {
|
|
continue;
|
|
}
|
|
|
|
if bot.trigger.trigger_type == TriggerType::Tool {
|
|
if let Some(tools) = &bot.trigger.tools {
|
|
if tools.iter().any(|t| t.to_uppercase() == tool_upper) {
|
|
matching_bots.push(bot.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
matching_bots.sort_by(|a, b| b.priority.cmp(&a.priority));
|
|
matching_bots
|
|
}
|
|
|
|
#[derive(QueryableByName)]
|
|
struct BoolResult {
|
|
#[diesel(sql_type = diesel::sql_types::Bool)]
|
|
exists: bool,
|
|
}
|
|
|
|
#[derive(QueryableByName)]
|
|
struct UuidResult {
|
|
#[diesel(sql_type = diesel::sql_types::Text)]
|
|
id: String,
|
|
}
|
|
|
|
#[derive(QueryableByName)]
|
|
struct SessionBotRow {
|
|
#[diesel(sql_type = diesel::sql_types::Text)]
|
|
id: String,
|
|
#[diesel(sql_type = diesel::sql_types::Text)]
|
|
session_id: String,
|
|
#[diesel(sql_type = diesel::sql_types::Text)]
|
|
bot_id: String,
|
|
#[diesel(sql_type = diesel::sql_types::Text)]
|
|
bot_name: String,
|
|
#[diesel(sql_type = diesel::sql_types::Text)]
|
|
trigger_config: String,
|
|
#[diesel(sql_type = diesel::sql_types::Integer)]
|
|
priority: i32,
|
|
#[diesel(sql_type = diesel::sql_types::Bool)]
|
|
is_active: bool,
|
|
}
|
|
|
|
#[derive(QueryableByName)]
|
|
struct BotConfigRow {
|
|
#[diesel(sql_type = diesel::sql_types::Text)]
|
|
id: String,
|
|
#[diesel(sql_type = diesel::sql_types::Text)]
|
|
name: String,
|
|
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>)]
|
|
system_prompt: Option<String>,
|
|
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>)]
|
|
model_config: Option<String>,
|
|
}
|