generalbots/src/basic/mod.rs
Rodrigo Rodriguez (Pragmatismo) 06c75cb690 Looking at this diff, I can see it's a comprehensive documentation
update and code refactoring focused on:

1. Adding new documentation pages to the table of contents
2. Restructuring the bot templates documentation
3. Changing keyword syntax from underscore format to space format (e.g.,
   `SET_BOT_MEMORY` → `SET BOT MEMORY`)
4. Updating compiler and keyword registration to support the new
   space-based syntax
5. Adding new keyword modules (social media, lead scoring, templates,
   etc.)

Refactor BASIC keywords to use spaces instead of underscores

Change keyword syntax from underscore format (SET_BOT_MEMORY) to more
natural space-separated format (SET BOT MEMORY) throughout the codebase.

Key changes:
- Update Rhai custom syntax registration to use space tokens
- Simplify compiler preprocessing (fewer replacements needed)
- Update all template .bas files to use new syntax
- Expand documentation with consolidated examples and new sections
- Add new keyword modules: social_media, lead_scoring, send_template,
  core_functions, qrcode, sms, procedures, import_export, llm_macros,
  on_form_submit
2025-11-30 10:53:59 -03:00

427 lines
16 KiB
Rust

use crate::basic::keywords::add_suggestion::clear_suggestions_keyword;
use crate::basic::keywords::set_user::set_user_keyword;
use crate::basic::keywords::string_functions::register_string_functions;
use crate::basic::keywords::switch_case::switch_keyword;
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::info;
use rhai::{Dynamic, Engine, EvalAltResult};
use std::sync::Arc;
pub mod compiler;
pub mod keywords;
use self::keywords::add_member::add_member_keyword;
use self::keywords::add_suggestion::add_suggestion_keyword;
use self::keywords::book::book_keyword;
use self::keywords::bot_memory::{get_bot_memory_keyword, set_bot_memory_keyword};
use self::keywords::clear_kb::register_clear_kb_keyword;
use self::keywords::clear_tools::clear_tools_keyword;
use self::keywords::core_functions::register_core_functions;
use self::keywords::create_draft::create_draft_keyword;
use self::keywords::create_site::create_site_keyword;
use self::keywords::create_task::create_task_keyword;
use self::keywords::data_operations::register_data_operations;
use self::keywords::file_operations::register_file_operations;
use self::keywords::find::find_keyword;
use self::keywords::first::first_keyword;
use self::keywords::for_next::for_keyword;
use self::keywords::format::format_keyword;
use self::keywords::get::get_keyword;
use self::keywords::hear_talk::{hear_keyword, talk_keyword};
use self::keywords::http_operations::register_http_operations;
use self::keywords::last::last_keyword;
use self::keywords::lead_scoring::register_lead_scoring_keywords;
use self::keywords::multimodal::register_multimodal_keywords;
use self::keywords::on_form_submit::on_form_submit_keyword;
use self::keywords::remember::remember_keyword;
use self::keywords::save_from_unstructured::save_from_unstructured_keyword;
use self::keywords::send_mail::send_mail_keyword;
use self::keywords::send_template::register_send_template_keywords;
use self::keywords::social_media::register_social_media_keywords;
use self::keywords::switch_case::preprocess_switch;
use self::keywords::use_kb::register_use_kb_keyword;
use self::keywords::use_tool::use_tool_keyword;
use self::keywords::use_website::{clear_websites_keyword, use_website_keyword};
use self::keywords::webhook::webhook_keyword;
use self::keywords::llm_keyword::llm_keyword;
use self::keywords::on::on_keyword;
use self::keywords::print::print_keyword;
use self::keywords::set::set_keyword;
use self::keywords::set_context::set_context_keyword;
use self::keywords::wait::wait_keyword;
#[derive(Debug)]
pub struct ScriptService {
pub engine: Engine,
}
impl ScriptService {
#[must_use]
pub fn new(state: Arc<AppState>, user: UserSession) -> Self {
let mut engine = Engine::new();
engine.set_allow_anonymous_fn(true);
engine.set_allow_looping(true);
// Core keywords
create_draft_keyword(&state, user.clone(), &mut engine);
set_bot_memory_keyword(state.clone(), user.clone(), &mut engine);
get_bot_memory_keyword(state.clone(), user.clone(), &mut engine);
create_site_keyword(&state, user.clone(), &mut engine);
find_keyword(&state, user.clone(), &mut engine);
for_keyword(&state, user.clone(), &mut engine);
let _ = register_use_kb_keyword(&mut engine, state.clone(), Arc::new(user.clone()));
let _ = register_clear_kb_keyword(&mut engine, state.clone(), Arc::new(user.clone()));
first_keyword(&mut engine);
last_keyword(&mut engine);
format_keyword(&mut engine);
llm_keyword(state.clone(), user.clone(), &mut engine);
get_keyword(state.clone(), user.clone(), &mut engine);
set_keyword(&state, user.clone(), &mut engine);
wait_keyword(&state, user.clone(), &mut engine);
print_keyword(&state, user.clone(), &mut engine);
on_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.clone(), user.clone(), &mut engine);
set_user_keyword(state.clone(), user.clone(), &mut engine);
clear_suggestions_keyword(state.clone(), user.clone(), &mut engine);
use_tool_keyword(state.clone(), user.clone(), &mut engine);
clear_tools_keyword(state.clone(), user.clone(), &mut engine);
use_website_keyword(state.clone(), user.clone(), &mut engine);
clear_websites_keyword(state.clone(), user.clone(), &mut engine);
add_suggestion_keyword(state.clone(), user.clone(), &mut engine);
// Register the 6 new power keywords
remember_keyword(state.clone(), user.clone(), &mut engine);
book_keyword(state.clone(), user.clone(), &mut engine);
send_mail_keyword(state.clone(), user.clone(), &mut engine);
save_from_unstructured_keyword(state.clone(), user.clone(), &mut engine);
create_task_keyword(state.clone(), user.clone(), &mut engine);
add_member_keyword(state.clone(), user.clone(), &mut engine);
// Register universal messaging keywords
keywords::universal_messaging::register_universal_messaging(
state.clone(),
user.clone(),
&mut engine,
);
// Register multimodal keywords (IMAGE, VIDEO, AUDIO, SEE)
// These connect to botmodels for image/video/audio generation and vision/captioning
register_multimodal_keywords(state.clone(), user.clone(), &mut engine);
// Register string functions (INSTR, IS_NUMERIC, LEN, LEFT, RIGHT, MID, etc.)
register_string_functions(state.clone(), user.clone(), &mut engine);
// Register SWITCH/CASE helper functions
switch_keyword(&state, user.clone(), &mut engine);
// ========================================================================
// NEW KEYWORDS for office.gbai - Compete with n8n
// ========================================================================
// HTTP Operations: POST, PUT, PATCH, DELETE_HTTP, SET_HEADER, GRAPHQL, SOAP
register_http_operations(state.clone(), user.clone(), &mut engine);
// Data Operations: SAVE, INSERT, UPDATE, DELETE, MERGE, FILL, MAP, FILTER,
// AGGREGATE, JOIN, PIVOT, GROUP_BY
register_data_operations(state.clone(), user.clone(), &mut engine);
// File Operations: READ, WRITE, DELETE_FILE, COPY, MOVE, LIST,
// COMPRESS, EXTRACT, UPLOAD, DOWNLOAD, GENERATE_PDF, MERGE_PDF
register_file_operations(state.clone(), user.clone(), &mut engine);
// Webhook keyword for event-driven automation
webhook_keyword(&state, user.clone(), &mut engine);
// ========================================================================
// NEW KEYWORDS: Social Media, Marketing, CRM
// ========================================================================
// Social Media: POST TO (Instagram, Facebook, LinkedIn, Twitter)
// GET METRICS, scheduled posting
register_social_media_keywords(state.clone(), user.clone(), &mut engine);
// SEND TEMPLATE: Multi-channel templated messaging (email, WhatsApp, SMS)
register_send_template_keywords(state.clone(), user.clone(), &mut engine);
// ON FORM SUBMIT: Webhook-based form handling for landing pages
on_form_submit_keyword(&state, user.clone(), &mut engine);
// Lead Scoring: SCORE LEAD, GET LEAD SCORE, QUALIFY LEAD, AI SCORE LEAD
register_lead_scoring_keywords(state.clone(), user.clone(), &mut engine);
// ========================================================================
// CORE BASIC FUNCTIONS: Math, Date/Time, Validation, Arrays, Error Handling
// ========================================================================
// Math: ABS, ROUND, INT, MAX, MIN, MOD, RANDOM, SGN, SQR, LOG, EXP, SIN, COS, TAN
// Date/Time: NOW, TODAY, YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DATEADD, DATEDIFF
// Validation: VAL, STR, ISNULL, ISEMPTY, ISDATE, TYPEOF
// Arrays: ARRAY, UBOUND, SORT, UNIQUE, CONTAINS, PUSH, POP, REVERSE, SLICE
// Error Handling: THROW, ERROR, IS_ERROR, ASSERT
register_core_functions(state.clone(), user, &mut engine);
ScriptService { engine }
}
fn preprocess_basic_script(&self, script: &str) -> String {
// First, preprocess SWITCH/CASE blocks
let script = preprocess_switch(script);
let mut result = String::new();
let mut for_stack: Vec<usize> = Vec::new();
let mut current_indent = 0;
for line in script.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("'") {
continue;
}
if trimmed.starts_with("FOR EACH") {
for_stack.push(current_indent);
result.push_str(&" ".repeat(current_indent));
result.push_str(trimmed);
result.push_str("{\n");
current_indent += 4;
result.push_str(&" ".repeat(current_indent));
result.push('\n');
continue;
}
if trimmed.starts_with("NEXT") {
if let Some(expected_indent) = for_stack.pop() {
if (current_indent - 4) != expected_indent {
panic!("NEXT without matching FOR EACH");
}
current_indent = current_indent - 4;
result.push_str(&" ".repeat(current_indent));
result.push_str("}\n");
result.push_str(&" ".repeat(current_indent));
result.push_str(trimmed);
result.push(';');
result.push('\n');
continue;
}
panic!("NEXT without matching FOR EACH");
}
if trimmed == "EXIT FOR" {
result.push_str(&" ".repeat(current_indent));
result.push_str(trimmed);
result.push('\n');
continue;
}
result.push_str(&" ".repeat(current_indent));
let basic_commands = [
// Core commands
"SET",
"CREATE",
"PRINT",
"FOR",
"FIND",
"GET",
"EXIT",
"IF",
"THEN",
"ELSE",
"END IF",
"WHILE",
"WEND",
"DO",
"LOOP",
"HEAR",
"TALK",
"SET CONTEXT",
"SET USER",
"GET BOT MEMORY",
"SET BOT MEMORY",
"IMAGE",
"VIDEO",
"AUDIO",
"SEE",
"SEND FILE",
"SWITCH",
"CASE",
"DEFAULT",
"END SWITCH",
"USE KB",
"CLEAR KB",
"USE TOOL",
"CLEAR TOOLS",
"ADD SUGGESTION",
"CLEAR SUGGESTIONS",
"INSTR",
"IS_NUMERIC",
"IS NUMERIC",
// HTTP Operations
"POST",
"PUT",
"PATCH",
"DELETE_HTTP",
"SET_HEADER",
"CLEAR_HEADERS",
"GRAPHQL",
"SOAP",
// Data Operations
"SAVE",
"INSERT",
"UPDATE",
"DELETE",
"MERGE",
"FILL",
"MAP",
"FILTER",
"AGGREGATE",
"JOIN",
"PIVOT",
"GROUP_BY",
// File Operations
"READ",
"WRITE",
"DELETE_FILE",
"COPY",
"MOVE",
"LIST",
"COMPRESS",
"EXTRACT",
"UPLOAD",
"DOWNLOAD",
"GENERATE_PDF",
"MERGE_PDF",
// Webhook
"WEBHOOK",
// Social Media
"POST TO",
"POST TO INSTAGRAM",
"POST TO FACEBOOK",
"POST TO LINKEDIN",
"POST TO TWITTER",
"GET INSTAGRAM METRICS",
"GET FACEBOOK METRICS",
"GET LINKEDIN METRICS",
"GET TWITTER METRICS",
"DELETE POST",
// Template & Messaging
"SEND TEMPLATE",
"CREATE TEMPLATE",
"GET TEMPLATE",
// Form Handling
"ON FORM SUBMIT",
// Lead Scoring
"SCORE LEAD",
"GET LEAD SCORE",
"QUALIFY LEAD",
"UPDATE LEAD SCORE",
"AI SCORE LEAD",
// Math Functions
"ABS",
"ROUND",
"INT",
"FIX",
"FLOOR",
"CEIL",
"MAX",
"MIN",
"MOD",
"RANDOM",
"RND",
"SGN",
"SQR",
"SQRT",
"LOG",
"EXP",
"POW",
"SIN",
"COS",
"TAN",
"SUM",
"AVG",
// Date/Time Functions
"NOW",
"TODAY",
"DATE",
"TIME",
"YEAR",
"MONTH",
"DAY",
"HOUR",
"MINUTE",
"SECOND",
"WEEKDAY",
"DATEADD",
"DATEDIFF",
"FORMAT_DATE",
"ISDATE",
// Validation Functions
"VAL",
"STR",
"CINT",
"CDBL",
"CSTR",
"ISNULL",
"ISEMPTY",
"TYPEOF",
"ISARRAY",
"ISOBJECT",
"ISSTRING",
"ISNUMBER",
"NVL",
"IIF",
// Array Functions
"ARRAY",
"UBOUND",
"LBOUND",
"COUNT",
"SORT",
"UNIQUE",
"CONTAINS",
"INDEX_OF",
"PUSH",
"POP",
"SHIFT",
"REVERSE",
"SLICE",
"SPLIT",
"CONCAT",
"FLATTEN",
"RANGE",
// Error Handling
"THROW",
"ERROR",
"IS_ERROR",
"ASSERT",
"LOG_ERROR",
"LOG_WARN",
"LOG_INFO",
];
let is_basic_command = basic_commands.iter().any(|&cmd| trimmed.starts_with(cmd));
let is_control_flow = trimmed.starts_with("IF")
|| trimmed.starts_with("ELSE")
|| trimmed.starts_with("END IF");
if is_basic_command || !for_stack.is_empty() || is_control_flow {
result.push_str(trimmed);
result.push(';');
} else {
result.push_str(trimmed);
if !trimmed.ends_with(';') && !trimmed.ends_with('{') && !trimmed.ends_with('}') {
result.push(';');
}
}
result.push('\n');
}
if !for_stack.is_empty() {
panic!("Unclosed FOR EACH loop");
}
result
}
pub fn compile(&self, script: &str) -> Result<rhai::AST, Box<EvalAltResult>> {
let processed_script = self.preprocess_basic_script(script);
info!("Processed Script:\n{}", processed_script);
match self.engine.compile(&processed_script) {
Ok(ast) => Ok(ast),
Err(parse_error) => Err(Box::new(parse_error.into())),
}
}
pub fn run(&self, ast: &rhai::AST) -> Result<Dynamic, Box<EvalAltResult>> {
self.engine.eval_ast(ast)
}
}