feat(automation): increase schedule field size and improve task checking

- Increased schedule field size from bpchar(12) to bpchar(20) in database schema
- Reduced task checking interval from 60s to 5s for more responsive automation
- Improved error handling for schedule parsing and execution
- Added proper error logging for automation failures
- Changed automation execution to use bot_id instead of nil UUID
- Enhanced HEAR keyword functionality (partial diff shown)
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-11-12 15:04:04 -03:00
parent a9af37e385
commit 7de29e6189
4 changed files with 1886 additions and 1374 deletions

View file

@ -58,7 +58,7 @@ CREATE TABLE public.system_automations (
bot_id uuid NOT NULL, bot_id uuid NOT NULL,
kind int4 NOT NULL, kind int4 NOT NULL,
"target" varchar(32) NULL, "target" varchar(32) NULL,
schedule bpchar(12) NULL, schedule bpchar(20) NULL,
param varchar(32) NOT NULL, param varchar(32) NOT NULL,
is_active bool DEFAULT true NOT NULL, is_active bool DEFAULT true NOT NULL,
last_triggered timestamptz NULL, last_triggered timestamptz NULL,

View file

@ -18,7 +18,7 @@ impl AutomationService {
Self { state } Self { state }
} }
pub async fn spawn(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { pub async fn spawn(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut ticker = interval(Duration::from_secs(60)); let mut ticker = interval(Duration::from_secs(5));
loop { loop {
ticker.tick().await; ticker.tick().await;
if let Err(e) = self.check_scheduled_tasks().await { if let Err(e) = self.check_scheduled_tasks().await {
@ -41,7 +41,8 @@ impl AutomationService {
.load::<Automation>(&mut conn)?; .load::<Automation>(&mut conn)?;
for automation in automations { for automation in automations {
if let Some(schedule_str) = &automation.schedule { if let Some(schedule_str) = &automation.schedule {
if let Ok(parsed_schedule) = Schedule::from_str(schedule_str) { match Schedule::from_str(schedule_str.trim()) {
Ok(parsed_schedule) => {
let now = Utc::now(); let now = Utc::now();
let next_run = parsed_schedule.upcoming(Utc).next(); let next_run = parsed_schedule.upcoming(Utc).next();
if let Some(next_time) = next_run { if let Some(next_time) = next_run {
@ -52,11 +53,20 @@ impl AutomationService {
continue; continue;
} }
} }
self.execute_automation(&automation).await?; if let Err(e) = self.execute_automation(&automation).await {
diesel::update(system_automations.filter(id.eq(automation.id))) error!("Error executing automation {}: {}", automation.id, e);
.set(lt_column.eq(Some(now)))
.execute(&mut conn)?;
} }
if let Err(e) = diesel::update(system_automations.filter(id.eq(automation.id)))
.set(lt_column.eq(Some(now)))
.execute(&mut conn)
{
error!("Error updating last_triggered for automation {}: {}", automation.id, e);
}
}
}
}
Err(e) => {
error!("Error parsing schedule for automation {} ({}): {}", automation.id, schedule_str, e);
} }
} }
} }
@ -91,7 +101,7 @@ impl AutomationService {
}; };
let session = { let session = {
let mut sm = self.state.session_manager.lock().await; let mut sm = self.state.session_manager.lock().await;
let admin_user = uuid::Uuid::nil(); let admin_user = automation.bot_id;
sm.get_or_create_user_session(admin_user, automation.bot_id, "Automation")? sm.get_or_create_user_session(admin_user, automation.bot_id, "Automation")?
.ok_or("Failed to create session")? .ok_or("Failed to create session")?
}; };

View file

@ -8,13 +8,23 @@ pub fn hear_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine
let state_clone = Arc::clone(&state); let state_clone = Arc::clone(&state);
engine engine
.register_custom_syntax(&["HEAR", "$ident$"], true, move |_context, inputs| { .register_custom_syntax(&["HEAR", "$ident$"], true, move |_context, inputs| {
let variable_name = inputs[0].get_string_value().expect("Expected identifier as string").to_string(); let variable_name = inputs[0]
trace!("HEAR command waiting for user input to store in variable: {}", variable_name); .get_string_value()
.expect("Expected identifier as string")
.to_string();
trace!(
"HEAR command waiting for user input to store in variable: {}",
variable_name
);
let state_for_spawn = Arc::clone(&state_clone); let state_for_spawn = Arc::clone(&state_clone);
let session_id_clone = session_id; let session_id_clone = session_id;
let var_name_clone = variable_name.clone(); let var_name_clone = variable_name.clone();
tokio::spawn(async move { tokio::spawn(async move {
trace!("HEAR: Setting session {} to wait for input for variable '{}'", session_id_clone, var_name_clone); trace!(
"HEAR: Setting session {} to wait for input for variable '{}'",
session_id_clone,
var_name_clone
);
let mut session_manager = state_for_spawn.session_manager.lock().await; let mut session_manager = state_for_spawn.session_manager.lock().await;
session_manager.mark_waiting(session_id_clone); session_manager.mark_waiting(session_id_clone);
if let Some(redis_client) = &state_for_spawn.cache { if let Some(redis_client) = &state_for_spawn.cache {
@ -26,22 +36,42 @@ pub fn hear_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine
} }
}; };
let key = format!("hear:{}:{}", session_id_clone, var_name_clone); let key = format!("hear:{}:{}", session_id_clone, var_name_clone);
let _: Result<(), _> = redis::cmd("SET").arg(&key).arg("waiting").query_async(&mut conn).await; let _: Result<(), _> = redis::cmd("SET")
.arg(&key)
.arg("waiting")
.query_async(&mut conn)
.await;
} }
}); });
Err(Box::new(EvalAltResult::ErrorRuntime("Waiting for user input".into(), rhai::Position::NONE))) Err(Box::new(EvalAltResult::ErrorRuntime(
"Waiting for user input".into(),
rhai::Position::NONE,
)))
}) })
.unwrap(); .unwrap();
} }
pub async fn execute_talk(state: Arc<AppState>, user_session: UserSession, message: String) -> Result<BotResponse, Box<dyn std::error::Error>> { pub async fn execute_talk(
state: Arc<AppState>,
user_session: UserSession,
message: String,
) -> Result<BotResponse, Box<dyn std::error::Error>> {
let mut suggestions = Vec::new(); let mut suggestions = Vec::new();
if let Some(redis_client) = &state.cache { if let Some(redis_client) = &state.cache {
let mut conn: redis::aio::MultiplexedConnection = redis_client.get_multiplexed_async_connection().await?; let mut conn: redis::aio::MultiplexedConnection =
redis_client.get_multiplexed_async_connection().await?;
let redis_key = format!("suggestions:{}:{}", user_session.user_id, user_session.id); let redis_key = format!("suggestions:{}:{}", user_session.user_id, user_session.id);
let suggestions_json: Result<Vec<String>, _> = redis::cmd("LRANGE").arg(redis_key.as_str()).arg(0).arg(-1).query_async(&mut conn).await; let suggestions_json: Result<Vec<String>, _> = redis::cmd("LRANGE")
.arg(redis_key.as_str())
.arg(0)
.arg(-1)
.query_async(&mut conn)
.await;
match suggestions_json { match suggestions_json {
Ok(suggestions_json) => { Ok(suggestions_json) => {
suggestions = suggestions_json.into_iter().filter_map(|s| serde_json::from_str(&s).ok()).collect(); suggestions = suggestions_json
.into_iter()
.filter_map(|s| serde_json::from_str(&s).ok())
.collect();
} }
Err(e) => { Err(e) => {
error!("Failed to load suggestions from Redis: {}", e); error!("Failed to load suggestions from Redis: {}", e);
@ -75,7 +105,10 @@ pub async fn execute_talk(state: Arc<AppState>, user_session: UserSession, messa
} else { } else {
let web_adapter = Arc::clone(&state.web_adapter); let web_adapter = Arc::clone(&state.web_adapter);
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = web_adapter.send_message_to_session(&user_id, response_clone).await { if let Err(e) = web_adapter
.send_message_to_session(&user_id, response_clone)
.await
{
error!("Failed to send TALK message via web adapter: {}", e); error!("Failed to send TALK message via web adapter: {}", e);
} else { } else {
trace!("TALK message sent via web adapter"); trace!("TALK message sent via web adapter");

File diff suppressed because it is too large Load diff