Creating Custom Keywords
BotServer’s BASIC scripting language can be extended with custom keywords. All keywords are implemented as Rust functions in the src/basic/keywords/ directory.
Overview
Keywords in BotServer are Rust functions that get registered with the Rhai scripting engine. They provide the core functionality that BASIC scripts can use to interact with the system.
Keyword Implementation Structure
File Organization
Each keyword is typically implemented in its own module file:
src/basic/keywords/
├── mod.rs # Module registration
├── hear_talk.rs # HEAR and TALK keywords
├── llm_keyword.rs # LLM keyword
├── bot_memory.rs # GET BOT MEMORY, SET BOT MEMORY
├── use_kb.rs # USE KB keyword
├── clear_kb.rs # CLEAR KB keyword
├── get.rs # GET keyword
├── format.rs # FORMAT keyword
└── [other keywords].rs
Creating a New Keyword
Step 1: Create the Module File
Create a new file in src/basic/keywords/ for your keyword:
src/basic/keywords/my_keyword.rs
Step 2: Implement the Keyword Function
Keywords are implemented using one of two Rhai registration methods:
Method 1: Simple Function Registration
For basic keywords that return values:
#![allow(unused)] fn main() { use rhai::Engine; use std::sync::Arc; use crate::core::shared::state::AppState; use crate::core::session::UserSession; pub fn my_keyword( state: Arc<AppState>, user_session: UserSession, engine: &mut Engine ) { let state_clone = Arc::clone(&state); let user_clone = user_session.clone(); engine.register_fn("MY_KEYWORD", move |param: String| -> String { // Your keyword logic here format!("Processed: {}", param) }); } }
Method 2: Custom Syntax Registration
For keywords with special syntax or side effects:
#![allow(unused)] fn main() { use rhai::{Engine, EvalAltResult}; use std::sync::Arc; use crate::core::shared::state::AppState; use crate::core::session::BotSession; pub fn register_my_keyword( state: Arc<AppState>, session: Arc<BotSession>, engine: &mut Engine ) -> Result<(), Box<EvalAltResult>> { let state_clone = Arc::clone(&state); let session_clone = Arc::clone(&session); engine.register_custom_syntax( &["MY_KEYWORD", "$expr$"], // Syntax pattern true, // Is statement (not expression) move |context, inputs| { let param = context.eval_expression_tree(&inputs[0])?.to_string(); // Your keyword logic here info!("MY_KEYWORD executed with: {}", param); Ok(().into()) } )?; Ok(()) } }
Step 3: Register in mod.rs
Add your module to src/basic/keywords/mod.rs:
#![allow(unused)] fn main() { pub mod my_keyword; }
Step 4: Add to Keyword Registry
Keywords are registered in the BASIC interpreter initialization. The registration happens in the main interpreter setup where all keywords are added to the Rhai engine.
Keyword Patterns
Pattern 1: Database Operations
Keywords that interact with the database (like GET BOT MEMORY):
#![allow(unused)] fn main() { pub fn database_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); let user_clone = user.clone(); engine.register_fn("DB_KEYWORD", move |key: String| -> String { let state = Arc::clone(&state_clone); let conn_result = state.conn.get(); if let Ok(mut conn) = conn_result { // Database query using Diesel // Return result } else { String::new() } }); } }
Pattern 2: Async Operations
Keywords that need async operations (like WEATHER):
#![allow(unused)] fn main() { pub fn async_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) { engine.register_custom_syntax(&["ASYNC_OP", "$expr$"], false, move |context, inputs| { let param = context.eval_expression_tree(&inputs[0])?; // Create channel for async result let (tx, rx) = std::sync::mpsc::channel(); // Spawn blocking task std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); let result = rt.block_on(async { // Async operation here "result".to_string() }); let _ = tx.send(result); }); // Wait for result match rx.recv_timeout(Duration::from_secs(30)) { Ok(result) => Ok(result.into()), Err(_) => Ok("Timeout".into()), } }); } }
Pattern 3: Session Management
Keywords that modify session state (like USE KB, CLEAR KB):
#![allow(unused)] fn main() { pub fn register_session_keyword( state: Arc<AppState>, session: Arc<BotSession>, engine: &mut Engine ) -> Result<(), Box<EvalAltResult>> { let session_clone = Arc::clone(&session); engine.register_custom_syntax(&["SESSION_OP", "$expr$"], true, move |context, inputs| { let param = context.eval_expression_tree(&inputs[0])?.to_string(); // Modify session state let mut session_lock = session_clone.blocking_write(); // Update session fields Ok(().into()) })?; Ok(()) } }
Available Dependencies
Keywords have access to:
-
AppState: Application-wide state including:
- Database connection pool (
state.conn) - Drive client for S3-compatible storage (
state.drive) - Cache client (
state.cache) - Configuration (
state.config) - LLM provider (
state.llm_provider)
- Database connection pool (
-
UserSession: Current user’s session data:
- User ID (
user_session.user_id) - Bot ID (
user_session.bot_id) - Session ID (
user_session.session_id)
- User ID (
-
BotSession: Bot conversation state:
- Context collections
- Tool definitions
- Conversation history
- Session variables
Error Handling
Keywords should handle errors gracefully:
#![allow(unused)] fn main() { engine.register_fn("SAFE_KEYWORD", move |param: String| -> String { match risky_operation(¶m) { Ok(result) => result, Err(e) => { error!("Keyword error: {}", e); format!("Error: {}", e) } } }); }
Testing Keywords
Keywords can be tested with unit tests:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; #[test] fn test_my_keyword() { // Create test engine let mut engine = Engine::new(); // Register keyword // Test keyword execution // Assert results } } }
Best Practices
- Clone Arc References: Always clone Arc-wrapped state before moving into closures
- Use Logging: Add info/debug logging for keyword execution
- Handle Errors: Don’t panic, return error messages as strings
- Timeout Async Ops: Use timeouts for network operations
- Document Parameters: Use clear parameter names and add comments
- Keep It Simple: Each keyword should do one thing well
- Thread Safety: Ensure all operations are thread-safe
Example: Complete Keyword Implementation
Here’s a complete example of a custom keyword that saves data:
#![allow(unused)] fn main() { // src/basic/keywords/save_data.rs use rhai::Engine; use std::sync::Arc; use log::{info, error}; use crate::core::shared::state::AppState; use crate::core::session::UserSession; pub fn save_data_keyword( state: Arc<AppState>, user_session: UserSession, engine: &mut Engine ) { let state_clone = Arc::clone(&state); let user_clone = user_session.clone(); engine.register_fn("SAVE_DATA", move |key: String, value: String| -> String { info!("SAVE_DATA called: key={}, value={}", key, value); let state = Arc::clone(&state_clone); let conn_result = state.conn.get(); match conn_result { Ok(mut conn) => { // Save to database using Diesel // (actual implementation would use proper Diesel queries) info!("Data saved successfully"); "OK".to_string() } Err(e) => { error!("Database error: {}", e); format!("Error: {}", e) } } }); } }
Limitations
- Keywords must be synchronous or use blocking operations
- Direct async/await is not supported (use channels for async)
- Keywords are registered globally for all scripts
- Cannot dynamically add keywords at runtime
- All keywords must be compiled into the binary
Summary
Creating custom keywords extends BotServer’s BASIC language capabilities. Keywords are Rust functions registered with the Rhai engine that provide access to system features, databases, external APIs, and more. Follow the patterns shown above to create robust, thread-safe keywords that integrate seamlessly with the BotServer ecosystem.