diff --git a/src/basic/compiler/goto_transform.rs b/src/basic/compiler/goto_transform.rs new file mode 100644 index 00000000..8ffe92e8 --- /dev/null +++ b/src/basic/compiler/goto_transform.rs @@ -0,0 +1,504 @@ +//! GOTO/Label transformation for BASIC +//! +//! Transforms GOTO-based control flow into a state machine that Rhai can execute. +//! +//! # Warning +//! +//! While GOTO is supported for backward compatibility, it is **strongly recommended** +//! to use event-driven patterns with the `ON` keyword instead: +//! +//! ```basic +//! ' ❌ OLD WAY - GOTO loop (not recommended) +//! mainLoop: +//! data = FIND "sensors", "processed = false" +//! WAIT 5 +//! GOTO mainLoop +//! +//! ' ✅ NEW WAY - Event-driven with ON (recommended) +//! ON INSERT OF "sensors" +//! data = GET LAST "sensors" +//! ' Process data reactively +//! END ON +//! ``` +//! +//! Benefits of ON over GOTO: +//! - More efficient (no polling) +//! - Cleaner code structure +//! - Better integration with LLM tools +//! - Automatic resource management + +use log::{trace, warn}; +use std::collections::HashSet; + +/// Represents a labeled block of code +#[derive(Debug, Clone)] +struct LabeledBlock { + name: String, + lines: Vec, + next_label: Option, // Fall-through label +} + +/// Check if source contains GOTO statements or labels +pub fn has_goto_constructs(source: &str) -> bool { + for line in source.lines() { + let trimmed = line.trim(); + let upper = trimmed.to_uppercase(); + + // Label detection: "labelname:" at start of line (not a string, not a comment) + if is_label_line(trimmed) { + return true; + } + + // GOTO detection (but not ON ERROR GOTO) + if upper.contains("GOTO ") && !upper.contains("ON ERROR GOTO") { + return true; + } + } + false +} + +/// Check if a line is a label definition +fn is_label_line(line: &str) -> bool { + let trimmed = line.trim(); + + // Must end with : and not contain spaces (except it could be indented) + if !trimmed.ends_with(':') { + return false; + } + + // Skip if it's a comment + if trimmed.starts_with('\'') || trimmed.starts_with("REM ") || trimmed.starts_with("//") { + return false; + } + + // Get the label name (everything before the colon) + let label_part = trimmed.trim_end_matches(':'); + + // Must be a valid identifier (alphanumeric + underscore, not starting with number) + if label_part.is_empty() { + return false; + } + + // Check it's not a CASE statement or other construct + let upper = label_part.to_uppercase(); + if upper == "CASE" || upper == "DEFAULT" || upper == "ELSE" { + return false; + } + + // Must be a simple identifier + let first_char = label_part.chars().next().unwrap(); + if !first_char.is_alphabetic() && first_char != '_' { + return false; + } + + label_part.chars().all(|c| c.is_alphanumeric() || c == '_') +} + +/// Extract label name from a label line +fn extract_label(line: &str) -> Option { + if is_label_line(line) { + Some(line.trim().trim_end_matches(':').to_string()) + } else { + None + } +} + +/// Transform BASIC code with GOTO/labels into Rhai-compatible state machine +/// +/// This function emits warnings when GOTO is detected, recommending the use of +/// ON keyword patterns instead. +pub fn transform_goto(source: &str) -> String { + let lines: Vec<&str> = source.lines().collect(); + + // First pass: find all labels and GOTO statements + let mut labels: HashSet = HashSet::new(); + let mut goto_targets: HashSet = HashSet::new(); + let mut has_goto = false; + + for line in &lines { + let trimmed = line.trim(); + let upper = trimmed.to_uppercase(); + + // Label detection + if let Some(label) = extract_label(trimmed) { + labels.insert(label); + } + + // GOTO detection (but not ON ERROR GOTO) + if upper.contains("GOTO ") && !upper.contains("ON ERROR GOTO") { + has_goto = true; + + // Extract target label + if let Some(target) = extract_goto_target(trimmed) { + goto_targets.insert(target); + } + } + } + + // No GOTO? Return unchanged + if !has_goto { + return source.to_string(); + } + + // Emit warning about GOTO usage + warn!( + "⚠️ GOTO detected in BASIC script. Consider using event-driven patterns with ON keyword instead." + ); + warn!(" Example: ON INSERT OF \"table\" ... END ON"); + warn!(" See documentation: https://docs.generalbots.com/06-gbdialog/keyword-on.html"); + + // Check for undefined labels + for target in &goto_targets { + if !labels.contains(target) { + warn!("⚠️ GOTO references undefined label: {}", target); + } + } + + trace!( + "Transforming GOTO: {} labels found, {} GOTO statements", + labels.len(), + goto_targets.len() + ); + + // Second pass: split code into labeled blocks + let blocks = split_into_blocks(&lines, &labels); + + // Third pass: generate state machine + generate_state_machine(&blocks) +} + +/// Split source lines into labeled blocks +fn split_into_blocks(lines: &[&str], labels: &HashSet) -> Vec { + let mut blocks: Vec = Vec::new(); + let mut current_label = "__start".to_string(); + let mut current_lines: Vec = Vec::new(); + let mut label_order: Vec = vec!["__start".to_string()]; + + for line in lines { + let trimmed = line.trim(); + + // Skip empty lines and comments in block splitting (but keep them in output) + if trimmed.is_empty() + || trimmed.starts_with('\'') + || trimmed.starts_with("//") + || trimmed.starts_with("REM ") + { + // Keep comments in the current block + if !trimmed.is_empty() { + current_lines.push(format!( + "// {}", + trimmed.trim_start_matches(&['\'', '/'][..]) + )); + } + continue; + } + + // New label starts a new block + if let Some(label) = extract_label(trimmed) { + // Save previous block if it has content + if !current_lines.is_empty() || current_label != "__start" || blocks.is_empty() { + let next_label = if labels.contains(&label) { + Some(label.clone()) + } else { + None + }; + + blocks.push(LabeledBlock { + name: current_label.clone(), + lines: current_lines.clone(), + next_label, + }); + } + + current_label = label.clone(); + label_order.push(label); + current_lines.clear(); + continue; + } + + current_lines.push(trimmed.to_string()); + } + + // Save final block + if !current_lines.is_empty() || blocks.is_empty() { + blocks.push(LabeledBlock { + name: current_label, + lines: current_lines, + next_label: None, // End of program + }); + } + + // Fix up next_label references based on order + let label_order_vec: Vec<_> = label_order.iter().collect(); + for (i, block) in blocks.iter_mut().enumerate() { + if block.next_label.is_none() && i + 1 < label_order_vec.len() { + // Check if there's a block after this one + let current_idx = label_order_vec.iter().position(|l| **l == block.name); + if let Some(idx) = current_idx { + if idx + 1 < label_order_vec.len() { + block.next_label = Some(label_order_vec[idx + 1].to_string()); + } + } + } + } + + blocks +} + +/// Extract the target label from a GOTO statement +fn extract_goto_target(line: &str) -> Option { + let upper = line.to_uppercase(); + + // Simple GOTO + if let Some(pos) = upper.find("GOTO ") { + let rest = &line[pos + 5..]; + let target = rest.trim().split_whitespace().next()?; + return Some(target.trim_matches(|c| c == '"' || c == '\'').to_string()); + } + + None +} + +/// Generate the state machine code +fn generate_state_machine(blocks: &[LabeledBlock]) -> String { + let mut output = String::new(); + + // Add warning comment at the top + output.push_str( + "// ⚠️ WARNING: This code uses GOTO which is transformed into a state machine.\n", + ); + output.push_str("// Consider using event-driven patterns with ON keyword instead:\n"); + output.push_str("// ON INSERT OF \"table\" ... END ON\n"); + output.push_str("// See: https://docs.generalbots.com/06-gbdialog/keyword-on.html\n\n"); + + // Determine start label + let start_label = if blocks.is_empty() { + "__start" + } else { + &blocks[0].name + }; + + output.push_str(&format!("let __goto_label = \"{}\";\n", start_label)); + output.push_str("let __goto_iterations = 0;\n"); + output.push_str("let __goto_max_iterations = 1000000;\n\n"); + output.push_str("while __goto_label != \"__exit\" {\n"); + output.push_str(" __goto_iterations += 1;\n"); + output.push_str(" if __goto_iterations > __goto_max_iterations {\n"); + output.push_str( + " throw \"GOTO loop exceeded maximum iterations. Possible infinite loop.\";\n", + ); + output.push_str(" }\n\n"); + + for block in blocks { + output.push_str(&format!(" if __goto_label == \"{}\" {{\n", block.name)); + + for line in &block.lines { + let transformed = transform_line(line); + // Indent the transformed line + for transformed_line in transformed.lines() { + if !transformed_line.trim().is_empty() { + output.push_str(&format!(" {}\n", transformed_line)); + } + } + } + + // Fall-through to next label or exit + match &block.next_label { + Some(next) => { + output.push_str(&format!(" __goto_label = \"{}\"; continue;\n", next)); + } + None => { + output.push_str(" __goto_label = \"__exit\";\n"); + } + } + + output.push_str(" }\n\n"); + } + + output.push_str("}\n"); + output +} + +/// Transform a single line, handling GOTO and IF...GOTO +fn transform_line(line: &str) -> String { + let trimmed = line.trim(); + let upper = trimmed.to_uppercase(); + + // Skip ON ERROR GOTO - that's handled separately + if upper.contains("ON ERROR GOTO") { + return trimmed.to_string(); + } + + // Simple GOTO at start of line + if upper.starts_with("GOTO ") { + let target = trimmed[5..].trim(); + return format!("__goto_label = \"{}\"; continue;", target); + } + + // IF ... THEN GOTO ... (single line if) + if upper.starts_with("IF ") && upper.contains(" THEN GOTO ") { + if let Some(then_pos) = upper.find(" THEN GOTO ") { + let condition = &trimmed[3..then_pos].trim(); + let target = trimmed[then_pos + 11..].trim(); + return format!( + "if {} {{ __goto_label = \"{}\"; continue; }}", + condition, target + ); + } + } + + // IF ... THEN ... (might contain GOTO after THEN) + if upper.starts_with("IF ") && upper.contains(" THEN ") { + if let Some(then_pos) = upper.find(" THEN ") { + let after_then = &trimmed[then_pos + 6..]; + let after_then_upper = after_then.trim().to_uppercase(); + + if after_then_upper.starts_with("GOTO ") { + let condition = &trimmed[3..then_pos].trim(); + let target = after_then.trim()[5..].trim(); + return format!( + "if {} {{ __goto_label = \"{}\"; continue; }}", + condition, target + ); + } + } + } + + // IF ... GOTO ... (without THEN - some BASIC dialects support this) + if upper.starts_with("IF ") && upper.contains(" GOTO ") && !upper.contains(" THEN ") { + if let Some(goto_pos) = upper.find(" GOTO ") { + let condition = &trimmed[3..goto_pos].trim(); + let target = trimmed[goto_pos + 6..].trim(); + return format!( + "if {} {{ __goto_label = \"{}\"; continue; }}", + condition, target + ); + } + } + + // Not a GOTO line, return as-is + trimmed.to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_label_line() { + assert!(is_label_line("start:")); + assert!(is_label_line(" mainLoop:")); + assert!(is_label_line("my_label:")); + assert!(is_label_line("label123:")); + + assert!(!is_label_line("TALK \"hello:\"")); + assert!(!is_label_line("' comment:")); + assert!(!is_label_line("CASE:")); + assert!(!is_label_line("123label:")); + assert!(!is_label_line("has space:")); + } + + #[test] + fn test_extract_goto_target() { + assert_eq!(extract_goto_target("GOTO start"), Some("start".to_string())); + assert_eq!( + extract_goto_target(" GOTO myLabel"), + Some("myLabel".to_string()) + ); + assert_eq!( + extract_goto_target("IF x > 5 THEN GOTO done"), + Some("done".to_string()) + ); + assert_eq!(extract_goto_target("TALK \"hello\""), None); + } + + #[test] + fn test_transform_line_simple_goto() { + assert_eq!( + transform_line("GOTO start"), + "__goto_label = \"start\"; continue;" + ); + assert_eq!( + transform_line(" GOTO myLoop "), + "__goto_label = \"myLoop\"; continue;" + ); + } + + #[test] + fn test_transform_line_if_then_goto() { + let result = transform_line("IF x < 10 THEN GOTO start"); + assert!(result.contains("if x < 10")); + assert!(result.contains("__goto_label = \"start\"")); + assert!(result.contains("continue")); + } + + #[test] + fn test_transform_line_if_goto_no_then() { + let result = transform_line("IF x < 10 GOTO start"); + assert!(result.contains("if x < 10")); + assert!(result.contains("__goto_label = \"start\"")); + } + + #[test] + fn test_transform_line_not_goto() { + assert_eq!(transform_line("TALK \"Hello\""), "TALK \"Hello\""); + assert_eq!(transform_line("x = x + 1"), "x = x + 1"); + assert_eq!(transform_line("ON ERROR GOTO 0"), "ON ERROR GOTO 0"); + } + + #[test] + fn test_has_goto_constructs() { + assert!(has_goto_constructs("start:\nTALK \"hi\"\nGOTO start")); + assert!(has_goto_constructs("IF x > 0 THEN GOTO done")); + assert!(!has_goto_constructs("TALK \"hello\"\nWAIT 1")); + assert!(!has_goto_constructs("ON ERROR GOTO 0")); // This is special, not regular GOTO + } + + #[test] + fn test_transform_goto_simple() { + let input = r#"start: + TALK "Hello" + x = x + 1 + IF x < 3 THEN GOTO start + TALK "Done""#; + + let output = transform_goto(input); + + assert!(output.contains("__goto_label")); + assert!(output.contains("while")); + assert!(output.contains("\"start\"")); + assert!(output.contains("WARNING")); + } + + #[test] + fn test_transform_goto_no_goto() { + let input = "TALK \"Hello\"\nTALK \"World\""; + let output = transform_goto(input); + assert_eq!(output, input); // Unchanged + } + + #[test] + fn test_transform_goto_multiple_labels() { + let input = r#"start: + TALK "Start" + GOTO middle +middle: + TALK "Middle" + GOTO done +done: + TALK "Done""#; + + let output = transform_goto(input); + + assert!(output.contains("\"start\"")); + assert!(output.contains("\"middle\"")); + assert!(output.contains("\"done\"")); + } + + #[test] + fn test_infinite_loop_protection() { + let output = transform_goto("loop:\nGOTO loop"); + assert!(output.contains("__goto_max_iterations")); + assert!(output.contains("throw")); + } +} diff --git a/src/basic/compiler/mod.rs b/src/basic/compiler/mod.rs index f90d1b62..bfaa3b9d 100644 --- a/src/basic/compiler/mod.rs +++ b/src/basic/compiler/mod.rs @@ -6,7 +6,9 @@ use crate::shared::state::AppState; use diesel::ExpressionMethods; use diesel::QueryDsl; use diesel::RunQueryDsl; -use log::warn; +use log::{trace, warn}; + +pub mod goto_transform; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::collections::HashSet; @@ -313,6 +315,16 @@ impl BasicCompiler { ) -> Result> { let bot_uuid = bot_id; let mut result = String::new(); + + // Transform GOTO/labels into state machine if present + // WARNING: GOTO is supported but event-driven ON patterns are recommended + let source = if goto_transform::has_goto_constructs(source) { + trace!("GOTO constructs detected, transforming to state machine"); + goto_transform::transform_goto(source) + } else { + source.to_string() + }; + let source = source.as_str(); let mut has_schedule = false; let mut _has_webhook = false; let script_name = Path::new(source_path) diff --git a/templates/README.md b/templates/README.md index e2fc10d5..54db3026 100644 --- a/templates/README.md +++ b/templates/README.md @@ -2,8 +2,50 @@ Pre-built bot packages for common business use cases. Templates are organized by category for easy discovery. +--- + +## Complete Template List (Flat Reference) + +| # | Template | Category | Folder | Key Features | +|---|----------|----------|--------|--------------| +| 1 | Default | Core | `default.gbai` | Minimal starter bot | +| 2 | Template | Core | `template.gbai` | Reference implementation | +| 3 | AI Search | Search | `ai-search.gbai` | QR codes, document search | +| 4 | Announcements | Communications | `announcements.gbai` | Company news, multiple KB | +| 5 | Analytics Dashboard | Platform | `analytics-dashboard.gbai` | Metrics, Reports | +| 6 | Backup | Platform | `backup.gbai` | Server backup scripts | +| 7 | Bank | Finance | `bank.gbai` | Banking services | +| 8 | BI | Platform | `bi.gbai` | Dashboards, role separation | +| 9 | Bling | Integration | `bling.gbai` | Bling ERP integration | +| 10 | Broadcast | Communications | `broadcast.gbai` | Mass messaging | +| 11 | Crawler | Search | `crawler.gbai` | Web indexing | +| 12 | CRM | Sales | `sales/crm.gbai` | Customer management | +| 13 | Attendance CRM | Sales | `sales/attendance-crm.gbai` | Event attendance tracking | +| 14 | Marketing | Sales | `sales/marketing.gbai` | Campaign tools | +| 15 | Education | Education | `edu.gbai` | Course management | +| 16 | ERP | Operations | `erp.gbai` | Process automation | +| 17 | HIPAA Medical | Compliance | `compliance/hipaa-medical.gbai` | HIPAA, HITECH | +| 18 | Privacy | Compliance | `compliance/privacy.gbai` | LGPD, GDPR, CCPA | +| 19 | Law | Legal | `law.gbai` | Document templates | +| 20 | LLM Server | AI | `llm-server.gbai` | Model hosting | +| 21 | LLM Tools | AI | `llm-tools.gbai` | TOOL-based LLM examples | +| 22 | Store | E-commerce | `store.gbai` | Product catalog | +| 23 | Talk to Data | Platform | `talk-to-data.gbai` | Natural language SQL | +| 24 | WhatsApp | Messaging | `whatsapp.gbai` | WhatsApp Business | + +--- + ## Categories +### `/sales` +Customer relationship and marketing templates. + +| Template | Description | Features | +|----------|-------------|----------| +| `crm.gbai` | Full CRM system | Leads, Contacts, Accounts, Opportunities | +| `marketing.gbai` | Marketing automation | Campaigns, Lead capture, Email sequences | +| `attendance-crm.gbai` | Event attendance | Check-ins, Tracking | + ### `/compliance` Privacy and regulatory compliance templates. @@ -12,13 +54,29 @@ Privacy and regulatory compliance templates. | `privacy.gbai` | Data subject rights portal | LGPD, GDPR, CCPA | | `hipaa-medical.gbai` | Healthcare privacy management | HIPAA, HITECH | -### `/sales` -Customer relationship and marketing templates. +### `/platform` +Platform administration and analytics templates. | Template | Description | Features | |----------|-------------|----------| -| `crm.gbai` | Full CRM system | Leads, Contacts, Accounts, Opportunities, Activities | -| `marketing.gbai` | Marketing automation | Campaigns, Lead capture, Email sequences | +| `analytics-dashboard.gbai` | Platform analytics bot | Metrics, Reports, AI insights | +| `bi.gbai` | Business intelligence | Dashboards, role separation | +| `backup.gbai` | Backup automation | Server backup scripts | +| `talk-to-data.gbai` | Data queries | Natural language SQL | + +### `/finance` +Financial services templates. + +| Template | Description | Features | +|----------|-------------|----------| +| `bank.gbai` | Banking services | Account management | + +### `/integration` +External API and service integrations. + +| Template | Description | APIs | +|----------|-------------|------| +| `bling.gbai` | Bling ERP | Brazilian ERP integration | ### `/productivity` Office and personal productivity templates. @@ -26,29 +84,14 @@ Office and personal productivity templates. | Template | Description | Features | |----------|-------------|----------| | `office.gbai` | Office automation | Document management, Scheduling | -| `reminder.gbai` | Reminder and notification system | Scheduled alerts, Follow-ups | - -### `/platform` -Platform administration and analytics templates. - -| Template | Description | Features | -|----------|-------------|----------| -| `analytics.gbai` | Platform analytics bot | Metrics, Reports, AI insights | - -### `/integration` -External API and service integrations. - -| Template | Description | APIs | -|----------|-------------|------| -| `api-client.gbai` | REST API client examples | Various | -| `public-apis.gbai` | Public API integrations | Weather, News, etc. | +| `reminder.gbai` | Reminder system | Scheduled alerts, Follow-ups | ### `/hr` Human resources templates. | Template | Description | Features | |----------|-------------|----------| -| `employee-mgmt.gbai` | Employee management | Directory, Onboarding | +| `employees.gbai` | Employee management | Directory, Onboarding | ### `/it` IT service management templates. @@ -64,14 +107,6 @@ Healthcare-specific templates. |----------|-------------|----------| | `patient-comm.gbai` | Patient communication | Appointments, Reminders | -### `/finance` -Financial services templates. - -| Template | Description | Features | -|----------|-------------|----------| -| `bank.gbai` | Banking services | Account management | -| `finance.gbai` | Financial operations | Invoicing, Payments | - ### `/nonprofit` Nonprofit organization templates. @@ -85,6 +120,7 @@ Core and utility templates. | Template | Description | |----------|-------------| | `default.gbai` | Starter template | +| `template.gbai` | Template for creating templates | | `ai-search.gbai` | AI-powered document search | | `announcements.gbai` | Company announcements | | `backup.gbai` | Backup automation | @@ -96,10 +132,10 @@ Core and utility templates. | `llm-server.gbai` | LLM server management | | `llm-tools.gbai` | LLM tool definitions | | `store.gbai` | E-commerce | -| `talk-to-data.gbai` | Natural language data queries | -| `template.gbai` | Template for creating templates | | `whatsapp.gbai` | WhatsApp-specific features | +--- + ## Template Structure Each `.gbai` template follows this structure: @@ -109,7 +145,7 @@ template-name.gbai/ ├── README.md # Template documentation ├── template-name.gbdialog/ # BASIC dialog scripts │ ├── start.bas # Entry point -│ └── *.bas # Additional dialogs +│ └── *.bas # Additional dialogs (auto-discovered as TOOLs) ├── template-name.gbot/ # Bot configuration │ └── config.csv # Settings ├── template-name.gbkb/ # Knowledge base (optional) @@ -119,6 +155,55 @@ template-name.gbai/ └── index.html ``` +--- + +## Event-Driven Patterns + +Templates should use **ON** triggers instead of polling loops: + +```basic +' ❌ OLD WAY - Polling (avoid) +mainLoop: + leads = FIND "leads", "processed = false" + WAIT 5 +GOTO mainLoop + +' ✅ NEW WAY - Event-Driven +ON INSERT OF "leads" + lead = GET LAST "leads" + score = SCORE LEAD lead + TALK TO "whatsapp:" + sales_phone, "New lead: " + lead.name +END ON +``` + +--- + +## TOOL-Based LLM Integration + +Every `.bas` file with `PARAM` and `DESCRIPTION` becomes an LLM-invokable tool: + +```basic +' score-lead.bas +PARAM email AS STRING LIKE "john@company.com" DESCRIPTION "Lead email" +PARAM name AS STRING LIKE "John Smith" DESCRIPTION "Lead name" + +DESCRIPTION "Score a new lead. Use when user mentions a prospect." + +lead = NEW OBJECT +lead.email = email +lead.name = name + +score = AI SCORE LEAD lead + +IF score.status = "hot" THEN + TALK TO "whatsapp:+5511999887766", "🔥 Hot lead: " + name +END IF + +TALK "Lead scored: " + score.score + "/100" +``` + +--- + ## Installation ### From Console @@ -141,6 +226,8 @@ Copy the template folder to your bot's packages directory: cp -r templates/sales/crm.gbai /path/to/your/bot/packages/ ``` +--- + ## Creating Custom Templates 1. Copy `template.gbai` as a starting point @@ -148,16 +235,22 @@ cp -r templates/sales/crm.gbai /path/to/your/bot/packages/ 3. Update internal folder names to match 4. Edit `config.csv` with your bot settings 5. Create dialog scripts in the `.gbdialog` folder -6. Add documentation in `README.md` +6. Use **ON** triggers instead of polling loops +7. Add `PARAM` and `DESCRIPTION` to make scripts LLM-invokable +8. Add documentation in `README.md` ### Template Best Practices -- Use `HEAR AS` for typed input validation -- Use spaces in keywords (e.g., `SET BOT MEMORY`, not `SET_BOT_MEMORY`) +- Use `ON` for event-driven automation +- Use `TALK TO` for multi-channel notifications +- Use `LLM` for intelligent decision-making +- Use `SCORE LEAD` / `AI SCORE LEAD` for lead qualification +- Create `.bas` files with `DESCRIPTION` for LLM tool discovery - Log activities for audit trails - Include error handling - Document all configuration options -- Provide example conversations + +--- ## Contributing Templates @@ -169,6 +262,8 @@ cp -r templates/sales/crm.gbai /path/to/your/bot/packages/ - Updated category README - Entry in this document +--- + ## License All templates are licensed under AGPL-3.0 as part of General Bots.