Migrate automations to param and sqlite

- Rename script_name to param in automation flow and DB schema
- Add BotMemory model and bot_memories table
- Remove script_name field from automation
- Enable sqlite support via rusqlite and related crates (optional)
- Update prompts and queries to use param instead of script_name
- Remove deprecated annoucements GBai templates and align add-req.sh
- Refactor main to initialize automation service and simplify startup
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-10-16 11:43:02 -03:00
parent 83d4a61fcd
commit 4acb9bb8f5
22 changed files with 1132 additions and 209 deletions

66
Cargo.lock generated
View file

@ -1009,7 +1009,7 @@ dependencies = [
[[package]]
name = "botserver"
version = "0.1.0"
version = "6.0.1"
dependencies = [
"actix-cors",
"actix-multipart",
@ -1045,6 +1045,7 @@ dependencies = [
"regex",
"reqwest 0.12.23",
"rhai",
"rusqlite",
"serde",
"serde_json",
"smartstring",
@ -1766,8 +1767,11 @@ dependencies = [
"diesel_derives",
"downcast-rs",
"itoa",
"libsqlite3-sys",
"pq-sys",
"serde_json",
"sqlite-wasm-rs",
"time",
"uuid",
]
@ -1986,6 +1990,18 @@ dependencies = [
"num-traits",
]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fastrand"
version = "2.3.0"
@ -2312,6 +2328,15 @@ version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "hashlink"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown 0.15.5",
]
[[package]]
name = "headless_chrome"
version = "1.0.18"
@ -2986,6 +3011,16 @@ dependencies = [
"windows-targets 0.53.5",
]
[[package]]
name = "libsqlite3-sys"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
dependencies = [
"pkg-config",
"vcpkg",
]
[[package]]
name = "libwebrtc"
version = "0.3.16"
@ -4258,6 +4293,20 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rusqlite"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
dependencies = [
"bitflags 2.9.4",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "rustc-demangle"
version = "0.1.26"
@ -4698,6 +4747,21 @@ dependencies = [
"der",
]
[[package]]
name = "sqlite-wasm-rs"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aead1c279716985b981b7940ef9b652d3f93d70a7296853c633b7ce8fa8088a"
dependencies = [
"js-sys",
"once_cell",
"thiserror 2.0.17",
"tokio",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"

View file

@ -1,17 +1,46 @@
[package]
name = "botserver"
version = "0.1.0"
version = "6.0.1"
edition = "2021"
authors = ["Rodrigo Rodriguez <me@rodrigorodriguez.com>"]
authors = [
"@AlanPerdomo",
"@AnaPaulaGil",
"@arenasio",
"@AtyllaL",
"@christopherdecastilho",
"@danielolima96",
"@Dariojunior3",
"@davidlerner26",
"@ExperimentationGarage",
"@flavioandrade91",
"@HeraldoAlmeida",
"@joao-parana",
"@jonathasc",
"@jramos-br",
"@lpicanco",
"@marcosvelasco",
"@matheus39x",
"@oerlabshenrique",
"@othonlima",
"@PH-Nascimento",
"@phpussente",
"@RobsonDantasE",
"@rodrigorodriguez",
"@SarahLourenco",
"@thipatriota",
"@webgus",
"@ZuilhoSe",
]
description = "General Bots Server"
license = "AGPL-3.0"
repository = "https://github.pragmatismo.com.br/generalbots/botserver"
repository = "https://alm.pragmatismo.com.br/generalbots/botserver"
[features]
default = ["vectordb"]
vectordb = ["qdrant-client"]
email = ["imap"]
web_automation = ["headless_chrome"]
sqlite = ["rusqlite"]
[dependencies]
actix-cors = "0.7"
@ -28,6 +57,9 @@ base64 = "0.22"
bytes = "1.8"
chrono = { version = "0.4", features = ["serde"] }
diesel = { version = "2.1", features = ["postgres", "uuid", "chrono", "serde_json"] }
rusqlite = { version = "0.37.0", optional = true }
[target.'cfg(not(release))'.dependencies]
diesel = { version = "2.1", features = ["sqlite"] }
dotenvy = "0.15"
downloader = "0.2"
env_logger = "0.11"

View file

@ -24,7 +24,7 @@ dirs=(
#"auth"
#"automation"
#"basic"
"bot"
#"bot"
#"channels"
#"config"
#"context"
@ -60,8 +60,7 @@ done
files=(
"$PROJECT_ROOT/src/main.rs"
"$PROJECT_ROOT/src/basic/keywords/hear_talk.rs"
"$PROJECT_ROOT/templates/annoucements.gbai/annoucements.gbdialog/start.bas"
"$PROJECT_ROOT/templates/annoucements.gbai/annoucements.gbdialog/auth.bas"
"$PROJECT_ROOT/templates/annoucements.gbai/annoucements.gbdialog/update-summary.bas"
)
for file in "${files[@]}"; do

90
docs/guide/automation.md Normal file
View file

@ -0,0 +1,90 @@
# Automation System Documentation
## Overview
The automation system allows you to execute scripts automatically based on triggers like database changes or scheduled times.
## Database Configuration
### system_automations Table Structure
To create an automation, insert a record into the `system_automations` table:
| Column | Type | Description |
|--------|------|-------------|
| id | UUID | Unique identifier (auto-generated) |
| name | TEXT | Human-readable name |
| kind | INTEGER | Trigger type (see below) |
| target | TEXT | Target table name (for table triggers) |
| param | TEXT | Script filename or path |
| schedule | TEXT | Cron pattern (for scheduled triggers) |
| is_active | BOOLEAN | Whether automation is enabled |
| last_triggered | TIMESTAMP | Last execution time |
### Trigger Types (kind field)
- `0` - TableInsert (triggers on new rows)
- `1` - TableUpdate (triggers on row updates)
- `2` - TableDelete (triggers on row deletions)
- `3` - Scheduled (triggers on cron schedule)
## Configuration Examples
### 1. Scheduled Automation (Daily at 2:30 AM)
```sql
INSERT INTO system_automations (name, kind, target, param, schedule, is_active)
VALUES ('Daily Resume Update', 3, NULL, 'daily_resume.js', '30 2 * * *', true);
```
### 2. Table Change Automation
```sql
-- Trigger when new documents are added to documents table
INSERT INTO system_automations (name, kind, target, param, schedule, is_active)
VALUES ('Process New Documents', 0, 'documents', 'process_document.js', NULL, true);
```
## Cron Pattern Format
Use standard cron syntax: `minute hour day month weekday`
Examples:
- `0 9 * * *` - Daily at 9:00 AM
- `30 14 * * 1-5` - Weekdays at 2:30 PM
- `0 0 1 * *` - First day of every month at midnight
## Sample Script
```BASIC
let text = GET "default.gbdrive/default.pdf"
let resume = LLM "Build table resume with deadlines, dates and actions: " + text
SET BOT MEMORY "resume", resume
```
## Script Capabilities
### Available Commands
- `GET "path"` - Read files from storage
- `LLM "prompt"` - Query language model with prompts
- `SET BOT MEMORY "key", value` - Store data in bot memory
- Database operations (query, insert, update)
- HTTP requests to external APIs
## Best Practices
1. **Keep scripts focused** - Each script should do one thing well
2. **Handle errors gracefully** - Use try/catch blocks
3. **Log important actions** - Use console.log for debugging
4. **Test thoroughly** - Verify scripts work before automating
5. **Monitor execution** - Check logs for any automation errors
## Monitoring
Check application logs to monitor automation execution:
```bash
# Look for automation-related messages
grep "Automation\|Script executed" application.log
```
The system will automatically update `last_triggered` timestamps and log any errors encountered during execution.

View file

0
docs/guide/debugging.md Normal file
View file

348
docs/guide/last.md Normal file
View file

@ -0,0 +1,348 @@
# 📚 **BASIC LEARNING EXAMPLES - LAST Function**
## 🎯 **EXAMPLE 1: BASIC CONCEPT OF LAST FUNCTION**
```
**BASIC CONCEPT:**
LAST FUNCTION - Extract last word
**LEVEL:**
☒ Beginner ☐ Intermediate ☐ Advanced
**LEARNING OBJECTIVE:**
Understand how the LAST function extracts the last word from text
**CODE EXAMPLE:**
```basic
10 PALAVRA$ = "The mouse chewed the clothes"
20 ULTIMA$ = LAST(PALAVRA$)
30 PRINT "Last word: "; ULTIMA$
```
**SPECIFIC QUESTIONS:**
- How does the function know where the last word ends?
- What happens if there are extra spaces?
- Can I use it with numeric variables?
**PROJECT CONTEXT:**
I'm creating a program that analyzes sentences
**EXPECTED RESULT:**
Should display: "Last word: clothes"
**PARTS I DON'T UNDERSTAND:**
- Why are parentheses needed?
- How does the function work internally?
```
---
## 🛠️ **EXAMPLE 2: SOLVING ERROR WITH LAST**
```
**BASIC ERROR:**
"Syntax error" when using LAST
**MY CODE:**
```basic
10 TEXTO$ = "Good day world"
20 RESULTADO$ = LAST TEXTO$
30 PRINT RESULTADO$
```
**PROBLEM LINE:**
Line 20
**EXPECTED BEHAVIOR:**
Show "world" on screen
**CURRENT BEHAVIOR:**
Syntax error
**WHAT I'VE TRIED:**
- Tried without parentheses
- Tried with different quotes
- Tried changing variable name
**BASIC VERSION:**
QBASIC with Rhai extension
**CORRECTED SOLUTION:**
```basic
10 TEXTO$ = "Good day world"
20 RESULTADO$ = LAST(TEXTO$)
30 PRINT RESULTADO$
```
```
---
## 📖 **EXAMPLE 3: EXPLAINING LAST COMMAND**
```
**COMMAND:**
LAST - Extracts last word
**SYNTAX:**
```basic
ULTIMA$ = LAST(TEXTO$)
```
**PARAMETERS:**
- TEXTO$: String from which to extract the last word
**SIMPLE EXAMPLE:**
```basic
10 FRASE$ = "The sun is bright"
20 ULTIMA$ = LAST(FRASE$)
30 PRINT ULTIMA$ ' Shows: bright
```
**PRACTICAL EXAMPLE:**
```basic
10 INPUT "Enter your full name: "; NOME$
20 SOBRENOME$ = LAST(NOME$)
30 PRINT "Hello Mr./Mrs. "; SOBRENOME$
```
**COMMON ERRORS:**
- Forgetting parentheses: `LAST TEXTO$`
- Using with numbers: `LAST(123)`
- Forgetting to assign to a variable
**BEGINNER TIP:**
Always use parentheses and ensure content is text
**SUGGESTED EXERCISE:**
Create a program that asks for a sentence and shows the first and last word
```
---
## 🎨 **EXAMPLE 4: COMPLETE PROJECT WITH LAST**
```
# BASIC PROJECT: SENTENCE ANALYZER
## 📝 DESCRIPTION
Program that analyzes sentences and extracts useful information
## 🎨 FEATURES
- [x] Extract last word
- [x] Count words
- [x] Show statistics
## 🧩 CODE STRUCTURE
```basic
10 PRINT "=== SENTENCE ANALYZER ==="
20 INPUT "Enter a sentence: "; FRASE$
30
40 ' Extract last word
50 ULTIMA$ = LAST(FRASE$)
60
70 ' Count words (simplified)
80 PALAVRAS = 1
90 FOR I = 1 TO LEN(FRASE$)
100 IF MID$(FRASE$, I, 1) = " " THEN PALAVRAS = PALAVRAS + 1
110 NEXT I
120
130 PRINT
140 PRINT "Last word: "; ULTIMA$
150 PRINT "Total words: "; PALAVRAS
160 PRINT "Original sentence: "; FRASE$
```
## 🎯 LEARNINGS
- How to use LAST function
- How to count words manually
- String manipulation in BASIC
## ❓ QUESTIONS TO EVOLVE
- How to extract the first word?
- How to handle punctuation?
- How to work with multiple sentences?
```
---
## 🏆 **EXAMPLE 5: SPECIAL CASES AND TESTS**
```
**BASIC CONCEPT:**
SPECIAL CASES OF LAST FUNCTION
**LEVEL:**
☐ Beginner ☒ Intermediate ☐ Advanced
**LEARNING OBJECTIVE:**
Understand how LAST behaves in special situations
**CODE EXAMPLES:**
```basic
' Case 1: Empty string
10 TEXTO$ = ""
20 PRINT LAST(TEXTO$) ' Result: ""
' Case 2: Single word only
30 TEXTO$ = "Sun"
40 PRINT LAST(TEXTO$) ' Result: "Sun"
' Case 3: Multiple spaces
50 TEXTO$ = "Hello World "
60 PRINT LAST(TEXTO$) ' Result: "World"
' Case 4: With tabs and newlines
70 TEXTO$ = "Line1" + CHR$(9) + "Line2" + CHR$(13)
80 PRINT LAST(TEXTO$) ' Result: "Line2"
```
**SPECIFIC QUESTIONS:**
- What happens with empty strings?
- How does it work with special characters?
- Is it case-sensitive?
**PROJECT CONTEXT:**
I need to robustly validate user inputs
**EXPECTED RESULT:**
Consistent behavior in all cases
**PARTS I DON'T UNDERSTAND:**
- How the function handles whitespace?
- What are CHR$(9) and CHR$(13)?
```
---
## 🛠️ **EXAMPLE 6: INTEGRATION WITH OTHER FUNCTIONS**
```
**BASIC CONCEPT:**
COMBINING LAST WITH OTHER FUNCTIONS
**LEVEL:**
☐ Beginner ☒ Intermediate ☐ Advanced
**LEARNING OBJECTIVE:**
Learn to use LAST in more complex expressions
**CODE EXAMPLE:**
```basic
10 ' Example 1: With concatenation
20 PARTE1$ = "Programming"
30 PARTE2$ = " in BASIC"
40 FRASE_COMPLETA$ = PARTE1$ + PARTE2$
50 PRINT LAST(FRASE_COMPLETA$) ' Result: "BASIC"
60 ' Example 2: With string functions
70 NOME_COMPLETO$ = "Maria Silva Santos"
80 SOBRENOME$ = LAST(NOME_COMPLETO$)
90 PRINT "Mr./Mrs. "; SOBRENOME$
100 ' Example 3: In conditional expressions
110 FRASE$ = "The sky is blue"
120 IF LAST(FRASE$) = "blue" THEN PRINT "The last word is blue!"
```
**SPECIFIC QUESTIONS:**
- Can I use LAST directly in IF?
- How to combine with LEFT$, RIGHT$, MID$?
- Is there a size limit for the string?
**PROJECT CONTEXT:**
Creating validations and text processing
**EXPECTED RESULT:**
Use LAST flexibly in different contexts
**PARTS I DON'T UNDERSTAND:**
- Expression evaluation order
- Performance with very large strings
```
---
## 📚 **EXAMPLE 7: PRACTICAL EXERCISES**
```
# EXERCISES: PRACTICING WITH LAST
## 🎯 EXERCISE 1 - BASIC
Create a program that asks for the user's full name and greets using only the last name.
**SOLUTION:**
```basic
10 INPUT "Enter your full name: "; NOME$
20 SOBRENOME$ = LAST(NOME$)
30 PRINT "Hello, Mr./Mrs. "; SOBRENOME$; "!"
```
## 🎯 EXERCISE 2 - INTERMEDIATE
Make a program that analyzes if the last word of a sentence is "end".
**SOLUTION:**
```basic
10 INPUT "Enter a sentence: "; FRASE$
20 IF LAST(FRASE$) = "end" THEN PRINT "Sentence ends with 'end'" ELSE PRINT "Sentence doesn't end with 'end'"
```
## 🎯 EXERCISE 3 - ADVANCED
Create a program that processes multiple sentences and shows statistics.
**SOLUTION:**
```basic
10 DIM FRASES$(3)
20 FRASES$(1) = "The sun shines"
30 FRASES$(2) = "The rain falls"
40 FRASES$(3) = "The wind blows"
50
60 FOR I = 1 TO 3
70 PRINT "Sentence "; I; ": "; FRASES$(I)
80 PRINT "Last word: "; LAST(FRASES$(I))
90 PRINT
100 NEXT I
```
## 💡 TIPS
- Always test with different inputs
- Use PRINT for debugging
- Start with simple examples
```
---
## 🎨 **EXAMPLE 8: MARKDOWN DOCUMENTATION**
```markdown
# LAST FUNCTION - COMPLETE GUIDE
## 🎯 OBJECTIVE
Extract the last word from a string
## 📋 SYNTAX
```basic
RESULTADO$ = LAST(TEXTO$)
```
## 🧩 PARAMETERS
- `TEXTO$`: Input string
## 🔍 BEHAVIOR
- Splits string by spaces
- Returns the last part
- Ignores extra spaces at beginning/end
## 🚀 EXAMPLES
```basic
10 PRINT LAST("hello world") ' Output: world
20 PRINT LAST("one word") ' Output: word
30 PRINT LAST(" spaces ") ' Output: spaces
```
## ⚠️ LIMITATIONS
- Doesn't work with numbers
- Requires parentheses
- Considers only spaces as separators
```
These examples cover from the basic concept to practical applications of the LAST function, always focusing on BASIC beginners! 🚀

0
docs/guide/quickstart.md Normal file
View file

View file

@ -118,12 +118,12 @@ pub async fn execute_set_schedule(
let result = sqlx::query(
"INSERT INTO system_automations
(kind, schedule, script_name)
(kind, schedule, param)
VALUES ($1, $2, $3)"
)
.bind(TriggerKind::Scheduled as i32)
.bind(cron)
.bind(script_name)
.bind(param)
.execute(pool)
.await?;

201
prompts/dev/doc-keyword.md Normal file
View file

@ -0,0 +1,201 @@
# Modelo de Prompt para Aprendizado de BASIC em Markdown
## 🎯 **ESTRUTURA PARA APRENDIZ DE BASIC**
```
**CONCEITO BASIC:**
[Nome do conceito ou comando]
**NÍVEL:**
☐ Iniciante ☐ Intermediário ☐ Avançado
**OBJETIVO DE APRENDIZADO:**
[O que você quer entender ou criar]
**CÓDIGO EXEMPLO:**
```basic
[Seu código ou exemplo aqui]
```
**DÚVIDAS ESPECÍFICAS:**
- [Dúvida 1 sobre o conceito]
- [Dúvida 2 sobre sintaxe]
- [Dúvida 3 sobre aplicação]
**CONTEXTO DO PROJETO:**
[Descrição do que está tentando fazer]
**RESULTADO ESPERADO:**
[O que o código deve fazer]
**PARTES QUE NÃO ENTENDE:**
- [Trecho específico do código]
- [Mensagem de erro]
- [Lógica confusa]
```
---
## 📚 **EXEMPLO PRÁTICO: LOOP FOR**
```
**CONCEITO BASIC:**
LOOP FOR
**NÍVEL:**
☒ Iniciante ☐ Intermediário ☐ Avançado
**OBJETIVO DE APRENDIZADO:**
Entender como criar um contador de 1 a 10
**CÓDIGO EXEMPLO:**
```basic
10 FOR I = 1 TO 10
20 PRINT "Número: "; I
30 NEXT I
```
**DÚVIDAS ESPECÍFICAS:**
- O que significa "NEXT I"?
- Posso usar outras letras além de "I"?
- Como fazer contagem regressiva?
**CONTEXTO DO PROJETO:**
Estou criando um programa que lista números
**RESULTADO ESPERADO:**
Que apareça: Número: 1, Número: 2, etc.
**PARTES QUE NÃO ENTENDE:**
- Por que precisa do número 10 na linha 10?
- O que acontece se esquecer o NEXT?
```
---
## 🛠️ **MODELO PARA RESOLVER ERROS**
```
**ERRO NO BASIC:**
[Mensagem de erro ou comportamento estranho]
**MEU CÓDIGO:**
```basic
[Coloque seu código completo]
```
**LINHA COM PROBLEMA:**
[Linha específica onde ocorre o erro]
**COMPORTAMENTO ESPERADO:**
[O que deveria acontecer]
**COMPORTAMENTO ATUAL:**
[O que está acontecendo de errado]
**O QUE JÁ TENTEI:**
- [Tentativa 1 de correção]
- [Tentativa 2]
- [Tentativa 3]
**VERSÃO DO BASIC:**
[QBASIC, GW-BASIC, FreeBASIC, etc.]
```
---
## 📖 **MODELO PARA EXPLICAR COMANDOS**
```
**COMANDO:**
[Nome do comando - ex: PRINT, INPUT, GOTO]
**SYNTAX:**
[Como escrever corretamente]
**PARÂMETROS:**
- Parâmetro 1: [Função]
- Parâmetro 2: [Função]
**EXEMPLO SIMPLES:**
```basic
[Exemplo mínimo e funcional]
```
**EXEMPLO PRÁTICO:**
```basic
[Exemplo em contexto real]
```
**ERROS COMUNS:**
- [Erro frequente 1]
- [Erro frequente 2]
**DICA PARA INICIANTES:**
[Dica simples para não errar]
**EXERCÍCIO SUGERIDO:**
[Pequeno exercício para praticar]
```
---
## 🎨 **FORMATAÇÃO MARKDOWN PARA BASIC**
### **Como documentar seu código em .md:**
```markdown
# [NOME DO PROGRAMA]
## 🎯 OBJETIVO
[O que o programa faz]
## 📋 COMO USAR
1. [Passo 1]
2. [Passo 2]
## 🧩 CÓDIGO FONTE
```basic
[Seu código aqui]
```
## 🔍 EXPLICAÇÃO
- **Linha X**: [Explicação]
- **Linha Y**: [Explicação]
## 🚀 EXEMPLO DE EXECUÇÃO
```
[Saída do programa]
```
```
---
## 🏆 **MODELO DE PROJETO COMPLETO**
```
# PROJETO BASIC: [NOME]
## 📝 DESCRIÇÃO
[Descrição do que o programa faz]
## 🎨 FUNCIONALIDADES
- [ ] Funcionalidade 1
- [ ] Funcionalidade 2
- [ ] Funcionalidade 3
## 🧩 ESTRUTURA DO CÓDIGO
```basic
[Seu código organizado]
```
## 🎯 APRENDIZADOS
- [Conceito 1 aprendido]
- [Conceito 2 aprendido]
## ❓ DÚVIDAS PARA EVOLUIR
- [Dúvida para melhorar]
- [O que gostaria de fazer depois]
```
gerenerate several examples
for this keyword written in rhai do this only for basic audience:

1
prompts/dev/doc-topic.md Normal file
View file

@ -0,0 +1 @@
- Be pragmatic and concise with examples.

View file

@ -1,8 +1,3 @@
-- public.bots definition
-- Drop table
-- DROP TABLE public.bots;
CREATE TABLE public.bots (
id uuid DEFAULT gen_random_uuid() NOT NULL,

View file

@ -0,0 +1,13 @@
CREATE TABLE bot_memories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
key TEXT NOT NULL,
value TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(bot_id, key)
);
CREATE INDEX idx_bot_memories_bot_id ON bot_memories(bot_id);
CREATE INDEX idx_bot_memories_key ON bot_memories(key);

View file

@ -0,0 +1,141 @@
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use diesel::prelude::*;
use log::{error, info};
use rhai::{Dynamic, Engine};
use std::sync::Arc;
use uuid::Uuid;
pub fn set_bot_memory_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine
.register_custom_syntax(
&["SET_BOT_MEMORY", "$expr$", "$expr$"],
true,
move |context, inputs| {
let key = context.eval_expression_tree(&inputs[0])?.to_string();
let value = context.eval_expression_tree(&inputs[1])?.to_string();
let state_for_spawn = Arc::clone(&state_clone);
let user_clone_spawn = user_clone.clone();
let key_clone = key.clone();
let value_clone = value.clone();
tokio::spawn(async move {
use crate::shared::models::bot_memories;
let mut conn = match state_for_spawn.conn.lock() {
Ok(conn) => conn,
Err(e) => {
error!(
"Failed to acquire database connection for SET BOT MEMORY: {}",
e
);
return;
}
};
let bot_uuid = match Uuid::parse_str(&user_clone_spawn.bot_id.to_string()) {
Ok(uuid) => uuid,
Err(e) => {
error!("Invalid bot ID format: {}", e);
return;
}
};
let now = chrono::Utc::now();
let existing_memory: Option<Uuid> = bot_memories::table
.filter(bot_memories::bot_id.eq(bot_uuid))
.filter(bot_memories::key.eq(&key_clone))
.select(bot_memories::id)
.first(&mut *conn)
.optional()
.unwrap_or(None);
if let Some(memory_id) = existing_memory {
let update_result = diesel::update(
bot_memories::table.filter(bot_memories::id.eq(memory_id)),
)
.set((
bot_memories::value.eq(&value_clone),
bot_memories::updated_at.eq(now),
))
.execute(&mut *conn);
match update_result {
Ok(_) => {
info!(
"Updated bot memory for key: {} with value length: {}",
key_clone,
value_clone.len()
);
}
Err(e) => {
error!("Failed to update bot memory: {}", e);
}
}
} else {
let new_memory = crate::shared::models::BotMemory {
id: Uuid::new_v4(),
bot_id: bot_uuid,
key: key_clone.clone(),
value: value_clone.clone(),
created_at: now,
updated_at: now,
};
let insert_result = diesel::insert_into(bot_memories::table)
.values(&new_memory)
.execute(&mut *conn);
match insert_result {
Ok(_) => {
info!(
"Created new bot memory for key: {} with value length: {}",
key_clone,
value_clone.len()
);
}
Err(e) => {
error!("Failed to insert bot memory: {}", e);
}
}
}
});
Ok(Dynamic::UNIT)
},
)
.unwrap();
}
pub fn get_bot_memory_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine.register_fn("GET_BOT_MEMORY", move |key_param: String| -> String {
use crate::shared::models::bot_memories;
let state = Arc::clone(&state_clone);
let conn_result = state.conn.lock();
if let Ok(mut conn) = conn_result {
let bot_uuid = user_clone.bot_id;
let memory_value: Option<String> = bot_memories::table
.filter(bot_memories::bot_id.eq(bot_uuid))
.filter(bot_memories::key.eq(&key_param))
.select(bot_memories::value)
.first(&mut *conn)
.optional()
.unwrap_or(None);
memory_value.unwrap_or_default()
} else {
String::new()
}
});
}

View file

@ -1,3 +1,4 @@
pub mod bot_memory;
pub mod create_site;
pub mod find;
pub mod first;

View file

@ -18,7 +18,7 @@ pub fn on_keyword(state: &AppState, _user: UserSession, engine: &mut Engine) {
move |context, inputs| {
let trigger_type = context.eval_expression_tree(&inputs[0])?.to_string();
let table = context.eval_expression_tree(&inputs[1])?.to_string();
let script_name = format!("{}_{}.rhai", table, trigger_type.to_lowercase());
let name = format!("{}_{}.rhai", table, trigger_type.to_lowercase());
let kind = match trigger_type.to_uppercase().as_str() {
"UPDATE" => TriggerKind::TableUpdate,
@ -28,7 +28,7 @@ pub fn on_keyword(state: &AppState, _user: UserSession, engine: &mut Engine) {
};
let mut conn = state_clone.conn.lock().unwrap();
let result = execute_on_trigger(&mut *conn, kind, &table, &script_name)
let result = execute_on_trigger(&mut *conn, kind, &table, &name)
.map_err(|e| format!("DB error: {}", e))?;
if let Some(rows_affected) = result.get("rows_affected") {
@ -45,11 +45,11 @@ pub fn execute_on_trigger(
conn: &mut diesel::PgConnection,
kind: TriggerKind,
table: &str,
script_name: &str,
param: &str,
) -> Result<Value, String> {
info!(
"Starting execute_on_trigger with kind: {:?}, table: {}, script_name: {}",
kind, table, script_name
"Starting execute_on_trigger with kind: {:?}, table: {}, param: {}",
kind, table, param
);
use crate::shared::models::system_automations;
@ -57,7 +57,7 @@ pub fn execute_on_trigger(
let new_automation = (
system_automations::kind.eq(kind as i32),
system_automations::target.eq(table),
system_automations::script_name.eq(script_name),
system_automations::param.eq(param),
);
let result = diesel::insert_into(system_automations::table)
@ -72,7 +72,7 @@ pub fn execute_on_trigger(
"command": "on_trigger",
"trigger_type": format!("{:?}", kind),
"table": table,
"script_name": script_name,
"param": param,
"rows_affected": result
}))
}

View file

@ -15,10 +15,10 @@ pub fn set_schedule_keyword(state: &AppState, _user: UserSession, engine: &mut E
.register_custom_syntax(&["SET_SCHEDULE", "$string$"], true, {
move |context, inputs| {
let cron = context.eval_expression_tree(&inputs[0])?.to_string();
let script_name = format!("cron_{}.rhai", cron.replace(' ', "_"));
let param = format!("cron_{}.rhai", cron.replace(' ', "_"));
let mut conn = state_clone.conn.lock().unwrap();
let result = execute_set_schedule(&mut *conn, &cron, &script_name)
let result = execute_set_schedule(&mut *conn, &cron, &param)
.map_err(|e| format!("DB error: {}", e))?;
if let Some(rows_affected) = result.get("rows_affected") {
@ -34,11 +34,11 @@ pub fn set_schedule_keyword(state: &AppState, _user: UserSession, engine: &mut E
pub fn execute_set_schedule(
conn: &mut diesel::PgConnection,
cron: &str,
script_name: &str,
param: &str,
) -> Result<Value, Box<dyn std::error::Error>> {
info!(
"Starting execute_set_schedule with cron: {}, script_name: {}",
cron, script_name
"Starting execute_set_schedule with cron: {}, param: {}",
cron, param
);
use crate::shared::models::system_automations;
@ -46,7 +46,7 @@ pub fn execute_set_schedule(
let new_automation = (
system_automations::kind.eq(TriggerKind::Scheduled as i32),
system_automations::schedule.eq(cron),
system_automations::script_name.eq(script_name),
system_automations::param.eq(param),
);
let result = diesel::insert_into(system_automations::table)
@ -56,7 +56,7 @@ pub fn execute_set_schedule(
Ok(json!({
"command": "set_schedule",
"schedule": cron,
"script_name": script_name,
"param": param,
"rows_affected": result
}))
}

View file

@ -23,6 +23,7 @@ mod session;
mod shared;
mod tools;
mod whatsapp;
use crate::automation::AutomationService;
use crate::bot::{
auth_handler, create_session, get_session_history, get_sessions, index, set_mode_handler,
start_session, static_files, voice_start, voice_stop, websocket_handler,
@ -42,203 +43,213 @@ use crate::shared::state::AppState;
use crate::whatsapp::WhatsAppAdapter;
#[actix_web::main]
async fn main() -> std::io::Result<()> {// Load environment variables from a .env file, if present.
dotenv().ok();
let llama_url =
std::env::var("LLM_URL").unwrap_or_else(|_| "http://localhost:8081".to_string());
async fn main() -> std::io::Result<()> {
// Load environment variables from a .env file, if present.
dotenv().ok();
let llama_url =
std::env::var("LLM_URL").unwrap_or_else(|_| "http://localhost:8081".to_string());
// Initialise logger with environmentbased log level (default to "info").
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
// Initialise logger with environmentbased log level (default to "info").
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
// Load application configuration.
let cfg = AppConfig::from_env();
let config = std::sync::Arc::new(cfg.clone());
// Load application configuration.
let cfg = AppConfig::from_env();
let config = std::sync::Arc::new(cfg.clone());
// ----------------------------------------------------------------------
// Database connections
// ----------------------------------------------------------------------
let db_pool = match diesel::Connection::establish(&cfg.database_url()) {
Ok(conn) => {
info!("Connected to main database successfully");
Arc::new(Mutex::new(conn))
}
Err(e) => {
log::error!("Failed to connect to main database: {}", e);
return Err(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
format!("Database connection failed: {}", e),
));
}
};
// ----------------------------------------------------------------------
// Database connections
// ----------------------------------------------------------------------
let db_pool = match diesel::Connection::establish(&cfg.database_url()) {
Ok(conn) => {
info!("Connected to main database successfully");
Arc::new(Mutex::new(conn))
}
Err(e) => {
log::error!("Failed to connect to main database: {}", e);
return Err(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
format!("Database connection failed: {}", e),
));
}
};
// Placeholder for a second/custom database currently just reusing the main pool.
let _custom_db_url = format!(
"postgres://{}:{}@{}:{}/{}",
cfg.database_custom.username,
cfg.database_custom.password,
cfg.database_custom.server,
cfg.database_custom.port,
cfg.database_custom.database
);
let db_custom_pool = db_pool.clone();
// Placeholder for a second/custom database currently just reusing the main pool.
let _custom_db_url = format!(
"postgres://{}:{}@{}:{}/{}",
cfg.database_custom.username,
cfg.database_custom.password,
cfg.database_custom.server,
cfg.database_custom.port,
cfg.database_custom.database
);
let db_custom_pool = db_pool.clone();
// ----------------------------------------------------------------------
// LLM local server initialisation
// ----------------------------------------------------------------------
ensure_llama_servers_running()
.await
.expect("Failed to initialize LLM local server.");
// ----------------------------------------------------------------------
// LLM local server initialisation
// ----------------------------------------------------------------------
ensure_llama_servers_running()
.await
.expect("Failed to initialize LLM local server.");
// ----------------------------------------------------------------------
// Redis client (optional)
// ----------------------------------------------------------------------
let redis_client = match redis::Client::open("redis://127.0.0.1/") {
Ok(client) => {
info!("Connected to Redis successfully");
Some(Arc::new(client))
}
Err(e) => {
log::warn!("Failed to connect to Redis: {}", e);
None
}
};
// ----------------------------------------------------------------------
// Redis client (optional)
// ----------------------------------------------------------------------
let redis_client = match redis::Client::open("redis://127.0.0.1/") {
Ok(client) => {
info!("Connected to Redis successfully");
Some(Arc::new(client))
}
Err(e) => {
log::warn!("Failed to connect to Redis: {}", e);
None
}
};
// ----------------------------------------------------------------------
// Tooling and LLM provider
// ----------------------------------------------------------------------
let tool_manager = Arc::new(tools::ToolManager::new());
let llm_provider = Arc::new(crate::llm::OpenAIClient::new(
"empty".to_string(),
Some(llama_url.clone()),
));
// ----------------------------------------------------------------------
// Tooling and LLM provider
// ----------------------------------------------------------------------
let tool_manager = Arc::new(tools::ToolManager::new());
let llm_provider = Arc::new(crate::llm::OpenAIClient::new(
"empty".to_string(),
Some(llama_url.clone()),
));
// ----------------------------------------------------------------------
// Channel adapters
// ----------------------------------------------------------------------
let web_adapter = Arc::new(WebChannelAdapter::new());
let voice_adapter = Arc::new(VoiceAdapter::new(
"https://livekit.example.com".to_string(),
"api_key".to_string(),
"api_secret".to_string(),
));
let whatsapp_adapter = Arc::new(WhatsAppAdapter::new(
"whatsapp_token".to_string(),
"phone_number_id".to_string(),
"verify_token".to_string(),
));
let tool_api = Arc::new(tools::ToolApi::new());
// ----------------------------------------------------------------------
// Channel adapters
// ----------------------------------------------------------------------
let web_adapter = Arc::new(WebChannelAdapter::new());
let voice_adapter = Arc::new(VoiceAdapter::new(
"https://livekit.example.com".to_string(),
"api_key".to_string(),
"api_secret".to_string(),
));
let whatsapp_adapter = Arc::new(WhatsAppAdapter::new(
"whatsapp_token".to_string(),
"phone_number_id".to_string(),
"verify_token".to_string(),
));
let tool_api = Arc::new(tools::ToolApi::new());
// ----------------------------------------------------------------------
// S3 / MinIO client
// ----------------------------------------------------------------------
let drive = init_drive(&config.minio)
.await
.expect("Failed to initialize Drive");
// ----------------------------------------------------------------------
// S3 / MinIO client
// ----------------------------------------------------------------------
let drive = init_drive(&config.minio)
.await
.expect("Failed to initialize Drive");
// ----------------------------------------------------------------------
// Session and authentication services
// ----------------------------------------------------------------------
let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new(
diesel::Connection::establish(&cfg.database_url()).unwrap(),
redis_client.clone(),
)));
// ----------------------------------------------------------------------
// Session and authentication services
// ----------------------------------------------------------------------
let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new(
diesel::Connection::establish(&cfg.database_url()).unwrap(),
redis_client.clone(),
)));
let auth_service = Arc::new(tokio::sync::Mutex::new(auth::AuthService::new(
diesel::Connection::establish(&cfg.database_url()).unwrap(),
redis_client.clone(),
)));
let auth_service = Arc::new(tokio::sync::Mutex::new(auth::AuthService::new(
diesel::Connection::establish(&cfg.database_url()).unwrap(),
redis_client.clone(),
)));
// ----------------------------------------------------------------------
// Global application state
// ----------------------------------------------------------------------
let app_state = Arc::new(AppState {
// `s3_client` expects an `Option<aws_sdk_s3::Client>`.
s3_client: Some(drive.clone()),
config: Some(cfg.clone()),
conn: db_pool.clone(),
custom_conn: db_custom_pool.clone(),
redis_client: redis_client.clone(),
session_manager: session_manager.clone(),
tool_manager: tool_manager.clone(),
llm_provider: llm_provider.clone(),
auth_service: auth_service.clone(),
channels: Arc::new(Mutex::new({
let mut map = HashMap::new();
map.insert(
"web".to_string(),
web_adapter.clone() as Arc<dyn crate::channels::ChannelAdapter>,
);
map
})),
response_channels: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
web_adapter: web_adapter.clone(),
voice_adapter: voice_adapter.clone(),
whatsapp_adapter: whatsapp_adapter.clone(),
tool_api: tool_api.clone(),
});
// ----------------------------------------------------------------------
// Global application state
// ----------------------------------------------------------------------
let app_state = Arc::new(AppState {
// `s3_client` expects an `Option<aws_sdk_s3::Client>`.
s3_client: Some(drive.clone()),
config: Some(cfg.clone()),
conn: db_pool.clone(),
custom_conn: db_custom_pool.clone(),
redis_client: redis_client.clone(),
session_manager: session_manager.clone(),
tool_manager: tool_manager.clone(),
llm_provider: llm_provider.clone(),
auth_service: auth_service.clone(),
channels: Arc::new(Mutex::new({
let mut map = HashMap::new();
map.insert(
"web".to_string(),
web_adapter.clone() as Arc<dyn crate::channels::ChannelAdapter>,
);
map
})),
response_channels: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
web_adapter: web_adapter.clone(),
voice_adapter: voice_adapter.clone(),
whatsapp_adapter: whatsapp_adapter.clone(),
tool_api: tool_api.clone(),
});
// ----------------------------------------------------------------------
// Start HTTP server (multithreaded)
// ----------------------------------------------------------------------
info!(
"Starting server on {}:{}",
config.server.host, config.server.port
);
// ----------------------------------------------------------------------
// Start HTTP server (multithreaded)
// ----------------------------------------------------------------------
info!(
"Starting server on {}:{}",
config.server.host, config.server.port
);
// Determine the number of worker threads default to the number of logical CPUs,
// fallback to 4 if the information cannot be retrieved.
let worker_count = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
// Determine the number of worker threads default to the number of logical CPUs,
// fallback to 4 if the information cannot be retrieved.
let worker_count = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
HttpServer::new(move || {
// CORS configuration allow any origin/method/header (adjust for production).
let cors = Cors::default()
.allow_any_origin()
.allow_any_method()
.allow_any_header()
.max_age(3600);
// Start automation service in background
let automation_state = app_state.clone();
let app_state_clone = app_state.clone();
let mut app = App::new()
.wrap(cors)
.wrap(Logger::default())
.wrap(Logger::new("HTTP REQUEST: %a %{User-Agent}i"))
.app_data(web::Data::from(app_state_clone));
let automation = AutomationService::new(
automation_state,
"templates/announcements.gbai/announcements.gbdialog",
);
let _automation_handle = automation.spawn();
// Register all route handlers / services.
app = app
.service(upload_file)
.service(index)
.service(static_files)
.service(websocket_handler)
.service(auth_handler)
.service(whatsapp_webhook_verify)
.service(voice_start)
.service(voice_stop)
.service(create_session)
.service(get_sessions)
.service(start_session)
.service(get_session_history)
.service(set_mode_handler)
.service(chat_completions_local)
.service(embeddings_local);
HttpServer::new(move || {
// CORS configuration allow any origin/method/header (adjust for production).
let cors = Cors::default()
.allow_any_origin()
.allow_any_method()
.allow_any_header()
.max_age(3600);
#[cfg(feature = "email")]
{
let app_state_clone = app_state.clone();
let mut app = App::new()
.wrap(cors)
.wrap(Logger::default())
.wrap(Logger::new("HTTP REQUEST: %a %{User-Agent}i"))
.app_data(web::Data::from(app_state_clone));
// Register all route handlers / services.
app = app
.service(get_latest_email_from)
.service(get_emails)
.service(list_emails)
.service(send_email)
.service(save_draft)
.service(save_click);
}
.service(upload_file)
.service(index)
.service(static_files)
.service(websocket_handler)
.service(auth_handler)
.service(whatsapp_webhook_verify)
.service(voice_start)
.service(voice_stop)
.service(create_session)
.service(get_sessions)
.service(start_session)
.service(get_session_history)
.service(set_mode_handler)
.service(chat_completions_local)
.service(embeddings_local);
app
})
.workers(worker_count) // Enable multithreaded handling
.bind((config.server.host.clone(), config.server.port))?
.run()
.await
#[cfg(feature = "email")]
{
app = app
.service(get_latest_email_from)
.service(get_emails)
.service(list_emails)
.service(send_email)
.service(save_draft)
.service(save_click);
}
app
})
.workers(worker_count) // Enable multithreaded handling
.bind((config.server.host.clone(), config.server.port))?
.run()
.await
}

View file

@ -11,6 +11,7 @@ pub struct Organization {
pub slug: String,
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Queryable, Serialize, Deserialize)]
#[diesel(table_name = users)]
pub struct User {
@ -67,7 +68,6 @@ pub struct Automation {
pub kind: i32,
pub target: Option<String>,
pub schedule: Option<String>,
pub script_name: String,
pub param: String,
pub is_active: bool,
pub last_triggered: Option<chrono::DateTime<chrono::Utc>>,
@ -136,6 +136,17 @@ pub struct PaginationQuery {
pub page_size: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Identifiable, Insertable)]
#[diesel(table_name = bot_memories)]
pub struct BotMemory {
pub id: Uuid,
pub bot_id: Uuid,
pub key: String,
pub value: String,
pub created_at: chrono::DateTime<Utc>,
pub updated_at: chrono::DateTime<Utc>,
}
diesel::table! {
organizations (org_id) {
org_id -> Uuid,
@ -162,7 +173,6 @@ diesel::table! {
kind -> Int4,
target -> Nullable<Text>,
schedule -> Nullable<Text>,
script_name -> Text,
param -> Text,
is_active -> Bool,
last_triggered -> Nullable<Timestamptz>,
@ -216,3 +226,14 @@ diesel::table! {
updated_at -> Timestamptz,
}
}
diesel::table! {
bot_memories (id) {
id -> Uuid,
bot_id -> Uuid,
key -> Text,
value -> Text,
created_at -> Timestamptz,
updated_at -> Timestamptz,
}
}

View file

@ -0,0 +1,6 @@
let text = GET "default.gbdrive/default.pdf"
let resume = LLM "Build table resume with deadlines, dates and actions: " + text
SET_BOT_MEMORY "resume" resume