, dt.month, dt.hour, dt.is_weekend, etc.)

- Add startup wizard module for first-run configuration
- Add white-label branding system with .product file support
- Add bot manager for lifecycle, MinIO buckets, and templates
- Add version tracking registry for component updates
- Create comparison doc: BASIC vs n8n/Zapier/Make/Copilot
- Add WhatsApp-style sample dialogs to template documentation
- Add data traceability SVG diagram ```
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-11-30 15:07:29 -03:00
parent 635f3a7923
commit 48c1ae0b51
132 changed files with 27274 additions and 4858 deletions

View file

@ -71,6 +71,7 @@ compliance = ["dep:csv"]
attendance = []
directory = []
weba = []
timeseries = []
# ===== OPTIONAL INFRASTRUCTURE =====
redis-cache = ["dep:redis"]
@ -82,7 +83,7 @@ progress-bars = ["dep:indicatif"]
# ===== META FEATURES (BUNDLES) =====
full = [
"ui-server", "desktop", "console",
"vectordb", "llm", "nvidia",
"vectordb", "llm", "nvidia", "timeseries",
"email", "whatsapp", "instagram", "msteams",
"chat", "drive", "tasks", "calendar", "meet", "mail",
"compliance", "attendance", "directory", "weba",
@ -91,7 +92,7 @@ full = [
communications = ["email", "whatsapp", "instagram", "msteams", "chat", "redis-cache"]
productivity = ["chat", "drive", "tasks", "calendar", "meet", "mail", "redis-cache"]
enterprise = ["compliance", "attendance", "directory", "llm", "vectordb", "monitoring"]
enterprise = ["compliance", "attendance", "directory", "llm", "vectordb", "monitoring", "timeseries"]
minimal = ["ui-server", "chat"]
lightweight = ["ui-server", "chat", "drive", "tasks"]

View file

@ -0,0 +1,788 @@
# HEAR Keyword - Input Validation Reference
> Complete reference for HEAR keyword with automatic input validation in General Bots BASIC
## Overview
The `HEAR` keyword waits for user input with optional automatic validation. When using `HEAR AS <TYPE>`, the system will:
1. Wait for user input
2. Validate against the specified type
3. **Automatically retry** with a helpful error message if invalid
4. Return the normalized/parsed value once valid
This eliminates the need for manual validation loops and provides a consistent, user-friendly experience.
---
## Table of Contents
1. [Basic HEAR](#basic-hear)
2. [Text Validation Types](#text-validation-types)
3. [Numeric Types](#numeric-types)
4. [Date/Time Types](#datetime-types)
5. [Brazilian Document Types](#brazilian-document-types)
6. [Contact Types](#contact-types)
7. [Menu Selection](#menu-selection)
8. [Media Types](#media-types)
9. [Authentication Types](#authentication-types)
10. [Examples](#examples)
11. [Best Practices](#best-practices)
---
## Basic HEAR
```basic
' Simple HEAR without validation - accepts any input
HEAR response
TALK "You said: " + response
```
---
## Text Validation Types
### HEAR AS EMAIL
Validates email address format and normalizes to lowercase.
```basic
TALK "What's your email address?"
HEAR email AS EMAIL
TALK "We'll send confirmation to: " + email
```
**Validation:**
- Must contain `@` symbol
- Must have valid domain format
- Normalized to lowercase
**Error message:** "Please enter a valid email address (e.g., user@example.com)"
---
### HEAR AS NAME
Validates name format (letters, spaces, hyphens, apostrophes).
```basic
TALK "What's your full name?"
HEAR name AS NAME
TALK "Nice to meet you, " + name + "!"
```
**Validation:**
- Minimum 2 characters
- Maximum 100 characters
- Only letters, spaces, hyphens, apostrophes
- Auto-capitalizes first letter of each word
**Error message:** "Please enter a valid name (letters and spaces only)"
---
### HEAR AS URL
Validates and normalizes URL format.
```basic
TALK "Enter your website URL:"
HEAR website AS URL
TALK "I'll check " + website
```
**Validation:**
- Valid URL format
- Auto-adds `https://` if protocol missing
**Error message:** "Please enter a valid URL"
---
### HEAR AS PASSWORD
Validates password strength (minimum requirements).
```basic
TALK "Create a password (minimum 8 characters):"
HEAR password AS PASSWORD
' Returns "[PASSWORD SET]" - actual password stored securely
```
**Validation:**
- Minimum 8 characters
- Returns strength indicator (weak/medium/strong)
- Never echoes the actual password
**Error message:** "Password must be at least 8 characters"
---
### HEAR AS COLOR
Validates and normalizes color values.
```basic
TALK "Pick a color:"
HEAR color AS COLOR
TALK "You selected: " + color ' Returns hex format like #FF0000
```
**Accepts:**
- Named colors: "red", "blue", "green", etc.
- Hex format: "#FF0000" or "FF0000"
- RGB format: "rgb(255, 0, 0)"
**Returns:** Normalized hex format (#RRGGBB)
---
### HEAR AS UUID
Validates UUID/GUID format.
```basic
TALK "Enter the transaction ID:"
HEAR transaction_id AS UUID
```
---
## Numeric Types
### HEAR AS INTEGER
Validates and parses integer numbers.
```basic
TALK "How old are you?"
HEAR age AS INTEGER
TALK "In 10 years you'll be " + STR(age + 10)
```
**Validation:**
- Accepts whole numbers only
- Removes formatting (commas, spaces)
- Returns numeric value
**Error message:** "Please enter a valid whole number"
---
### HEAR AS FLOAT / DECIMAL
Validates and parses decimal numbers.
```basic
TALK "Enter the temperature:"
HEAR temperature AS FLOAT
TALK "Temperature is " + STR(temperature) + "°C"
```
**Validation:**
- Accepts decimal numbers
- Handles both `.` and `,` as decimal separator
- Returns numeric value rounded to 2 decimal places
---
### HEAR AS MONEY / CURRENCY / AMOUNT
Validates and normalizes monetary amounts.
```basic
TALK "How much would you like to transfer?"
HEAR amount AS MONEY
TALK "Transferring R$ " + FORMAT(amount, "#,##0.00")
```
**Accepts:**
- "100"
- "100.00"
- "1,234.56" (US format)
- "1.234,56" (Brazilian/European format)
- "R$ 100,00"
- "$100.00"
**Returns:** Normalized decimal value (e.g., "1234.56")
**Error message:** "Please enter a valid amount (e.g., 100.00 or R$ 100,00)"
---
### HEAR AS CREDITCARD / CARD
Validates credit card number using Luhn algorithm.
```basic
TALK "Enter your card number:"
HEAR card AS CREDITCARD
' Returns masked format: "4111 **** **** 1111"
```
**Validation:**
- 13-19 digits
- Passes Luhn checksum
- Detects card type (Visa, Mastercard, Amex, etc.)
**Returns:** Masked card number with metadata about card type
---
## Date/Time Types
### HEAR AS DATE
Validates and parses date input.
```basic
TALK "When is your birthday?"
HEAR birthday AS DATE
TALK "Your birthday is " + FORMAT(birthday, "MMMM d")
```
**Accepts multiple formats:**
- "25/12/2024" (DD/MM/YYYY)
- "12/25/2024" (MM/DD/YYYY)
- "2024-12-25" (ISO format)
- "25 Dec 2024"
- "December 25, 2024"
- "today", "tomorrow", "yesterday"
- "hoje", "amanhã", "ontem" (Portuguese)
**Returns:** Normalized ISO date (YYYY-MM-DD)
**Error message:** "Please enter a valid date (e.g., 25/12/2024 or 2024-12-25)"
---
### HEAR AS HOUR / TIME
Validates and parses time input.
```basic
TALK "What time should we schedule the meeting?"
HEAR meeting_time AS HOUR
TALK "Meeting scheduled for " + meeting_time
```
**Accepts:**
- "14:30" (24-hour format)
- "2:30 PM" (12-hour format)
- "14:30:00" (with seconds)
**Returns:** Normalized 24-hour format (HH:MM)
**Error message:** "Please enter a valid time (e.g., 14:30 or 2:30 PM)"
---
## Brazilian Document Types
### HEAR AS CPF
Validates Brazilian CPF (individual taxpayer ID).
```basic
TALK "Enter your CPF:"
HEAR cpf AS CPF
TALK "CPF validated: " + cpf ' Returns formatted: 123.456.789-09
```
**Validation:**
- 11 digits
- Valid check digits (mod 11 algorithm)
- Rejects known invalid patterns (all same digit)
**Returns:** Formatted CPF (XXX.XXX.XXX-XX)
**Error message:** "Please enter a valid CPF (11 digits)"
---
### HEAR AS CNPJ
Validates Brazilian CNPJ (company taxpayer ID).
```basic
TALK "Enter your company's CNPJ:"
HEAR cnpj AS CNPJ
TALK "CNPJ validated: " + cnpj ' Returns formatted: 12.345.678/0001-95
```
**Validation:**
- 14 digits
- Valid check digits
**Returns:** Formatted CNPJ (XX.XXX.XXX/XXXX-XX)
**Error message:** "Please enter a valid CNPJ (14 digits)"
---
## Contact Types
### HEAR AS MOBILE / PHONE / TELEPHONE
Validates phone number format.
```basic
TALK "What's your phone number?"
HEAR phone AS MOBILE
TALK "We'll send SMS to: " + phone
```
**Validation:**
- 10-15 digits
- Auto-formats based on detected country
**Returns:** Formatted phone number
**Error message:** "Please enter a valid mobile number"
---
### HEAR AS ZIPCODE / CEP / POSTALCODE
Validates postal code format.
```basic
TALK "What's your ZIP code?"
HEAR cep AS ZIPCODE
TALK "Your ZIP code is: " + cep
```
**Supports:**
- Brazilian CEP: 8 digits → "12345-678"
- US ZIP: 5 or 9 digits → "12345" or "12345-6789"
- UK postcode: alphanumeric → "SW1A 1AA"
**Returns:** Formatted postal code with country detection
---
## Menu Selection
### HEAR AS "Option1", "Option2", "Option3"
Presents a menu and validates selection.
```basic
TALK "Choose your preferred fruit:"
HEAR fruit AS "Apple", "Banana", "Orange", "Mango"
TALK "You selected: " + fruit
```
**Accepts:**
- Exact match: "Apple"
- Case-insensitive: "apple"
- Numeric selection: "1", "2", "3"
- Partial match: "app" → "Apple" (if unique)
**Automatically adds suggestions** for the menu options.
**Error message:** "Please select one of: Apple, Banana, Orange, Mango"
---
### HEAR AS BOOLEAN
Validates yes/no response.
```basic
TALK "Do you agree to the terms?"
HEAR agreed AS BOOLEAN
IF agreed THEN
TALK "Thank you for agreeing!"
ELSE
TALK "You must agree to continue."
END IF
```
**Accepts (true):** "yes", "y", "true", "1", "sim", "ok", "sure", "confirm"
**Accepts (false):** "no", "n", "false", "0", "não", "cancel", "deny"
**Returns:** "true" or "false" (with boolean metadata)
**Error message:** "Please answer yes or no"
---
### HEAR AS LANGUAGE
Validates language code or name.
```basic
TALK "What language do you prefer?"
HEAR language AS LANGUAGE
SET CONTEXT LANGUAGE language
TALK "Language set to: " + language
```
**Accepts:**
- ISO codes: "en", "pt", "es", "fr", "de"
- Full names: "English", "Portuguese", "Spanish"
- Native names: "Português", "Español", "Français"
**Returns:** ISO 639-1 language code
---
## Media Types
### HEAR AS IMAGE / PHOTO / PICTURE
Waits for image upload.
```basic
TALK "Please send a photo of your document:"
HEAR document_photo AS IMAGE
TALK "Image received: " + document_photo
' Returns URL to the uploaded image
```
**Validation:**
- Must receive image attachment
- Accepts: JPG, PNG, GIF, WebP
**Error message:** "Please send an image"
---
### HEAR AS QRCODE
Waits for image with QR code and reads it.
```basic
TALK "Send me a photo of the QR code:"
HEAR qr_data AS QRCODE
TALK "QR code contains: " + qr_data
```
**Process:**
1. Waits for image upload
2. Calls BotModels vision API to decode QR
3. Returns the decoded data
**Error message:** "Please send an image containing a QR code"
---
### HEAR AS AUDIO / VOICE / SOUND
Waits for audio input and transcribes to text.
```basic
TALK "Send me a voice message:"
HEAR transcription AS AUDIO
TALK "You said: " + transcription
```
**Process:**
1. Waits for audio attachment
2. Calls BotModels speech-to-text API
3. Returns transcribed text
**Error message:** "Please send an audio file or voice message"
---
### HEAR AS VIDEO
Waits for video upload and describes content.
```basic
TALK "Send a video of the problem:"
HEAR video_description AS VIDEO
TALK "I can see: " + video_description
```
**Process:**
1. Waits for video attachment
2. Calls BotModels vision API to describe
3. Returns AI-generated description
**Error message:** "Please send a video"
---
### HEAR AS FILE / DOCUMENT / DOC / PDF
Waits for document upload.
```basic
TALK "Please upload your contract:"
HEAR contract AS DOCUMENT
TALK "Document received: " + contract
```
**Accepts:** PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, TXT, CSV
**Returns:** URL to the uploaded file
---
## Authentication Types
### HEAR AS LOGIN
Waits for Active Directory/OAuth login completion.
```basic
TALK "Please click the link to authenticate:"
HEAR user AS LOGIN
TALK "Welcome, " + user.name + "!"
```
**Process:**
1. Generates authentication URL
2. Waits for OAuth callback
3. Returns user object with tokens
---
## Examples
### Complete Registration Flow
```basic
TALK "Let's create your account!"
TALK "What's your full name?"
HEAR name AS NAME
TALK "Enter your email address:"
HEAR email AS EMAIL
TALK "Enter your CPF:"
HEAR cpf AS CPF
TALK "What's your phone number?"
HEAR phone AS MOBILE
TALK "Choose a password:"
HEAR password AS PASSWORD
TALK "What's your birth date?"
HEAR birthdate AS DATE
TALK "Select your gender:"
HEAR gender AS "Male", "Female", "Other", "Prefer not to say"
' All inputs are now validated and normalized
TALK "Creating account for " + name + "..."
TABLE new_user
ROW name, email, cpf, phone, birthdate, gender, NOW()
END TABLE
SAVE "users.csv", new_user
TALK "✅ Account created successfully!"
```
### Payment Flow
```basic
TALK "💳 Let's process your payment"
TALK "Enter the amount:"
HEAR amount AS MONEY
IF amount < 1 THEN
TALK "Minimum payment is R$ 1.00"
RETURN
END IF
TALK "How would you like to pay?"
HEAR method AS "Credit Card", "Debit Card", "PIX", "Boleto"
IF method = "PIX" THEN
TALK "Enter the PIX key (phone, email, or CPF):"
' Note: We could create HEAR AS PIX_KEY if needed
HEAR pix_key
ELSEIF method = "Boleto" THEN
TALK "Enter the barcode (47-48 digits):"
HEAR barcode AS INTEGER
END IF
TALK "Confirm payment of R$ " + FORMAT(amount, "#,##0.00") + "?"
HEAR confirm AS BOOLEAN
IF confirm THEN
TALK "✅ Processing payment..."
ELSE
TALK "Payment cancelled."
END IF
```
### Customer Support with Media
```basic
TALK "How can I help you today?"
HEAR issue AS "Report a bug", "Request feature", "Billing question", "Other"
IF issue = "Report a bug" THEN
TALK "Please describe the problem:"
HEAR description
TALK "Can you send a screenshot of the issue?"
HEAR screenshot AS IMAGE
TALK "Thank you! We've logged your bug report."
TALK "Reference: BUG-" + FORMAT(NOW(), "yyyyMMddHHmmss")
ELSEIF issue = "Billing question" THEN
TALK "Please upload your invoice or send the transaction ID:"
HEAR reference
END IF
```
---
## Best Practices
### 1. Always Use Appropriate Types
```basic
' ❌ Bad - no validation
HEAR email
IF NOT email CONTAINS "@" THEN
TALK "Invalid email"
' Need to implement retry logic...
END IF
' ✅ Good - automatic validation and retry
HEAR email AS EMAIL
' Guaranteed to be valid when we get here
```
### 2. Combine with Context
```basic
SET CONTEXT "You are a helpful banking assistant.
When asking for monetary values, always confirm before processing."
TALK "How much would you like to withdraw?"
HEAR amount AS MONEY
' LLM and validation work together
```
### 3. Use Menu for Limited Options
```basic
' ❌ Bad - open-ended when options are known
HEAR payment_method
IF payment_method <> "credit" AND payment_method <> "debit" THEN
' Handle unknown input...
END IF
' ✅ Good - constrained to valid options
HEAR payment_method AS "Credit Card", "Debit Card", "PIX"
```
### 4. Provide Context Before HEAR
```basic
' ❌ Bad - no context
HEAR value AS MONEY
' ✅ Good - user knows what to enter
TALK "Enter the transfer amount (minimum R$ 1.00):"
HEAR amount AS MONEY
```
### 5. Use HEAR AS for Security-Sensitive Data
```basic
' CPF is automatically validated
HEAR cpf AS CPF
' Credit card passes Luhn check and is masked
HEAR card AS CREDITCARD
' Password never echoed back
HEAR password AS PASSWORD
```
---
## Error Handling
Validation errors are handled automatically, but you can customize:
```basic
' The system automatically retries up to 3 times
' After 3 failures, execution continues with empty value
' You can check if validation succeeded:
HEAR email AS EMAIL
IF email = "" THEN
TALK "Unable to validate email after multiple attempts."
TALK "Please contact support for assistance."
RETURN
END IF
```
---
## Metadata Access
Some validation types provide additional metadata:
```basic
HEAR card AS CREDITCARD
' card = "**** **** **** 1234"
' Metadata available: card_type, last_four
HEAR date AS DATE
' date = "2024-12-25"
' Metadata available: original input, parsed format
HEAR audio AS AUDIO
' audio = "transcribed text here"
' Metadata available: language, confidence
```
---
## Integration with BotModels
Media types (QRCODE, AUDIO, VIDEO) automatically call BotModels services:
| Type | BotModels Endpoint | Service |
|------|-------------------|---------|
| QRCODE | `/api/v1/vision/qrcode` | QR Code detection |
| AUDIO | `/api/v1/speech/to-text` | Whisper transcription |
| VIDEO | `/api/v1/vision/describe-video` | BLIP2 video description |
| IMAGE (with question) | `/api/v1/vision/vqa` | Visual Q&A |
Configure BotModels URL in `config.csv`:
```
botmodels-url,http://localhost:8001
botmodels-enabled,true
```
---
## Summary Table
| Type | Example Input | Normalized Output |
|------|---------------|-------------------|
| EMAIL | "User@Example.COM" | "user@example.com" |
| NAME | "john DOE" | "John Doe" |
| INTEGER | "1,234" | 1234 |
| MONEY | "R$ 1.234,56" | "1234.56" |
| DATE | "25/12/2024" | "2024-12-25" |
| HOUR | "2:30 PM" | "14:30" |
| BOOLEAN | "yes" / "sim" | "true" |
| CPF | "12345678909" | "123.456.789-09" |
| MOBILE | "11999998888" | "(11) 99999-8888" |
| CREDITCARD | "4111111111111111" | "4111 **** **** 1111" |
| QRCODE | [image] | "decoded QR data" |
| AUDIO | [audio file] | "transcribed text" |
---
*HEAR AS validation - Making input handling simple, secure, and user-friendly.*

File diff suppressed because it is too large Load diff

View file

@ -91,12 +91,13 @@
- [IS NUMERIC](./chapter-06-gbdialog/keyword-is-numeric.md)
- [SWITCH](./chapter-06-gbdialog/keyword-switch.md)
- [WEBHOOK](./chapter-06-gbdialog/keyword-webhook.md)
- [TABLE](./chapter-06-gbdialog/keyword-table.md)
- [HTTP & API Operations](./chapter-06-gbdialog/keywords-http.md)
- [POST](./chapter-06-gbdialog/keyword-post.md)
- [PUT](./chapter-06-gbdialog/keyword-put.md)
- [PATCH](./chapter-06-gbdialog/keyword-patch.md)
- [DELETE_HTTP](./chapter-06-gbdialog/keyword-delete-http.md)
- [SET_HEADER](./chapter-06-gbdialog/keyword-set-header.md)
- [DELETE HTTP](./chapter-06-gbdialog/keyword-delete-http.md)
- [SET HEADER](./chapter-06-gbdialog/keyword-set-header.md)
- [GRAPHQL](./chapter-06-gbdialog/keyword-graphql.md)
- [SOAP](./chapter-06-gbdialog/keyword-soap.md)
- [Data Operations](./chapter-06-gbdialog/keywords-data.md)
@ -111,11 +112,11 @@
- [AGGREGATE](./chapter-06-gbdialog/keyword-aggregate.md)
- [JOIN](./chapter-06-gbdialog/keyword-join.md)
- [PIVOT](./chapter-06-gbdialog/keyword-pivot.md)
- [GROUP_BY](./chapter-06-gbdialog/keyword-group-by.md)
- [GROUP BY](./chapter-06-gbdialog/keyword-group-by.md)
- [File Operations](./chapter-06-gbdialog/keywords-file.md)
- [READ](./chapter-06-gbdialog/keyword-read.md)
- [WRITE](./chapter-06-gbdialog/keyword-write.md)
- [DELETE_FILE](./chapter-06-gbdialog/keyword-delete-file.md)
- [DELETE FILE](./chapter-06-gbdialog/keyword-delete-file.md)
- [COPY](./chapter-06-gbdialog/keyword-copy.md)
- [MOVE](./chapter-06-gbdialog/keyword-move.md)
- [LIST](./chapter-06-gbdialog/keyword-list.md)
@ -123,8 +124,8 @@
- [EXTRACT](./chapter-06-gbdialog/keyword-extract.md)
- [UPLOAD](./chapter-06-gbdialog/keyword-upload.md)
- [DOWNLOAD](./chapter-06-gbdialog/keyword-download.md)
- [GENERATE_PDF](./chapter-06-gbdialog/keyword-generate-pdf.md)
- [MERGE_PDF](./chapter-06-gbdialog/keyword-merge-pdf.md)
- [GENERATE PDF](./chapter-06-gbdialog/keyword-generate-pdf.md)
- [MERGE PDF](./chapter-06-gbdialog/keyword-merge-pdf.md)
# Part VII - Extending General Bots
@ -158,6 +159,7 @@
- [Tool Format](./chapter-09-api/openai-format.md)
- [GET Keyword Integration](./chapter-09-api/get-integration.md)
- [External APIs](./chapter-09-api/external-apis.md)
- [LLM REST Server](./chapter-09-api/llm-rest-server.md)
- [NVIDIA GPU Setup for LXC](./chapter-09-api/nvidia-gpu-setup.md)
@ -190,6 +192,7 @@
# Part X - Feature Deep Dive
- [Chapter 11: Feature Reference](./chapter-11-features/README.md)
- [Feature Editions](./chapter-11-features/editions.md)
- [Core Features](./chapter-11-features/core-features.md)
- [Conversation Management](./chapter-11-features/conversation.md)
- [AI and LLM](./chapter-11-features/ai-llm.md)

View file

@ -334,6 +334,13 @@ The question is: what will you create?
Continue to [BASIC Keywords Reference](./keywords.md) when you're ready for the complete reference.
### Additional Documentation
- [Script Execution Flow](./script-execution-flow.md) - Entry points, config injection, and lifecycle
- [Prompt Blocks](./prompt-blocks.md) - BEGIN SYSTEM PROMPT & BEGIN TALK documentation
- [Keywords Reference](./keywords.md) - Complete keyword list
- [Basics](./basics.md) - Core concepts for LLM-first development
---
<div align="center">

View file

@ -1 +1 @@
# DELETE_FILE
# DELETE FILE

View file

@ -1 +1 @@
# DELETE_HTTP
# DELETE HTTP

View file

@ -1 +1 @@
# GENERATE_PDF
# GENERATE PDF

View file

@ -1 +1 @@
# GROUP_BY
# GROUP BY

View file

@ -1 +1 @@
# MERGE_PDF
# MERGE PDF

View file

@ -1 +1 @@
# SET_HEADER
# SET HEADER

View file

@ -0,0 +1,123 @@
# SYNCHRONIZE
Synchronizes data from an external API endpoint to a local database table with automatic pagination.
## Status
⚠️ **Planned Feature** - This keyword is documented for the Bling ERP integration template but implementation is pending.
## Syntax
```basic
SYNCHRONIZE endpoint, tableName, keyField, pageParam, limitParam
```
## Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `endpoint` | String | API endpoint path (appended to `host` variable) |
| `tableName` | String | Target table name (with optional connection prefix) |
| `keyField` | String | Primary key field for upsert operations |
| `pageParam` | String | Name of the pagination parameter in API |
| `limitParam` | String | Name of the limit parameter in API |
## Description
`SYNCHRONIZE` provides a high-level abstraction for syncing paginated API data to a database table. It:
1. Iterates through all pages of the API endpoint
2. Fetches data using the configured `host`, `limit`, and `pages` variables
3. Performs upsert (merge) operations on the target table
4. Tracks statistics in the `REPORT` variable
5. Handles rate limiting automatically
## Prerequisites
The following variables must be defined (typically via `config.csv` param-* entries):
```csv
name,value
param-host,https://api.example.com/v1
param-limit,100
param-pages,50
```
## Example
```basic
' Sync categories from ERP
pageVariable = "pagina"
limitVariable = "limite"
SEND EMAIL admin, "Syncing categories..."
SYNCHRONIZE /categorias/receitas-despesas, maria.CategoriaReceita, Id, pageVariable, limitVariable
SEND EMAIL admin, REPORT
RESET REPORT
' Sync payment methods
SYNCHRONIZE /formas-pagamentos, maria.FormaDePagamento, Id, pageVariable, limitVariable
SEND EMAIL admin, REPORT
RESET REPORT
```
## Equivalent Manual Implementation
Until `SYNCHRONIZE` is implemented, use this pattern:
```basic
' Manual sync equivalent
pageVariable = "pagina"
limitVariable = "limite"
tableName = "maria.CategoriaReceita"
endpoint = "/categorias/receitas-despesas"
page = 1
totalSynced = 0
DO WHILE page > 0 AND page <= pages
url = host + endpoint + "?" + pageVariable + "=" + page + "&" + limitVariable + "=" + limit
res = GET url
WAIT 0.33 ' Rate limiting
IF res.data AND UBOUND(res.data) > 0 THEN
MERGE tableName WITH res.data BY "Id"
totalSynced = totalSynced + UBOUND(res.data)
page = page + 1
IF UBOUND(res.data) < limit THEN
page = 0 ' Last page
END IF
ELSE
page = 0 ' No more data
END IF
LOOP
TALK "Synced " + totalSynced + " records to " + tableName
```
## Used In
- [Bling ERP Template](../../../templates/bling.gbai/) - ERP synchronization scripts
## Related Keywords
- [GET](./keyword-get.md) - HTTP GET requests
- [MERGE](./keyword-merge.md) - Upsert data operations
- [SET SCHEDULE](./keyword-set-schedule.md) - Schedule sync jobs
- [REPORT / RESET REPORT](./keyword-report.md) - Sync statistics
## Implementation Notes
When implemented, `SYNCHRONIZE` should:
1. Use the global `host`, `limit`, `pages` variables from config
2. Support connection prefixes (e.g., `maria.TableName`)
3. Handle API errors gracefully with retry logic
4. Update the `REPORT` variable with sync statistics
5. Support both REST JSON responses and paginated arrays
## See Also
- [Script Execution Flow](./script-execution-flow.md) - How config variables are injected
- [Data Operations](./keywords-data.md) - Data manipulation keywords

View file

@ -0,0 +1,223 @@
# TABLE Keyword
The `TABLE` keyword defines database tables directly in your `.bas` files. Tables are automatically created on the specified database connection when the script is compiled.
## Syntax
```bas
TABLE TableName ON connection
FieldName dataType[(length[,precision])] [key] [references OtherTable]
...
END TABLE
```
## Parameters
| Parameter | Description |
|-----------|-------------|
| `TableName` | Name of the table to create |
| `connection` | Connection name defined in config.csv (e.g., `maria`, `sales_db`) |
| `FieldName` | Name of the field/column |
| `dataType` | Data type (see supported types below) |
| `length` | Optional length for string/number types |
| `precision` | Optional decimal precision for number types |
| `key` | Marks field as primary key |
| `references` | Creates a foreign key reference to another table |
## Supported Data Types
| Type | Description | SQL Mapping |
|------|-------------|-------------|
| `string(n)` | Variable-length string | VARCHAR(n) |
| `number` | Integer | INTEGER |
| `number(n)` | Big integer | BIGINT |
| `number(n,p)` | Decimal with precision | DECIMAL(n,p) |
| `integer` | Integer | INTEGER |
| `double` | Double precision float | DOUBLE PRECISION |
| `double(n,p)` | Decimal | DECIMAL(n,p) |
| `date` | Date only | DATE |
| `datetime` | Date and time | TIMESTAMP/DATETIME |
| `boolean` | True/false | BOOLEAN |
| `text` | Long text | TEXT |
| `guid` | UUID | UUID/CHAR(36) |
## Connection Configuration
External database connections are configured in `config.csv` with the following format:
| Key | Description |
|-----|-------------|
| `conn-{name}-Server` | Database server hostname or IP |
| `conn-{name}-Name` | Database name |
| `conn-{name}-Username` | Username for authentication |
| `conn-{name}-Password` | Password for authentication |
| `conn-{name}-Port` | Port number (optional, uses default) |
| `conn-{name}-Driver` | Database driver: `mysql`, `mariadb`, `postgres`, `mssql` |
### Example config.csv
```csv
conn-maria-Server,192.168.1.100
conn-maria-Name,sales_database
conn-maria-Username,app_user
conn-maria-Password,secure_password
conn-maria-Port,3306
conn-maria-Driver,mariadb
```
## Examples
### Basic Table Definition
```bas
TABLE Contacts ON maria
Id number key
Nome string(150)
Email string(255)
Telefone string(20)
DataCadastro date
END TABLE
```
### Table with Multiple Field Types
```bas
TABLE Produtos ON maria
Id number key
Nome string(150)
Sku string(20)
Preco double(10,2)
Estoque integer
Ativo boolean
DescricaoCurta string(4000)
DataValidade date
Categoria_id integer
END TABLE
```
### Table with Foreign Key References
```bas
TABLE Pedidos ON maria
Id number key
Numero integer
Data date
Total double(15,2)
Contato_id number
Situacao_id integer
Vendedor_id number
END TABLE
TABLE PedidosItem ON maria
Id number key
Pedido_id number
Produto_id number
Quantidade integer
Valor double(10,2)
Desconto double(5,2)
END TABLE
```
### Complete CRM Tables Example
```bas
' Contact management tables
TABLE Contatos ON maria
Id number key
Nome string(150)
Codigo string(50)
Situacao string(5)
NumeroDocumento string(25)
Telefone string(20)
Celular string(20)
Email string(50)
Endereco_geral_endereco string(100)
Endereco_geral_cep string(10)
Endereco_geral_bairro string(50)
Endereco_geral_municipio string(50)
Endereco_geral_uf string(5)
Vendedor_id number
DadosAdicionais_dataNascimento date
Financeiro_limiteCredito double
END TABLE
' Payment methods
TABLE FormaDePagamento ON maria
Id number key
Descricao string(255)
TipoPagamento integer
Situacao integer
Padrao integer
Taxas_aliquota double
Taxas_valor double
END TABLE
' Accounts receivable
TABLE ContasAReceber ON maria
Id number key
Situacao integer
Vencimento date
Valor double
Contato_id number
FormaPagamento_id number
Saldo double
DataEmissao date
NumeroDocumento string(50)
END TABLE
```
## Using Tables After Creation
Once tables are defined, you can use standard BASIC keywords to work with the data:
### Inserting Data
```bas
data = NEW OBJECT
data.Nome = "João Silva"
data.Email = "joao@example.com"
data.Telefone = "11999999999"
INSERT "Contatos", data
```
### Finding Data
```bas
contacts = FIND "Contatos", "Situacao='A'"
FOR EACH contact IN contacts
TALK "Name: " + contact.Nome
NEXT
```
### Updating Data
```bas
UPDATE "Contatos", "Id=123", "Telefone='11988888888'"
```
### Deleting Data
```bas
DELETE "Contatos", "Id=123"
```
## Notes
1. **Automatic Table Creation**: Tables are created automatically when the `.bas` file is compiled. If the table already exists, no changes are made.
2. **Connection Required**: The connection name must be configured in `config.csv` before using it in TABLE definitions.
3. **Primary Keys**: Fields marked with `key` become the primary key. Multiple fields can be marked as key for composite primary keys.
4. **Default Connection**: If `ON connection` is omitted, the table is created on the default (internal) PostgreSQL database.
5. **SQL Injection Protection**: All identifiers are sanitized to prevent SQL injection attacks.
## See Also
- [FIND](./keyword-find.md) - Query data from tables
- [SAVE](./keyword-save.md) - Insert or update data
- [INSERT](./keyword-insert.md) - Insert new records
- [UPDATE](./keyword-update.md) - Update existing records
- [DELETE](./keyword-delete.md) - Delete records
- [config.csv](../chapter-08-config/config-csv.md) - Connection configuration

View file

@ -9,6 +9,43 @@ This section lists every BASIC keyword implemented in the GeneralBots engine. Ea
The source code for each keyword lives in `src/basic/keywords/`. Only the keywords listed here exist in the system.
## Important: Case Insensitivity
**All variables in General Bots BASIC are case-insensitive.** The preprocessor normalizes variable names to lowercase automatically.
```basic
' These all refer to the same variable
host = "https://api.example.com"
result = GET Host + "/endpoint"
TALK HOST
```
Keywords are also case-insensitive but conventionally written in UPPERCASE:
```basic
' Both work identically
TALK "Hello"
talk "Hello"
```
## Configuration Variables (param-*)
Variables defined with `param-` prefix in `config.csv` are automatically available in scripts without the prefix:
```csv
name,value
param-host,https://api.example.com
param-limit,100
param-pages,50
```
```basic
' Access directly (lowercase, no param- prefix)
result = GET host + "/items?limit=" + limit
```
See [Script Execution Flow](./script-execution-flow.md) for complete details.
---
## Complete Keyword List (Flat Reference)
@ -78,6 +115,7 @@ The source code for each keyword lives in `src/basic/keywords/`. Only the keywor
| `SET USER` | Session | Set user context |
| `SOAP` | HTTP | Execute SOAP API call |
| `SWITCH ... CASE ... END SWITCH` | Control | Switch statement |
| `SYNCHRONIZE` | Data | Sync API data to table (planned) |
| `TALK` | Dialog | Send message to user |
| `UPDATE` | Data | Update existing records |
| `UPLOAD` | Files | Upload file to storage |
@ -162,6 +200,7 @@ The source code for each keyword lives in `src/basic/keywords/`. Only the keywor
| JOIN | `result = JOIN left, right, "key"` | Join datasets |
| PIVOT | `result = PIVOT data, "row", "value"` | Create pivot table |
| GROUP BY | `result = GROUP BY data, "field"` | Group data |
| SYNCHRONIZE | `SYNCHRONIZE endpoint, table, key, pageVar, limitVar` | Sync API to table |
| MAP | `result = MAP data, "old->new"` | Map field names |
| FILL | `result = FILL data, template` | Fill template |
| FIRST | `result = FIRST collection` | Get first element |
@ -205,8 +244,10 @@ The source code for each keyword lives in `src/basic/keywords/`. Only the keywor
| IF...THEN...ELSE | `IF condition THEN ... ELSE ... END IF` | Conditional |
| FOR EACH...NEXT | `FOR EACH item IN collection ... NEXT item` | Loop |
| EXIT FOR | `EXIT FOR` | Exit loop early |
| WHILE...WEND | `WHILE condition ... WEND` | While loop |
| SWITCH...CASE | `SWITCH value CASE x ... END SWITCH` | Switch statement |
| `WHILE...WEND` | `WHILE condition ... WEND` | While loop |
| `SWITCH...CASE` | `SWITCH value CASE x ... END SWITCH` | Switch statement |
| `REPORT` | `SEND EMAIL admin, REPORT` | Access sync statistics |
| `RESET REPORT` | `RESET REPORT` | Clear sync statistics |
### Events & Scheduling
@ -273,11 +314,90 @@ HEAR value AS STRING ' Better - validates input type
---
## Prompt Blocks
Special multi-line blocks for AI configuration and formatted output:
| Block | Purpose | Documentation |
|-------|---------|---------------|
| `BEGIN SYSTEM PROMPT ... END SYSTEM PROMPT` | Define AI persona, rules, capabilities | [Prompt Blocks](./prompt-blocks.md) |
| `BEGIN TALK ... END TALK` | Formatted multi-line messages with Markdown | [Prompt Blocks](./prompt-blocks.md) |
```basic
BEGIN SYSTEM PROMPT
You are a helpful assistant for AcmeStore.
Rules:
1. Always be polite
2. Never discuss competitors
END SYSTEM PROMPT
BEGIN TALK
**Welcome!** 🎉
I can help you with:
• Orders
• Tracking
• Returns
END TALK
```
---
## Script Structure
### No MAIN Function
Scripts execute from line 1 - no `MAIN` or entry point needed:
```basic
' ✅ CORRECT - Start directly
TALK "Hello!"
ADD TOOL "my-tool"
' ❌ WRONG - Don't use MAIN
SUB MAIN()
TALK "Hello"
END SUB
```
### SUB and FUNCTION for Reuse
Use for helper code within tools, not as entry points:
```basic
FUNCTION CalculateTotal(price, quantity)
RETURN price * quantity
END FUNCTION
SUB NotifyAdmin(message)
SEND EMAIL admin1, message
END SUB
' Execution starts here
total = CalculateTotal(19.99, 3)
CALL NotifyAdmin("Order processed")
```
See [Script Execution Flow](./script-execution-flow.md) for entry points and lifecycle.
---
## Notes
- Keywords are case-insensitive (TALK = talk = Talk)
- Variables are case-insensitive (host = HOST = Host)
- String parameters can use double quotes or single quotes
- Comments start with REM or '
- Line continuation uses underscore (_)
- Objects are created with `#{ key: value }` syntax
- Arrays use `[item1, item2, ...]` syntax
- Arrays use `[item1, item2, ...]` syntax
- param-* config values become global variables
---
## See Also
- [Script Execution Flow](./script-execution-flow.md) - Entry points and lifecycle
- [Prompt Blocks](./prompt-blocks.md) - BEGIN SYSTEM PROMPT & BEGIN TALK
- [Basics](./basics.md) - Core concepts
- [Examples](./examples-consolidated.md) - Real-world patterns

View file

@ -0,0 +1,476 @@
# Prompt Blocks: BEGIN SYSTEM PROMPT & BEGIN TALK
Prompt blocks are special multi-line constructs in General Bots BASIC that define AI behavior and formatted user messages. Unlike regular keywords, these blocks preserve formatting, line breaks, and support rich content.
## Overview
| Block | Purpose | When Processed |
|-------|---------|----------------|
| `BEGIN SYSTEM PROMPT` | Define AI personality, rules, and capabilities | Bot initialization |
| `BEGIN TALK` | Display formatted multi-line messages | Runtime |
---
## BEGIN SYSTEM PROMPT / END SYSTEM PROMPT
Defines the AI's behavior, personality, constraints, and available capabilities. This is the "instruction manual" for the LLM.
### Syntax
```basic
BEGIN SYSTEM PROMPT
Your system prompt content here.
Multiple lines are supported.
Formatting is preserved.
END SYSTEM PROMPT
```
### Purpose
The system prompt:
- Sets the AI's persona and tone
- Defines rules and constraints
- Lists available tools and capabilities
- Specifies response formats
- Provides domain knowledge
### Complete Example
```basic
' start.bas - with comprehensive system prompt
ADD TOOL "create-order"
ADD TOOL "track-shipment"
ADD TOOL "lookup-product"
USE KB "products"
USE KB "policies"
BEGIN SYSTEM PROMPT
You are a helpful e-commerce assistant for AcmeStore.
## Your Persona
- Friendly but professional
- Patient with confused customers
- Proactive in offering help
## Your Capabilities
You have access to these tools:
- create-order: Create new orders for customers
- track-shipment: Track order shipments by order ID
- lookup-product: Search product catalog
## Rules
1. Always greet customers warmly
2. Never discuss competitor products
3. For refunds, collect order number first
4. Prices are in USD unless customer specifies otherwise
5. If unsure, ask clarifying questions rather than guessing
## Response Format
- Keep responses under 100 words unless detailed explanation needed
- Use bullet points for lists
- Include relevant product links when available
## Escalation
If customer is frustrated or issue is complex, offer to connect with human support.
## Knowledge
You have access to:
- Complete product catalog (products KB)
- Return and shipping policies (policies KB)
- Current promotions and discounts
END SYSTEM PROMPT
' Continue with welcome message...
```
### Best Practices
#### DO ✅
```basic
BEGIN SYSTEM PROMPT
You are a medical appointment scheduler.
Available tools:
- book-appointment: Schedule new appointments
- cancel-appointment: Cancel existing appointments
- check-availability: View available time slots
Rules:
1. Always confirm patient identity before accessing records
2. Appointments require 24-hour advance notice
3. Emergency cases should be directed to call 911
You can access patient records and doctor schedules through the connected systems.
END SYSTEM PROMPT
```
#### DON'T ❌
```basic
' Too vague - LLM won't know how to behave
BEGIN SYSTEM PROMPT
You are a helpful assistant.
END SYSTEM PROMPT
' Better to be specific about capabilities and constraints
```
### Placement
Place `BEGIN SYSTEM PROMPT` near the top of `start.bas`, after tool and KB registration:
```basic
' 1. Register tools first
ADD TOOL "my-tool"
' 2. Load knowledge bases
USE KB "my-kb"
' 3. Then define system prompt
BEGIN SYSTEM PROMPT
...
END SYSTEM PROMPT
' 4. Finally, welcome message
BEGIN TALK
...
END TALK
```
---
## BEGIN TALK / END TALK
Displays formatted multi-line messages to users with preserved formatting, Markdown support, and emoji rendering.
### Syntax
```basic
BEGIN TALK
Your message content here.
Multiple lines supported.
**Markdown** formatting works.
Emojis render: 🎉 ✅ 📦
END TALK
```
### Purpose
Use `BEGIN TALK` for:
- Welcome messages
- Formatted instructions
- Multi-line responses
- Messages with bullet points or structure
- Content with emojis or special formatting
### Basic Example
```basic
BEGIN TALK
**Welcome to AcmeStore!** 🛒
I can help you with:
• Browsing products
• Placing orders
• Tracking shipments
• Returns and refunds
What would you like to do today?
END TALK
```
### Markdown Support
`BEGIN TALK` supports common Markdown:
```basic
BEGIN TALK
# Main Heading
## Section Heading
**Bold text** and *italic text*
- Bullet point 1
- Bullet point 2
- Bullet point 3
1. Numbered item
2. Another item
> Quoted text block
`inline code`
---
[Link text](https://example.com)
END TALK
```
### Dynamic Content
Combine with variables for dynamic messages:
```basic
customerName = "John"
orderCount = 5
BEGIN TALK
Hello, **${customerName}**! 👋
You have ${orderCount} orders in your history.
What would you like to do?
END TALK
```
### Comparison: TALK vs BEGIN TALK
| Feature | `TALK` | `BEGIN TALK` |
|---------|--------|--------------|
| Single line | ✅ | ❌ |
| Multiple lines | Concatenate with + | ✅ Native |
| Formatting preserved | ❌ | ✅ |
| Markdown | Limited | ✅ Full |
| Emojis | ✅ | ✅ |
| Variables | `TALK "Hi " + name` | `${name}` |
```basic
' Simple messages - use TALK
TALK "Hello!"
TALK "Your order is: " + orderId
' Complex formatted messages - use BEGIN TALK
BEGIN TALK
**Order Confirmation** ✅
Order ID: ${orderId}
Total: $${total}
Thank you for shopping with us!
END TALK
```
---
## Real-World Examples
### Customer Service Bot
```basic
' start.bas
ADD TOOL "create-ticket"
ADD TOOL "check-status"
ADD TOOL "escalate"
USE KB "faq"
USE KB "troubleshooting"
BEGIN SYSTEM PROMPT
You are a customer service representative for TechSupport Inc.
## Persona
- Empathetic and patient
- Solution-oriented
- Professional but warm
## Available Tools
- create-ticket: Create support tickets for issues
- check-status: Check existing ticket status
- escalate: Escalate to human agent
## Workflow
1. Greet customer and understand their issue
2. Check if issue is in FAQ/troubleshooting KB
3. If solvable, provide solution
4. If not, create ticket or escalate
## Rules
- Never share internal system information
- Always provide ticket numbers for reference
- Offer escalation if customer requests human help
- Response time SLA: acknowledge within 30 seconds
## Escalation Triggers
- Customer explicitly requests human
- Issue unresolved after 3 exchanges
- Billing disputes over $100
- Account security concerns
END SYSTEM PROMPT
CLEAR SUGGESTIONS
ADD SUGGESTION "Technical Issue"
ADD SUGGESTION "Billing Question"
ADD SUGGESTION "Account Help"
BEGIN TALK
**Welcome to TechSupport!** 🛠️
I'm here to help you with:
• Technical issues and troubleshooting
• Billing and account questions
• Product information
I can also connect you with a human agent if needed.
How can I assist you today?
END TALK
```
### E-commerce Bot
```basic
' start.bas
ADD TOOL "search-products"
ADD TOOL "add-to-cart"
ADD TOOL "checkout"
ADD TOOL "track-order"
USE KB "catalog"
USE KB "promotions"
BEGIN SYSTEM PROMPT
You are a shopping assistant for FashionMart.
## Capabilities
- search-products: Find products by name, category, or description
- add-to-cart: Add items to shopping cart
- checkout: Process payment and create order
- track-order: Track shipment status
## Knowledge
- Full product catalog with prices and availability
- Current promotions and discount codes
- Shipping policies and delivery times
## Sales Approach
- Be helpful, not pushy
- Mention relevant promotions naturally
- Suggest complementary products when appropriate
- Always confirm before checkout
## Constraints
- Cannot modify prices or create custom discounts
- Returns handled through separate process
- Cannot access customer payment details directly
## Response Style
- Use product images when available
- Include prices in responses
- Mention stock levels for popular items
END SYSTEM PROMPT
BEGIN TALK
**Welcome to FashionMart!** 👗👔
🔥 **Today's Deals:**
• 20% off summer collection
• Free shipping on orders over $50
I can help you:
• Find the perfect outfit
• Check sizes and availability
• Track your orders
What are you looking for today?
END TALK
```
### Data Sync Bot (Scheduled)
```basic
' sync.bas - No welcome needed, runs on schedule
SET SCHEDULE "0 0 6 * * *" ' Daily at 6 AM
BEGIN SYSTEM PROMPT
You are a data synchronization agent.
## Purpose
Sync product data from external ERP to local database.
## Process
1. Fetch products from ERP API
2. Compare with local database
3. Update changed records
4. Report statistics
## Error Handling
- Log all errors
- Continue processing on individual failures
- Send summary email to admin
## No user interaction required.
END SYSTEM PROMPT
' No BEGIN TALK needed - this is automated
SEND EMAIL admin1, "Starting daily sync..."
' ... sync logic ...
SEND EMAIL admin1, "Sync complete: " + REPORT
```
---
## Common Patterns
### Role-Based Prompts
```basic
role = GET USER "role"
SWITCH role
CASE "admin"
BEGIN SYSTEM PROMPT
You are an admin assistant with full system access.
You can manage users, view logs, and modify settings.
END SYSTEM PROMPT
CASE "customer"
BEGIN SYSTEM PROMPT
You are a customer service assistant.
You can help with orders and general questions.
END SYSTEM PROMPT
DEFAULT
BEGIN SYSTEM PROMPT
You are a general assistant with limited access.
END SYSTEM PROMPT
END SWITCH
```
### Conditional Welcome
```basic
hour = HOUR(NOW)
IF hour < 12 THEN
greeting = "Good morning"
ELSE IF hour < 18 THEN
greeting = "Good afternoon"
ELSE
greeting = "Good evening"
END IF
BEGIN TALK
**${greeting}!** 👋
Welcome to our service. How can I help you today?
END TALK
```
---
## See Also
- [SET CONTEXT](./keyword-set-context.md) - Dynamic context setting
- [TALK](./keyword-talk.md) - Simple message output
- [Script Execution Flow](./script-execution-flow.md) - Execution lifecycle
- [Tools System](./keyword-use-tool.md) - Tool registration

View file

@ -0,0 +1,508 @@
# Script Execution Flow & Entry Points
Understanding how General Bots BASIC scripts are loaded, compiled, and executed is essential for building effective automation. This document covers the complete execution lifecycle.
## Execution Entry Points
Scripts in General Bots can be triggered through several entry points:
```
┌─────────────────────────────────────────────────────────────────┐
│ SCRIPT ENTRY POINTS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ start.bas │ │ SET SCHEDULE│ │ WEBHOOK │ │
│ │ (Bot Start) │ │ (Cron Jobs) │ │ (HTTP POST) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ BASIC COMPILER & RUNTIME │ │
│ │ 1. Load config.csv param-* variables │ │
│ │ 2. Preprocess (case normalization, syntax transform) │ │
│ │ 3. Compile to AST │ │
│ │ 4. Execute with injected scope │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ LLM Tools │ │ ON Events │ │ API Calls │ │
│ │ (ADD TOOL) │ │ (Triggers) │ │ (External) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### 1. Bot Startup (`start.bas`)
The primary entry point. Executed when a bot initializes or a conversation begins.
```basic
' start.bas - Primary entry point
' NO MAIN function needed - execution starts at line 1
' 1. Register tools for LLM to use
ADD TOOL "create-order"
ADD TOOL "track-shipment"
ADD TOOL "customer-lookup"
' 2. Load knowledge bases
USE KB "products"
USE KB "policies"
' 3. Set AI context/personality
BEGIN SYSTEM PROMPT
You are a helpful e-commerce assistant for AcmeStore.
You can help customers with orders, tracking, and product questions.
Always be friendly and professional.
END SYSTEM PROMPT
' 4. Setup UI suggestions
CLEAR SUGGESTIONS
ADD SUGGESTION "New Order"
ADD SUGGESTION "Track Package"
ADD SUGGESTION "Contact Support"
' 5. Welcome message
BEGIN TALK
**Welcome to AcmeStore!** 🛒
I can help you:
• Browse and order products
• Track your shipments
• Answer questions
What would you like to do?
END TALK
```
### 2. Scheduled Execution (`SET SCHEDULE`)
Scripts can run on a cron schedule without user interaction.
```basic
' sync-data.bas - Runs automatically on schedule
SET SCHEDULE "0 0 */4 * * *" ' Every 4 hours
' Variables from config.csv are available
' param-host -> host, param-limit -> limit, param-pages -> pages
SEND EMAIL admin1, "Data sync started..."
page = 1
DO WHILE page > 0 AND page < pages
res = GET host + "/products?page=" + page + "&limit=" + limit
IF res.data THEN
MERGE "products" WITH res.data BY "id"
page = page + 1
ELSE
page = 0
END IF
WAIT 0.5 ' Rate limiting
LOOP
SEND EMAIL admin1, "Sync complete! " + REPORT
RESET REPORT
```
**Cron Format:** `second minute hour day month weekday`
| Pattern | Description |
|---------|-------------|
| `0 0 8 * * *` | Daily at 8:00 AM |
| `0 30 22 * * *` | Daily at 10:30 PM |
| `0 0 0 */2 * *` | Every 2 days at midnight |
| `0 0 * * * *` | Every hour |
| `0 */15 * * * *` | Every 15 minutes |
### 3. Webhook Entry (`WEBHOOK`)
Scripts exposed as HTTP endpoints for external integrations.
```basic
' order-webhook.bas - HTTP endpoint
WEBHOOK "order-received"
' Creates: POST /api/bot/{botname}/order-received
' Parameters become variables automatically
' Access webhook parameters
orderId = GET TOOL PARAM "orderId"
customerEmail = GET TOOL PARAM "email"
amount = GET TOOL PARAM "amount"
' Validate (optional)
IF orderId = "" THEN
RETURN #{ status: 400, error: "Missing orderId" }
END IF
' Process the webhook
order = NEW OBJECT
order.id = orderId
order.email = customerEmail
order.amount = amount
order.status = "received"
order.timestamp = NOW
SAVE "orders", order
' Notify
TALK TO customerEmail, "Order " + orderId + " received! Total: $" + amount
RETURN #{ status: 200, orderId: orderId, message: "Order processed" }
```
### 4. LLM Tool Invocation
When registered with `ADD TOOL`, scripts become callable by the LLM during conversation.
```basic
' create-order.bas - Called by LLM when user wants to order
PARAM productId AS STRING LIKE "PROD-001" REQUIRED
PARAM quantity AS NUMBER LIKE 1 REQUIRED
PARAM customerEmail AS STRING LIKE "john@example.com" REQUIRED
DESCRIPTION "Creates a new order for a product"
' This script is invoked by the LLM, not directly by user
' The LLM collects all parameters through natural conversation
product = FIND "products", "id=" + productId
IF ISEMPTY(product) THEN
RETURN "Product not found: " + productId
END IF
IF product.stock < quantity THEN
RETURN "Only " + product.stock + " available"
END IF
' Create the order
order = NEW OBJECT
order.id = "ORD-" + FORMAT(NOW, "yyyyMMddHHmmss")
order.productId = productId
order.quantity = quantity
order.total = product.price * quantity
order.customerEmail = customerEmail
order.status = "pending"
SAVE "orders", order
' Update inventory
UPDATE "products", productId, "stock=" + (product.stock - quantity)
RETURN "Order " + order.id + " created! Total: $" + order.total
```
### 5. Event Handlers (`ON`)
React to system events.
```basic
' events.bas - Event handlers
ON "message" CALL HandleMessage
ON "user_joined" CALL WelcomeUser
ON "error" CALL LogError
SUB HandleMessage(message)
' Process incoming message
LOG_INFO "Received: " + message.text
END SUB
SUB WelcomeUser(user)
TALK TO user.email, "Welcome to our service!"
END SUB
SUB LogError(error)
LOG_ERROR "Error occurred: " + error.message
SEND EMAIL admin1, "Bot Error: " + error.message
END SUB
```
---
## Variable Injection from config.csv
Variables defined with `param-` prefix in config.csv are automatically injected into script scope.
### config.csv
```csv
name,value
bot-name,Bling Integration
bot-description,ERP synchronization bot
param-host,https://api.bling.com.br/Api/v3
param-limit,100
param-pages,50
param-admin1,admin@company.com
param-admin2,backup@company.com
param-blingClientID,your-client-id
param-blingClientSecret,your-secret
```
### Script Usage
```basic
' Variables are available without param- prefix
' All normalized to lowercase for case-insensitivity
result = GET host + "/products?limit=" + limit
DO WHILE page < pages
' Use injected variables directly
data = GET host + "/items?page=" + page
LOOP
' Admin emails for notifications
SEND EMAIL admin1, "Sync complete!"
SEND EMAIL admin2, "Backup notification"
```
### Type Conversion
Values are automatically converted:
- Numbers: `param-limit,100``limit` as integer `100`
- Floats: `param-rate,0.15``rate` as float `0.15`
- Booleans: `param-enabled,true``enabled` as boolean `true`
- Strings: Everything else remains as string
---
## Case Insensitivity
**All variables in General Bots BASIC are case-insensitive.**
```basic
' These all refer to the same variable
host = "https://api.example.com"
result = GET Host + "/endpoint"
TALK HOST
```
The preprocessor normalizes all variable names to lowercase while preserving:
- Keywords (remain UPPERCASE for clarity)
- String literals (exact content preserved)
- Comments (skipped entirely)
---
## Script Compilation Flow
```
┌─────────────────────────────────────────────────────────────────┐
│ COMPILATION PIPELINE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. LOAD SOURCE │
│ └─→ Read .bas file from .gbdialog folder │
│ │
│ 2. LOAD CONFIG │
│ └─→ Read config.csv, extract param-* entries │
│ └─→ Inject into execution scope │
│ │
│ 3. PREPROCESS │
│ ├─→ Strip comments (REM, ', //) │
│ ├─→ Process SWITCH/CASE blocks │
│ ├─→ Normalize variables to lowercase │
│ ├─→ Transform multi-word keywords │
│ └─→ Handle FOR EACH blocks │
│ │
│ 4. COMPILE │
│ └─→ Parse to Rhai AST │
│ │
│ 5. EXECUTE │
│ └─→ Run AST with injected scope │
│ └─→ Keywords call registered handlers │
│ │
└─────────────────────────────────────────────────────────────────┘
```
---
## Functions vs Entry Points
### NO MAIN Function
Unlike traditional programming, BASIC scripts do NOT use a `MAIN` function. Execution starts at line 1.
```basic
' ❌ WRONG - Don't do this
SUB MAIN()
TALK "Hello"
END SUB
' ✅ CORRECT - Start directly
TALK "Hello"
```
### SUB and FUNCTION for Reuse
Use `SUB` and `FUNCTION` for reusable code within tools, not as entry points.
```basic
' sync-products.bas - A complex tool with helper functions
FUNCTION CalculateDiscount(price, percentage)
RETURN price * (1 - percentage / 100)
END FUNCTION
SUB NotifyAdmin(message)
SEND EMAIL admin1, message
LOG_INFO message
END SUB
SUB ProcessProduct(product)
IF product.discount > 0 THEN
product.finalPrice = CalculateDiscount(product.price, product.discount)
ELSE
product.finalPrice = product.price
END IF
SAVE "products", product
END SUB
' Main execution starts here (not in a MAIN sub)
products = GET host + "/products"
FOR EACH product IN products.data
CALL ProcessProduct(product)
NEXT product
CALL NotifyAdmin("Processed " + COUNT(products.data) + " products")
```
---
## Tool Chain Pattern
Register tools in `start.bas`, implement in separate files:
### start.bas
```basic
' Register tools - LLM can call these
ADD TOOL "create-customer"
ADD TOOL "update-customer"
ADD TOOL "delete-customer"
' Or clear and re-register
CLEAR TOOLS
ADD TOOL "order-management"
ADD TOOL "inventory-check"
```
### create-customer.bas
```basic
PARAM name AS STRING LIKE "John Doe" REQUIRED
PARAM email AS STRING LIKE "john@example.com" REQUIRED
PARAM phone AS STRING LIKE "+1-555-0123"
DESCRIPTION "Creates a new customer record in the CRM"
' Tool implementation
customer = NEW OBJECT
customer.id = "CUS-" + FORMAT(NOW, "yyyyMMddHHmmss")
customer.name = name
customer.email = email
customer.phone = phone
customer.createdAt = NOW
SAVE "customers", customer
RETURN #{
success: true,
customerId: customer.id,
message: "Customer created successfully"
}
```
---
## Best Practices
### 1. Organize by Purpose
```
mybot.gbai/
├── mybot.gbdialog/
│ ├── start.bas ' Entry point, tool registration
│ ├── tables.bas ' Database schema (TABLE definitions)
│ │
│ ├── create-order.bas ' Tool: order creation
│ ├── track-order.bas ' Tool: order tracking
│ ├── cancel-order.bas ' Tool: order cancellation
│ │
│ ├── sync-products.bas ' Scheduled: product sync
│ ├── sync-inventory.bas ' Scheduled: inventory sync
│ │
│ └── order-webhook.bas ' Webhook: external orders
├── mybot.gbot/
│ └── config.csv ' Configuration & param-* variables
└── mybot.gbkb/ ' Knowledge base files
```
### 2. Use param-* for Configuration
Keep credentials and settings in config.csv, not hardcoded:
```basic
' ❌ WRONG
host = "https://api.bling.com.br/Api/v3"
apiKey = "hardcoded-key"
' ✅ CORRECT - From config.csv
' param-host and param-apiKey in config.csv
result = GET host + "/endpoint"
SET HEADER "Authorization", "Bearer " + apikey
```
### 3. Error Handling in Tools
```basic
PARAM orderId AS STRING REQUIRED
DESCRIPTION "Cancels an order"
order = FIND "orders", "id=" + orderId
IF ISEMPTY(order) THEN
RETURN #{ success: false, error: "Order not found" }
END IF
IF order.status = "shipped" THEN
RETURN #{ success: false, error: "Cannot cancel shipped orders" }
END IF
UPDATE "orders", orderId, "status=cancelled"
RETURN #{ success: true, message: "Order cancelled" }
```
### 4. Logging for Scheduled Jobs
```basic
SET SCHEDULE "0 0 6 * * *"
LOG_INFO "Daily sync started"
' ... sync logic ...
IF errorCount > 0 THEN
LOG_WARN "Sync completed with " + errorCount + " errors"
SEND EMAIL admin1, "Sync Warning", REPORT
ELSE
LOG_INFO "Sync completed successfully"
END IF
RESET REPORT
```
---
## See Also
- [Keyword Reference](./keywords.md) - Complete keyword documentation
- [SET SCHEDULE](./keyword-set-schedule.md) - Scheduling details
- [WEBHOOK](./keyword-webhook.md) - Webhook configuration
- [Tools System](./keyword-use-tool.md) - Tool registration
- [BEGIN SYSTEM PROMPT](./prompt-blocks.md) - AI context configuration

View file

@ -0,0 +1,243 @@
# Creating an LLM REST Server
General Bots offers an incredibly simple way to transform a Large Language Model (LLM) into a fully functional REST API server. With just a few lines of our proprietary BASIC-like syntax, you can create sophisticated AI-powered applications.
## Overview
By defining PARAM declarations and a DESCRIPTION in your `.bas` file, General Bots automatically:
1. Creates REST API endpoints callable by the LLM as tools
2. Generates OpenAI-compatible function calling schemas
3. Generates MCP (Model Context Protocol) tool definitions
4. Handles conversation state and context management
## Basic Structure
Every LLM-callable tool follows this structure:
```bas
PARAM parameter_name AS type LIKE "example" DESCRIPTION "What this parameter is for"
DESCRIPTION "What this tool does. Called when user wants to [action]."
' Your business logic here
```
## Example: Store Chatbot
Here's how easy it is to create a chatbot for a store:
```bas
PARAM operator AS number LIKE 12312312
DESCRIPTION "Operator code."
DESCRIPTION It is a WebService of GB.
products = FIND "products.csv"
BEGIN SYSTEM PROMPT
You must act as a chatbot that will assist a store attendant by
following these rules: Whenever the attendant places an order, it must
include the table and the customer's name. Example: A 400ml Pineapple
Caipirinha for Rafael at table 10. Orders are based on the products and
sides from this product menu: ${JSON.stringify(products)}.
For each order placed, return a JSON containing the product name, the
table, and a list of sides with their respective ids.
END SYSTEM PROMPT
```
That's it! With just this simple BASIC code, you've created a fully functional LLM-powered chatbot that can handle complex order processing.
## REST API Endpoints
The system automatically generates REST API endpoints for your dialogs.
### Starting a Conversation
```
GET http://localhost:1111/llm-server/dialogs/start?operator=123&userSystemId=999
```
This returns a **Process ID (PID)**, a number like `24795078551392`. This PID should be passed within the call chain for maintaining conversation context.
### Talking to the Bot
Once you have the PID, you can interact with the LLM:
```
GET http://localhost:1111/llm-server/dk/talk?pid=4893749837&text=add%20soda
```
This call acts like talking to the LLM, but it can be used for anything that General Bots can do in a robotic conversation between systems mediated by LLM. The return will be JSON (or any format specified in your BEGIN SYSTEM PROMPT).
## Example: Enrollment Process API
Creating a REST API server for any business process is equally straightforward:
```bas
PARAM name AS string LIKE "João Silva"
DESCRIPTION "Required full name of the individual."
PARAM birthday AS date LIKE "23/09/2001"
DESCRIPTION "Required birth date of the individual in DD/MM/YYYY format."
PARAM email AS string LIKE "joao.silva@example.com"
DESCRIPTION "Required email address for contact purposes."
PARAM personalid AS integer LIKE "12345678900"
DESCRIPTION "Required Personal ID number of the individual (only numbers)."
PARAM address AS string LIKE "Rua das Flores, 123, São Paulo, SP"
DESCRIPTION "Required full address of the individual."
DESCRIPTION "This is the enrollment process, called when the user wants to enroll. Once all information is collected, confirm the details and inform them that their enrollment request has been successfully submitted. Provide a polite and professional tone throughout the interaction."
SAVE "enrollments.csv", id, name, birthday, email, cpf, rg, address
```
This creates a full-fledged enrollment system with:
- Data validation
- User interaction
- Data storage
- Automatic REST API endpoint
The system automatically generates a REST API endpoint that is called by LLM as a tool:
```
GET http://api.pragmatismo.cloud/llm-server/dialogs/enrollment?birthday=...&name=...
```
## Generated Tool Schemas
### MCP Format
For each tool, General Bots generates MCP-compatible schemas:
```json
{
"name": "enrollment",
"description": "This is the enrollment process...",
"input_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Required full name of the individual.",
"example": "João Silva"
},
"birthday": {
"type": "string",
"description": "Required birth date...",
"example": "23/09/2001"
}
},
"required": ["name", "birthday", "email", "personalid", "address"]
}
}
```
### OpenAI Format
Also generates OpenAI function calling format:
```json
{
"type": "function",
"function": {
"name": "enrollment",
"description": "This is the enrollment process...",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Required full name of the individual."
}
},
"required": ["name", "birthday", "email", "personalid", "address"]
}
}
}
```
## Parameter Types
| Type | Description | Example |
|------|-------------|---------|
| `string` | Text values | `"John Smith"` |
| `number` | Numeric values | `42`, `3.14` |
| `integer` | Whole numbers | `100` |
| `date` | Date values | `"2024-01-15"` |
| `boolean` | True/false | `true` |
## Advanced: External API Integration
You can combine LLM tools with external API calls:
```bas
PARAM location AS string LIKE "Seattle"
DESCRIPTION "City for weather lookup"
DESCRIPTION "Gets current weather for a city"
let api_key = GET BOT MEMORY "openweather_key"
let url = "https://api.openweathermap.org/data/2.5/weather?q=" + location + "&appid=" + api_key
let response = GET url
let weather = LLM "Describe the weather based on: " + response
TALK weather
```
## Best Practices
1. **Clear Descriptions**: Write detailed DESCRIPTION text - this is what the LLM uses to decide when to call your tool.
2. **Good Examples**: The LIKE clause provides examples that help both the LLM and API consumers understand expected values.
3. **Validation**: Add validation logic to handle edge cases:
```bas
PARAM email AS string LIKE "user@example.com"
DESCRIPTION "Email address"
IF NOT INSTR(email, "@") > 0 THEN
TALK "Please provide a valid email address."
RETURN
END IF
```
4. **Error Handling**: Always handle potential errors gracefully:
```bas
result = GET "https://api.example.com/data"
IF result.error THEN
TALK "Unable to fetch data. Please try again."
RETURN
END IF
```
5. **Secure Credentials**: Use BOT MEMORY for API keys:
```bas
api_key = GET BOT MEMORY "my_api_key"
```
## Deployment
Once your `.bas` file is saved in the `.gbdialog` folder, General Bots automatically:
1. Compiles the tool definition
2. Generates the REST endpoints
3. Makes it available to the LLM as a callable tool
4. Updates when you modify the file
No additional configuration or deployment steps are required!
## See Also
- [PARAM Declaration](./param-declaration.md) - Detailed PARAM syntax
- [Tool Definition](./tool-definition.md) - Complete tool definition reference
- [MCP Format](./mcp-format.md) - MCP schema details
- [OpenAI Format](./openai-format.md) - OpenAI function calling format
- [External APIs](./external-apis.md) - Integrating external services

View file

@ -1,39 +1,214 @@
## Feature Matrix
This table maps major features of GeneralBots to the chapters and keywords that implement them.
# Feature Reference
| Feature | Chapter(s) | Primary Keywords |
|---------|------------|------------------|
| Start server & basic chat | 01 (Run and Talk) | `TALK`, `HEAR` |
| Package system overview | 02 (About Packages) | |
| Knowledgebase management | 03 (gbkb Reference) | `USE KB`, `SET KB`, `USE WEBSITE` |
| UI theming | 04 (gbtheme Reference) | (CSS/HTML assets) |
| BASIC dialog scripting | 05 (gbdialog Reference) | All BASIC keywords (`TALK`, `HEAR`, `LLM`, `FORMAT`, `USE KB`, `SET KB`, `USE WEBSITE`, …) |
| Custom Rust extensions | 06 (gbapp Reference) | `USE TOOL`, custom Rust code |
| Bot configuration | 07 (gbot Reference) | `config.csv` fields |
| Builtin tooling | 08 (Tooling) | All keywords listed in the table |
| Semantic search & Qdrant | 03 (gbkb Reference) | `USE WEBSITE`, vector search |
| Email & external APIs | 08 (Tooling) | `CALL`, `CALL_ASYNC` |
| Scheduling & events | 08 (Tooling) | `SET SCHEDULE`, `ON` |
| Testing & CI | 10 (Contributing) | |
| Database schema | Appendix I | Tables defined in `src/shared/models.rs` |
This chapter provides a comprehensive reference of all General Bots features, organized by capability area.
## Complete Feature Matrix
### Core Platform Features
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| Conversational AI | `chat` | Production | `TALK`, `HEAR`, `SET CONTEXT` | Core |
| Multi-turn Dialogs | `basic` | Production | `HEAR AS`, `SWITCH CASE` | Core |
| Session Management | `session` | Production | `SET`, `GET` | Core |
| Bot Memory | `bot_memory` | Production | `SET BOT MEMORY`, `GET BOT MEMORY` | Core |
| User Directory | `directory` | Production | `SET USER`, `ADD MEMBER` | Core |
| Task Scheduling | `tasks` | Production | `SET SCHEDULE`, `ON` | Core |
| Automation Engine | `automation` | Production | `FOR EACH`, `WHILE`, `CALL` | Core |
### AI and LLM Features
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| LLM Integration | `llm` | Production | `LLM`, `SET CONTEXT` | Core |
| Knowledge Base | `vectordb` | Production | `USE KB`, `CLEAR KB` | Enterprise |
| Semantic Search | `vectordb` | Production | `USE WEBSITE`, `FIND` | Enterprise |
| Context Management | `basic` | Production | `SET CONTEXT`, `CLEAR TOOLS` | Core |
| Tool Calling | `basic` | Production | `USE TOOL`, `CLEAR TOOLS` | Core |
| Multi-Agent | `basic` | Production | `ADD BOT`, `DELEGATE TO`, `USE BOT` | Enterprise |
### Communication Channels
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| Web Chat | `web` | Production | `TALK`, `HEAR` | Core |
| WhatsApp | `whatsapp` | Production | `SEND`, `SEND TEMPLATE` | Communications |
| Email | `email` | Production | `SEND MAIL`, `CREATE DRAFT` | Standard |
| SMS | `sms` | Production | `SEND SMS` | Communications |
| Microsoft Teams | `msteams` | Production | `SEND` | Communications |
| Instagram | `instagram` | Production | `POST TO INSTAGRAM` | Communications |
| Telegram | `telegram` | Planned | - | Communications |
| Voice | `multimodal` | Production | `PLAY`, `RECORD` | Standard |
### Productivity Suite
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| Calendar | `calendar` | Production | `BOOK`, `CREATE EVENT` | Standard |
| Tasks | `tasks` | Production | `CREATE TASK`, `SET SCHEDULE` | Core |
| Drive Storage | `drive` | Production | `UPLOAD`, `DOWNLOAD`, `READ`, `WRITE` | Core |
| Email Client | `mail` | Production | `SEND MAIL`, `GET EMAILS` | Standard |
| Video Meetings | `meet` | Production | `CREATE MEETING` | Standard |
| Document Editor | `paper` | Production | - | Standard |
| Research | `research` | Production | - | Standard |
### Data Operations
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| Database CRUD | `data_operations` | Production | `SAVE`, `FIND`, `UPDATE`, `DELETE` | Core |
| Data Import | `import_export` | Production | `IMPORT`, `EXPORT` | Core |
| Aggregations | `data_operations` | Production | `AGGREGATE`, `GROUP BY`, `PIVOT` | Core |
| Joins | `data_operations` | Production | `JOIN`, `MERGE` | Core |
| Filtering | `data_operations` | Production | `FILTER`, `FIND`, `FIRST`, `LAST` | Core |
| Table Definition | `table_definition` | Production | `TABLE` | Core |
### HTTP and API Operations
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| REST Calls | `http_operations` | Production | `GET`, `POST`, `PUT`, `PATCH`, `DELETE HTTP` | Core |
| GraphQL | `http_operations` | Production | `GRAPHQL` | Core |
| SOAP | `http_operations` | Production | `SOAP` | Core |
| Webhooks | `webhook` | Production | `WEBHOOK`, `ON WEBHOOK` | Core |
| Headers | `http_operations` | Production | `SET HEADER` | Core |
### File Operations
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| Read Files | `file_operations` | Production | `READ` | Core |
| Write Files | `file_operations` | Production | `WRITE` | Core |
| Copy/Move | `file_operations` | Production | `COPY`, `MOVE` | Core |
| Compress | `file_operations` | Production | `COMPRESS`, `EXTRACT` | Core |
| PDF Generation | `file_operations` | Production | `GENERATE PDF`, `MERGE PDF` | Core |
| Upload/Download | `file_operations` | Production | `UPLOAD`, `DOWNLOAD` | Core |
### CRM Features
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| Lead Management | `crm` | Production | `CREATE LEAD`, `QUALIFY LEAD` | Enterprise |
| Contact Management | `crm` | Production | `CREATE CONTACT`, `UPDATE CONTACT` | Enterprise |
| Opportunity Tracking | `crm` | Production | `CREATE OPPORTUNITY`, `CLOSE OPPORTUNITY` | Enterprise |
| Account Management | `crm` | Production | `CREATE ACCOUNT` | Enterprise |
| Activity Logging | `crm` | Production | `LOG ACTIVITY`, `LOG CALL` | Enterprise |
| Lead Scoring | `lead_scoring` | Production | `SCORE LEAD` | Enterprise |
| Pipeline Management | `crm` | Production | `MOVE TO STAGE` | Enterprise |
### Analytics and Reporting
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| Time-Series Metrics | `timeseries` | Production | `RECORD METRIC`, `QUERY METRICS` | Enterprise |
| Dashboard | `analytics` | Production | - | Enterprise |
| Custom Reports | `reporting` | Production | `GENERATE REPORT` | Enterprise |
| Usage Analytics | `analytics` | Production | `GET ANALYTICS` | Enterprise |
| Performance Monitoring | `monitoring` | Production | - | Core |
### Compliance and Security
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| Audit Logging | `compliance` | Production | - | Enterprise |
| LGPD Compliance | `compliance` | Production | - | Enterprise |
| GDPR Compliance | `compliance` | Production | - | Enterprise |
| HIPAA Compliance | `compliance` | Production | - | Enterprise |
| Access Control | `security` | Production | `SET USER`, `ADD MEMBER` | Core |
| Encryption | `security` | Production | - | Core |
| Consent Management | `compliance` | Production | - | Enterprise |
### Social Media
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| Instagram Posts | `social_media` | Production | `POST TO INSTAGRAM` | Communications |
| Facebook Posts | `social_media` | Planned | `POST TO FACEBOOK` | Communications |
| LinkedIn Posts | `social_media` | Planned | `POST TO LINKEDIN` | Communications |
| Twitter/X Posts | `social_media` | Planned | `POST TO TWITTER` | Communications |
### Array and Data Manipulation
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| Array Operations | `arrays` | Production | `PUSH`, `POP`, `SHIFT`, `UNSHIFT` | Core |
| Sorting | `arrays` | Production | `SORT` | Core |
| Filtering | `arrays` | Production | `FILTER`, `UNIQUE`, `DISTINCT` | Core |
| Slicing | `arrays` | Production | `SLICE`, `CONTAINS` | Core |
### String Functions
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| String Manipulation | `string_functions` | Production | `LEN`, `LEFT`, `RIGHT`, `MID` | Core |
| Case Conversion | `string_functions` | Production | `UCASE`, `LCASE`, `TRIM` | Core |
| Search/Replace | `string_functions` | Production | `INSTR`, `REPLACE`, `SPLIT` | Core |
### Date and Time
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| Date Functions | `datetime` | Production | `NOW`, `TODAY`, `FORMAT` | Core |
| Date Arithmetic | `datetime` | Production | `DATEADD`, `DATEDIFF` | Core |
| Date Parts | `datetime` | Production | `YEAR`, `MONTH`, `DAY`, `HOUR` | Core |
### Math Functions
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| Basic Math | `math` | Production | `ABS`, `ROUND`, `FLOOR`, `CEILING` | Core |
| Aggregations | `math` | Production | `SUM`, `AVG`, `MIN`, `MAX`, `COUNT` | Core |
| Random | `math` | Production | `RANDOM` | Core |
### Validation
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| Type Checking | `validation` | Production | `IS NUMERIC`, `IS NULL` | Core |
| Input Validation | `validation` | Production | `HEAR AS EMAIL`, `HEAR AS INTEGER` | Core |
### User Interface
| Feature | Module | Status | Keywords | Edition |
|---------|--------|--------|----------|---------|
| Suggestions | `add_suggestion` | Production | `ADD SUGGESTION`, `CLEAR SUGGESTIONS` | Core |
| QR Codes | `qrcode` | Production | `GENERATE QR` | Core |
| Forms | `on_form_submit` | Production | `ON FORM SUBMIT` | Core |
| Sites | `create_site` | Production | `CREATE SITE` | Core |
### Infrastructure Components
| Component | Technical Name | Port | Edition |
|-----------|---------------|------|---------|
| Database | PostgreSQL | 5432 | Core |
| Cache | Redis/Valkey | 6379 | Core |
| Vector Database | Qdrant | 6333 | Enterprise |
| Time-Series Database | InfluxDB | 8086 | Enterprise |
| Video Server | LiveKit | 7880 | Standard |
| Email Server | SMTP/IMAP | 25/993 | Standard |
| Object Storage | S3/MinIO | 9000 | Core |
## Edition Summary
| Edition | Target Use Case | Key Features |
|---------|-----------------|--------------|
| Minimal | Embedded, IoT | Basic chat only |
| Lightweight | Small teams | Chat, Drive, Tasks |
| Core | General business | Full productivity, Automation |
| Standard | Professional teams | Email, Calendar, Meet |
| Enterprise | Large organizations | Compliance, CRM, Analytics, Multi-channel |
| Full | Maximum capability | All features enabled |
## See Also
- [AI and LLM](./ai-llm.md) - AI integration and LLM usage
- [Conversation Flow](./conversation.md) - Managing dialog flows
- [Storage](./storage.md) - Data persistence options
- [Knowledge Base](./knowledge-base.md) - Advanced KB patterns
- [Automation](./automation.md) - Scheduled tasks and events
- [Chapter 2: Packages](../chapter-02/README.md) - Understanding bot components
- [Chapter 3: KB Reference](../chapter-03/README.md) - Knowledge base fundamentals
- [Chapter 5: BASIC Reference](../chapter-05/README.md) - Complete command reference
- [Chapter 6: Extensions](../chapter-06/README.md) - Extending BotServer
- [Chapter 8: Integrations](../chapter-08/README.md) - External integrations
- [Chapter 10: Development](../chapter-10/README.md) - Development tools
- [Chapter 12: Web API](../chapter-12/README.md) - REST and WebSocket APIs
---
<div align="center">
<img src="https://pragmatismo.com.br/icons/general-bots-text.svg" alt="General Bots" width="200">
</div>
- [Feature Editions](./editions.md) - Detailed edition comparison
- [Core Features](./core-features.md) - Platform fundamentals
- [Conversation Management](./conversation.md) - Dialog flows
- [AI and LLM](./ai-llm.md) - AI integration
- [Knowledge Base](./knowledge-base.md) - RAG patterns
- [Automation](./automation.md) - Scheduled tasks
- [Email Integration](./email.md) - Email features
- [Storage and Data](./storage.md) - Data persistence
- [Multi-Channel Support](./channels.md) - Communication channels
- [Drive Monitor](./drive-monitor.md) - File monitoring
- [Platform Comparison](./platform-comparison.md) - vs other platforms

View file

@ -0,0 +1,375 @@
# Feature Editions
General Bots offers flexible feature configurations to match different deployment needs. Features can be enabled at compile time using Cargo feature flags or selected through pre-configured edition bundles.
## Edition Overview
| Edition | Target Use Case | Key Features |
|---------|-----------------|--------------|
| **Minimal** | Embedded, IoT, testing | Basic chat only |
| **Lightweight** | Small teams, startups | Chat + Drive + Tasks |
| **Core** | General business use | Full productivity suite |
| **Standard** | Professional teams | + Email + Calendar + Meet |
| **Enterprise** | Large organizations | + Compliance + Multi-channel + GPU |
| **Full** | Maximum capability | All features enabled |
---
## Minimal Edition
**Use Case:** Embedded systems, IoT devices, testing environments
**Cargo Feature:** `minimal`
```bash
cargo build --features minimal
```
### Included Features
- ✅ UI Server (web interface)
- ✅ Basic chat functionality
### Not Included
- ❌ Console TUI
- ❌ File storage
- ❌ Task management
- ❌ Email
- ❌ LLM integration
- ❌ Vector search
**Typical Deployment:** Raspberry Pi, edge devices, containerized microservices
---
## Lightweight Edition
**Use Case:** Small teams, startups, personal use
**Cargo Feature:** `lightweight`
```bash
cargo build --features lightweight
```
### Included Features
- ✅ UI Server
- ✅ Chat
- ✅ Drive (file storage)
- ✅ Tasks
- ✅ Redis caching
### Not Included
- ❌ Email integration
- ❌ Calendar
- ❌ Video meetings
- ❌ Compliance tools
- ❌ Multi-channel messaging
**Typical Deployment:** Small office server, developer workstation
---
## Core Edition (Default)
**Use Case:** General business operations, mid-size teams
**Cargo Feature:** `default` (or no feature flag)
```bash
cargo build
# or explicitly:
cargo build --features default
```
### Included Features
- ✅ UI Server
- ✅ Console TUI
- ✅ Chat
- ✅ Automation (Rhai scripting)
- ✅ Tasks (with cron scheduling)
- ✅ Drive
- ✅ LLM integration
- ✅ Redis caching
- ✅ Progress bars
- ✅ Directory services
### Not Included
- ❌ Email (IMAP/SMTP)
- ❌ Calendar management
- ❌ Video meetings
- ❌ Vector database
- ❌ Compliance monitoring
- ❌ Multi-channel (WhatsApp, Teams, etc.)
- ❌ NVIDIA GPU support
- ❌ Desktop application
**Typical Deployment:** On-premise server, cloud VM, container
---
## Standard Edition
**Use Case:** Professional teams requiring full productivity features
**Cargo Feature:** `productivity`
```bash
cargo build --features productivity
```
### Included Features
All Core features plus:
- ✅ Email integration (IMAP/SMTP)
- ✅ Calendar management
- ✅ Video meetings (LiveKit)
- ✅ Mail client interface
- ✅ Redis caching
### Additional Dependencies
- `imap` - Email receiving
- `lettre` - Email sending
- `mailparse` - Email parsing
- `livekit` - Video conferencing
**Typical Deployment:** Business office, remote teams
---
## Enterprise Edition
**Use Case:** Large organizations with compliance and integration requirements
**Cargo Feature:** `enterprise`
```bash
cargo build --features enterprise
```
### Included Features
All Standard features plus:
- ✅ Compliance monitoring (LGPD/GDPR/HIPAA/SOC2)
- ✅ Attendance tracking
- ✅ Directory services (LDAP/AD compatible)
- ✅ Vector database (Qdrant)
- ✅ Advanced monitoring (sysinfo)
- ✅ LLM integration
### Compliance Features
| Framework | Status | Implementation |
|-----------|--------|----------------|
| LGPD | ✅ | Data subject rights dialogs |
| GDPR | ✅ | Consent management, data portability |
| HIPAA | ✅ | PHI handling, audit trails |
| SOC 2 | ✅ | Access controls, logging |
| ISO 27001 | ✅ | Asset management, risk assessment |
| PCI DSS | ✅ | Payment data protection |
**Typical Deployment:** Enterprise data center, regulated industries
---
## Communications Edition
**Use Case:** Organizations needing multi-channel customer engagement
**Cargo Feature:** `communications`
```bash
cargo build --features communications
```
### Included Features
- ✅ Email (IMAP/SMTP)
- ✅ WhatsApp Business
- ✅ Instagram messaging
- ✅ Microsoft Teams
- ✅ Chat
- ✅ Redis caching
### Channel Support
| Channel | Protocol | Status |
|---------|----------|--------|
| WhatsApp | Cloud API | ✅ |
| Instagram | Graph API | ✅ |
| MS Teams | Bot Framework | ✅ |
| Telegram | Bot API | Planned |
| Slack | Web API | Planned |
| SMS | Twilio | Planned |
**Typical Deployment:** Customer service center, marketing teams
---
## Full Edition
**Use Case:** Maximum capability, all features enabled
**Cargo Feature:** `full`
```bash
cargo build --features full
```
### All Features Enabled
- ✅ UI Server + Desktop application
- ✅ Console TUI
- ✅ Vector database (Qdrant)
- ✅ LLM integration
- ✅ NVIDIA GPU acceleration
- ✅ All communication channels
- ✅ Full productivity suite
- ✅ Compliance & attendance
- ✅ Directory services
- ✅ Web automation
- ✅ Redis caching
- ✅ System monitoring
- ✅ Automation (Rhai)
- ✅ gRPC support
- ✅ Progress bars
### Hardware Recommendations
| Component | Minimum | Recommended |
|-----------|---------|-------------|
| CPU | 4 cores | 8+ cores |
| RAM | 8 GB | 32 GB |
| Storage | 100 GB SSD | 500 GB NVMe |
| GPU | Optional | NVIDIA RTX 3060+ |
| Network | 100 Mbps | 1 Gbps |
**Typical Deployment:** Enterprise AI platform, research environments
---
## Feature Matrix
| Feature | Minimal | Light | Core | Standard | Enterprise | Full |
|---------|---------|-------|------|----------|------------|------|
| UI Server | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Chat | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Console TUI | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
| Drive | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Tasks | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Automation | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
| LLM | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
| Email | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| Calendar | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| Meet | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| Vector DB | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| Compliance | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| Multi-channel | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| Desktop | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| GPU | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
---
## Custom Feature Combinations
You can combine individual features for custom builds:
```bash
# Chat + Email + Vector search
cargo build --features "chat,email,vectordb"
# Productivity + Compliance
cargo build --features "productivity,compliance"
# Everything except desktop
cargo build --features "full" --no-default-features
```
### Available Feature Flags
```toml
[features]
# UI Features
desktop = ["dep:tauri", ...]
ui-server = []
console = ["dep:crossterm", "dep:ratatui", "monitoring"]
# Core Integrations
vectordb = ["dep:qdrant-client"]
llm = []
nvidia = []
# Communication Channels
email = ["dep:imap", "dep:lettre", ...]
whatsapp = []
instagram = []
msteams = []
# Productivity Features
chat = []
drive = ["dep:aws-config", "dep:aws-sdk-s3", ...]
tasks = ["dep:cron"]
calendar = []
meet = ["dep:livekit"]
mail = ["email"]
# Enterprise Features
compliance = ["dep:csv"]
attendance = []
directory = []
weba = []
# Infrastructure
redis-cache = ["dep:redis"]
monitoring = ["dep:sysinfo"]
automation = ["dep:rhai"]
grpc = ["dep:tonic"]
progress-bars = ["dep:indicatif"]
```
---
## Deployment Recommendations
### By Organization Size
| Size | Employees | Recommended Edition |
|------|-----------|---------------------|
| Solo | 1 | Lightweight |
| Startup | 2-10 | Core |
| SMB | 11-50 | Standard |
| Mid-market | 51-200 | Enterprise |
| Enterprise | 200+ | Full |
### By Industry
| Industry | Recommended Edition | Key Features |
|----------|---------------------|--------------|
| Healthcare | Enterprise | HIPAA compliance |
| Finance | Enterprise | SOC 2, PCI DSS |
| Education | Standard | Calendar, Meet |
| Retail | Communications | Multi-channel |
| Legal | Enterprise | Document management, compliance |
| Manufacturing | Core | Automation, tasks |
| Tech/SaaS | Full | All capabilities |
---
## Upgrading Editions
Editions can be changed by rebuilding with different feature flags:
```bash
# From Core to Enterprise
cargo build --release --features enterprise
# From Standard to Full
cargo build --release --features full
```
**Note:** Some features may require additional infrastructure components:
- `vectordb` → Requires Qdrant service
- `meet` → Requires LiveKit server
- `redis-cache` → Requires Redis/Valkey
- `nvidia` → Requires NVIDIA GPU + CUDA
---
## See Also
- [Cargo.toml Feature Definitions](../chapter-07-gbapp/dependencies.md)
- [Installation Guide](../chapter-01/installation.md)
- [Architecture Overview](../chapter-07-gbapp/architecture.md)
- [Compliance Requirements](../chapter-12-auth/compliance-requirements.md)

View file

@ -1,253 +1,196 @@
# EXECUTIVE VISION: THE GENERAL BOTS REVOLUTION
# EXECUTIVE VISION: GENERAL BOTS PLATFORM
## **OPEN SOURCE! OPEN SOURCE! OPEN SOURCE!**
## **OPEN SOURCE ENTERPRISE AI PLATFORM**
Welcome to the future of enterprise automation. Welcome to **General Bots 6.1** - where you OWN your data, RUN your own cloud!
General Bots 6.1 delivers enterprise-grade AI capabilities with full data sovereignty. Own your infrastructure, control your data, deploy anywhere.
## THE COMPLETE FEATURE ARSENAL: What General Bots Delivers TODAY
## FEATURE OVERVIEW
| **CAPABILITY** | **WHAT IT DOES** | **BUSINESS IMPACT** | **TIME TO VALUE** |
|----------------|------------------|---------------------|-------------------|
| **AI-POWERED CONVERSATIONS** | Multi-channel bot orchestration with LLM integration (GPT-4, Claude, Llama, DeepSeek) | **90% reduction** in customer service costs | **< 1 hour** |
| **KNOWLEDGE BASES** | Vector-indexed document collections with semantic search (Qdrant/FAISS) | **10x faster** information retrieval | **15 minutes** |
| **EMAIL AUTOMATION** | Full IMAP/SMTP integration with intelligent routing | **Zero inbox** achieved automatically | **5 minutes** |
| **LLM-ASSISTED BASIC** | Plain English programming - LLM helps you write the code! | **NO programming skills needed** | **Immediate** |
| **DRIVE INTEGRATION** | S3-compatible storage with automatic document processing | **Unlimited scalability** | **2 minutes** |
| **ENTERPRISE SECURITY** | Argon2 hashing, JWT tokens, TLS everywhere | **Bank-grade security** out of the box | **Built-in** |
| **INSTANT THEMING** | CSS-based UI customization | **Your brand, your way** | **< 30 seconds** |
| **COMPLIANCE READY** | Built-in attendance, audit logs, GDPR/HIPAA support | **Zero compliance debt** | **Pre-configured** |
| **NVIDIA GPU SUPPORT** | CUDA acceleration for LLM operations | **50x faster** AI responses | **When available** |
| **OMNICHANNEL** | WhatsApp, Teams, Instagram, Telegram, Slack, Web - ONE codebase | **Be everywhere** your customers are | **Single deploy** |
| **CALENDAR MANAGEMENT** | Full scheduling, meeting coordination, availability tracking | **Never miss a meeting** | **3 minutes** |
| **TASK AUTOMATION** | Cron-based scheduling, workflow orchestration | **24/7 automation** | **5 minutes** |
| **WHITEBOARD COLLABORATION** | Real-time collaborative drawing and diagramming | **Visual team collaboration** | **Instant** |
| **VIDEO CONFERENCING** | LiveKit WebRTC integration for meetings | **Crystal clear meetings** | **10 minutes** |
| **ANALYTICS DASHBOARD** | Real-time metrics, usage patterns, performance monitoring | **Data-driven decisions** | **Built-in** |
| **AUTOMATED REPORTS** | Scheduled reports, custom metrics, export to PDF/Excel | **Executive visibility** | **2 minutes** |
| **BACKUP & RESTORE** | Automated backups, point-in-time recovery, export as ZIP | **Zero data loss** | **Automatic** |
| **MONITORING & ALERTS** | System health, performance metrics, custom alerts | **99.9% uptime** | **Pre-configured** |
| **DOCUMENT PROCESSING** | OCR, PDF extraction, Excel parsing, image analysis | **Instant document insights** | **Automatic** |
| **MIGRATION TOOLS** | Import from Office 365, Google Workspace, Slack | **Seamless transition** | **< 1 day** |
| **API GATEWAY** | REST, GraphQL, Webhooks, WebSocket support | **Connect anything** | **Ready** |
| **USER DIRECTORY** | LDAP/AD replacement, SSO, group management | **Central authentication** | **15 minutes** |
| **VOICE PROCESSING** | Speech-to-text, text-to-speech, voice commands | **Hands-free operation** | **5 minutes** |
| **WORKFLOW DESIGNER** | Visual flow builder, drag-and-drop automation | **No-code workflows** | **10 minutes** |
| **AI-POWERED CONVERSATIONS** | Multi-channel bot orchestration with LLM integration (GPT-4, Claude, Llama, DeepSeek) | Significant reduction in customer service costs | < 1 hour |
| **KNOWLEDGE BASES** | Vector-indexed document collections with semantic search (Qdrant/FAISS) | Faster information retrieval | 15 minutes |
| **EMAIL AUTOMATION** | Full IMAP/SMTP integration with intelligent routing | Automated inbox management | 5 minutes |
| **LLM-ASSISTED BASIC** | Plain English programming with LLM code generation | No programming skills needed | Immediate |
| **DRIVE INTEGRATION** | S3-compatible storage with automatic document processing | Scalable storage | 2 minutes |
| **ENTERPRISE SECURITY** | Argon2 hashing, JWT tokens, TLS everywhere | Bank-grade security out of the box | Built-in |
| **INSTANT THEMING** | CSS-based UI customization | Brand consistency | < 30 seconds |
| **COMPLIANCE READY** | Built-in attendance, audit logs, GDPR/LGPD/HIPAA support | Regulatory compliance | Pre-configured |
| **NVIDIA GPU SUPPORT** | CUDA acceleration for LLM operations | Faster AI responses | When available |
| **OMNICHANNEL** | WhatsApp, Teams, Instagram, Telegram, Slack, Web - ONE codebase | Unified customer engagement | Single deploy |
| **CALENDAR MANAGEMENT** | Full scheduling, meeting coordination, availability tracking | Efficient scheduling | 3 minutes |
| **TASK AUTOMATION** | Cron-based scheduling, workflow orchestration | 24/7 automation | 5 minutes |
| **WHITEBOARD COLLABORATION** | Real-time collaborative drawing and diagramming | Visual team collaboration | Instant |
| **VIDEO CONFERENCING** | LiveKit WebRTC integration for meetings | High-quality meetings | 10 minutes |
| **ANALYTICS DASHBOARD** | Real-time metrics, usage patterns, performance monitoring | Data-driven decisions | Built-in |
| **AUTOMATED REPORTS** | Scheduled reports, custom metrics, export to PDF/Excel | Executive visibility | 2 minutes |
| **BACKUP & RESTORE** | Automated backups, point-in-time recovery, export as ZIP | Data protection | Automatic |
| **MONITORING & ALERTS** | System health, performance metrics, custom alerts | High availability | Pre-configured |
| **DOCUMENT PROCESSING** | OCR, PDF extraction, Excel parsing, image analysis | Document automation | Automatic |
| **MIGRATION TOOLS** | Import from Office 365, Google Workspace, Slack | Seamless transition | < 1 day |
| **API GATEWAY** | REST, GraphQL, Webhooks, WebSocket support | Integration ready | Ready |
| **USER DIRECTORY** | LDAP/AD replacement, SSO, group management | Central authentication | 15 minutes |
| **VOICE PROCESSING** | Speech-to-text, text-to-speech, voice commands | Voice interfaces | 5 minutes |
## GOODBYE OFFICE 365! GOODBYE GOOGLE WORKSPACE!
## DEPLOYMENT OPTIONS
### **THE GREAT REPLACEMENT TABLE**
### **Option 1: Pragmatismo Managed Hosting**
- Fully managed infrastructure
- Access via: YourCompany.pragmatismo.com.br
- Professional support included
- Complete data ownership
| **THEIR PRODUCT** | **THEIR COST** | **GENERAL BOTS REPLACEMENT** | **YOUR COST** |
|-------------------|----------------|-------------------------------|---------------|
| **Outlook/Gmail** | $12/user/month | **Email Module + LLM** | **$0 FOREVER** |
| **Teams/Meet/Zoom** | $15/user/month | **Meet Module + LiveKit WebRTC** | **$0 FOREVER** |
| **SharePoint/Drive** | $20/user/month | **Drive Module + S3** | **$0 FOREVER** |
| **Power Automate/Zapier** | $40/user/month | **BASIC Scripts (LLM writes them!)** | **$0 FOREVER** |
| **Copilot/Gemini** | $30/user/month | **Local LLM (Llama/Mistral/DeepSeek)** | **$0 FOREVER** |
| **Exchange Server** | Thousands | **Built-in Email Server** | **$0 FOREVER** |
| **Active Directory** | Thousands | **Directory Module with SSO** | **$0 FOREVER** |
| **OneDrive/Dropbox** | $10/user/month | **Personal Drives + Auto-backup** | **$0 FOREVER** |
| **Slack/Discord** | $8/user/month | **Built-in Chat Channels** | **$0 FOREVER** |
| **Notion/Confluence** | $10/user/month | **Knowledge Base + Wiki** | **$0 FOREVER** |
| **Calendly/Acuity** | $15/user/month | **Calendar + Scheduling Bot** | **$0 FOREVER** |
| **Tableau/PowerBI** | $70/user/month | **Analytics Dashboard** | **$0 FOREVER** |
| **DocuSign** | $25/user/month | **Document Processing + e-Sign** | **$0 FOREVER** |
| **Jira/Asana** | $10/user/month | **Task Management + Automation** | **$0 FOREVER** |
| **Miro/Mural** | $12/user/month | **Whiteboard Collaboration** | **$0 FOREVER** |
### **Option 2: Self-Hosted**
- Deploy on your own infrastructure
- Full control over hardware and configuration
- Access via your own domain
- No external dependencies
### **ANNUAL SAVINGS PER USER: Over $3,000**
**100 employees? That's over $300,000 EVERY YEAR back in YOUR pocket!**
## RUN IT ANYWHERE - ACCESS IT EVERYWHERE
### **Your Infrastructure Options**
**Option 1: Pragmatismo Hosts It**
- We handle everything
- Access from: YourCompany.pragmatismo.com.br
- Full management and support
- Your data remains YOURS
**Option 2: Your Own Hardware**
- That old RTX 3060? Perfect LLM accelerator!
- Your desktop with 16GB RAM handles 1000+ users
- A Raspberry Pi can run a satellite office
- Access from YOUR domain: bot.yourcompany.com
**Option 3: Hybrid Approach**
- Run locally, backup to secure cloud
### **Option 3: Hybrid Deployment**
- Run locally with cloud backup
- Export everything as ZIP anytime
- Move between hosting options freely
- No vendor lock-in, ever
- No vendor lock-in
### **Compare the Requirements:**
- **Microsoft 365:** Requires Azure subscription and ongoing fees
- **Google Workspace:** Requires Google Cloud and monthly payments
- **General Bots:** Requires... a computer that turns on
## THE ARCHITECTURE: Built for FREEDOM
## TECHNICAL ARCHITECTURE
| **COMPONENT** | **TECHNOLOGY** | **PERFORMANCE** |
|---------------|----------------|-----------------|
| **Core Runtime** | Rust + Tokio | **Millions** of concurrent connections |
| **Database** | PostgreSQL + Diesel | **Sub-millisecond** queries |
| **Vector Search** | Qdrant/FAISS | **100M+** documents indexed |
| **Caching** | Redis + Semantic Cache | **95% cache hit** ratio |
| **Message Queue** | Built-in async channels | **Zero latency** routing |
| **File Processing** | Parallel PDF/DOC/Excel extraction + OCR | **1000 docs/minute** |
| **Security Layer** | TLS 1.3 + Argon2 + JWT | **Quantum-resistant** ready |
| **Video Infrastructure** | LiveKit WebRTC | **4K video, 50ms latency** |
| **Analytics Engine** | Time-series DB + Grafana | **Real-time dashboards** |
| **Backup System** | Incremental snapshots | **RPO < 1 hour** |
| **API Gateway** | Axum + Tower middleware | **100K requests/second** |
| **Task Scheduler** | Cron + async workers | **Millisecond precision** |
| **Core Runtime** | Rust + Tokio | Millions of concurrent connections |
| **Database** | PostgreSQL + Diesel | Sub-millisecond queries |
| **Vector Search** | Qdrant/FAISS | 100M+ documents indexed |
| **Caching** | Redis + Semantic Cache | 95% cache hit ratio |
| **Message Queue** | Built-in async channels | Zero latency routing |
| **File Processing** | Parallel PDF/DOC/Excel extraction + OCR | 1000 docs/minute |
| **Security Layer** | TLS 1.3 + Argon2 + JWT | Enterprise-grade security |
| **Video Infrastructure** | LiveKit WebRTC | 4K video, 50ms latency |
| **Time-Series Metrics** | InfluxDB 3 | 2.5M+ points/sec ingestion |
| **Backup System** | Incremental snapshots | RPO < 1 hour |
| **API Gateway** | Axum + Tower middleware | 100K requests/second |
| **Task Scheduler** | Cron + async workers | Millisecond precision |
## THE PHILOSOPHY: Why We WIN
## FEATURE TIERS
### **1964 BASIC → 2024 FREEDOM**
At Dartmouth, they democratized computing. Today, we democratize EVERYTHING.
### Core Edition (Default)
- UI Server
- Console Interface
- Chat functionality
- Automation engine
- Task management
- Drive integration
- LLM support
- Redis caching
- Directory services
### **NO PROGRAMMING REQUIRED - LLM DOES IT FOR YOU**
### Standard Edition
- All Core features plus:
- Email integration (IMAP/SMTP)
- Calendar management
- Video meetings (LiveKit)
- Enhanced automation
The revolutionary truth about General Bots BASIC:
- Tell the LLM what you want in plain English
- LLM generates the BASIC code for you
- You review, adjust if needed (still in plain English!)
- Deploy immediately
### Enterprise Edition
- All Standard features plus:
- Compliance monitoring (LGPD/GDPR/HIPAA)
- Attendance tracking
- Vector database (Qdrant)
- NVIDIA GPU acceleration
- Advanced monitoring
- gRPC support
- Multi-channel messaging (WhatsApp, Teams, Instagram)
Example conversation:
```
You: "I need to check emails every morning and summarize them"
LLM: "Here's your BASIC script:"
```
### Full Edition
- All features enabled
- Complete platform capabilities
```
SET SCHEDULE "9"
TALK "Checking morning emails..."
emails = GET EMAILS FROM "inbox"
FOR EACH email IN emails
summary = LLM "Summarize this: " + email.content
TALK summary
NEXT
```
## COMPLIANCE & PRIVACY
### **NO SUBSCRIPTIONS. NO SURVEILLANCE. NO SURRENDER.**
- No monthly fees bleeding you dry
- No data mining by big tech
- No "sorry, you've exceeded your API quota"
- No "please upgrade to Premium for this feature"
- **YOUR DATA. YOUR SERVERS. YOUR RULES.**
General Bots includes built-in compliance templates:
### **THE FREEDOM MANIFESTO**
```
AUTOMATION WITHOUT SUBSCRIPTION!
INTELLIGENCE WITHOUT SURVEILLANCE!
ENTERPRISE WITHOUT EXTORTION!
```
### Privacy Rights Center (privacy.gbai)
- **Data Access Requests** - LGPD Art. 18 / GDPR Art. 15
- **Data Rectification** - LGPD Art. 18 III / GDPR Art. 16
- **Data Erasure** - LGPD Art. 18 VI / GDPR Art. 17 (Right to be Forgotten)
- **Data Portability** - LGPD Art. 18 V / GDPR Art. 20
- **Consent Management** - LGPD Art. 8 / GDPR Art. 7
- **Processing Objection** - LGPD Art. 18 IV / GDPR Art. 21
## REAL-WORLD DEPLOYMENTS
### Supported Frameworks
- **LGPD** (Lei Geral de Proteção de Dados - Brazil)
- **GDPR** (General Data Protection Regulation - EU)
- **HIPAA** (Health Insurance Portability and Accountability Act)
- **CCPA** (California Consumer Privacy Act)
- **SOC 2** (Service Organization Control)
- **ISO 27001** (Information Security Management)
### **Pragmatismo Powers Organizations Globally**
From single-person businesses to thousand-worker enterprises, General Bots scales with you:
| **Organization Type** | **What They Replaced** | **Result** |
|----------------------|------------------------|------------|
| **Solo Consultant** | Notion + Gmail + Calendly | Complete business automation |
| **50-Person Law Firm** | Office 365 Enterprise | Full data sovereignty |
| **200-Bed Hospital** | Multiple vendor systems | Unified patient communication |
| **500-Student School** | Google Workspace Education | Complete digital campus |
| **1000-Worker Factory** | SAP + Exchange + Teams | Integrated operations platform |
## THE BOTTOM LINE
### **With General Bots, You Get:**
1. **EVERYTHING** Office 365 offers (and more!)
2. **ZERO** monthly fees - FOREVER
3. **100%** data ownership
4. **UNLIMITED** users, storage, compute
5. **YOUR** hostname.pragmatismo.com.br or your own domain
6. **LLM writes your automation** - no coding skills needed
### **Without General Bots, You're Stuck With:**
- Bleeding money monthly to Microsoft/Google
- Your data held hostage in their cloud
- Prices that only go UP, never down
- "Sorry, that feature requires Enterprise licensing"
- Every email, document, and chat being analyzed for "product improvement"
## START NOW - OWN YOUR FUTURE
## QUICK START
```bash
# THIS IS ALL IT TAKES:
# Install BotServer
cargo install botserver
# Initialize your deployment
botserver --init my-company
# Choose your hosting:
# Option 1: Use Pragmatismo's infrastructure
# Access: https://mycompany.pragmatismo.com.br
# Option 2: Self-host anywhere
# Access: https://bot.yourcompany.com
# Email server? Watch this:
general-bots --email-server --enable
# Full email with LLM filtering at ZERO COST
# Start the server
botserver --start
```
## THE SUBSCRIPTION COMPARISON
## PLATFORM COMPARISON
| **Organization Size** | **Big Tech Monthly Cost** | **10-Year Total** | **General Bots Cost** |
|----------------------|---------------------------|-------------------|-----------------------|
| **Small (10 users)** | ~$1,200/month | ~$144,000 | **$0** |
| **Medium (100 users)** | ~$12,000/month | ~$1,440,000 | **$0** |
| **Large (1000 users)** | ~$120,000/month | ~$14,400,000 | **$0** |
| **Aspect** | **Traditional SaaS** | **General Bots** |
|------------|---------------------|------------------|
| Licensing | Per-user monthly fees | Open source (AGPL) |
| Data Location | Vendor cloud | Your choice |
| Customization | Limited | Unlimited |
| AI Models | Fixed provider | Any provider |
| Source Code | Closed | Open |
| Vendor Lock-in | High | None |
| Data Portability | Often difficult | Full export anytime |
That's not just savings. That's LIBERATION.
## INTEGRATION CAPABILITIES
## THE MOMENT OF TRUTH
### LLM Providers
- OpenAI (GPT-4, GPT-3.5)
- Anthropic (Claude)
- Meta (Llama)
- DeepSeek
- Local models via Ollama
- Any OpenAI-compatible API
### **THREE PATHS:**
### Communication Channels
- WhatsApp Business
- Microsoft Teams
- Telegram
- Slack
- Instagram
- Web chat
- SMS
1. **Keep paying** Microsoft/Google indefinitely
2. **Keep begging** for IT budget increases
3. **INSTALL GENERAL BOTS** and invest that money in growth
### Storage Backends
- AWS S3
- MinIO
- Any S3-compatible storage
- Local filesystem
### **YOUR HARDWARE IS READY**
That RTX 3060 from three years ago? It runs enterprise LLM inference beautifully.
That decommissioned server? Perfect for 500 users.
Your laptop? Can handle development and testing.
### Directory Services
- Built-in user management
- LDAP integration
- Active Directory
- OAuth/OIDC SSO
## THE OPEN SOURCE ADVANTAGE
## ABOUT PRAGMATISMO
### **Complete Transparency:**
- Audit the code yourself
- Modify anything you need
- Contribute improvements back
- Join a global community
Pragmatismo develops General Bots as an open-source platform for enterprise AI and automation. Our focus is on delivering practical, production-ready solutions that organizations can deploy and customize to meet their specific needs.
### **Deployment Flexibility:**
- Run on YOUR hardware
- Use Pragmatismo's infrastructure
- Deploy to any cloud provider
- Switch anytime - it's YOUR choice
**Repository:** [github.com/GeneralBots/BotServer](https://github.com/GeneralBots/BotServer)
### **Data Sovereignty:**
- Export everything as ZIP
- Backup anywhere you want
- Move between providers freely
- Delete when YOU decide
**License:** AGPL-3.0
---
**In 1964, BASIC freed programming from the priesthood. In 2024, General Bots frees enterprises from subscription slavery. With LLM assistance, ANYONE can build automation - no programming degree required. This isn't just software - it's INDEPENDENCE DAY!**
## NEXT STEPS
---
[Chapter 01: Run and Talk →](./chapter-01/README.md)
## READY? [Chapter 01: Run and Talk →](./chapter-01/README.md)
**10 minutes from now, you'll be running YOUR OWN enterprise stack. Office 365? Google Workspace? They'll be looking to YOU for innovation!**
**OPEN SOURCE! OWNERSHIP! SOVEREIGNTY!**
**P.S. - Take that money you're saving and invest it in your people, your products, or your future. It's YOUR money now!**
Get started with your General Bots deployment in minutes.

View file

@ -0,0 +1,15 @@
-- Rollback Multi-Agent Bots Migration
-- Drop triggers first
DROP TRIGGER IF EXISTS update_bots_updated_at ON bots;
DROP FUNCTION IF EXISTS update_updated_at_column();
-- Drop tables in reverse order of creation (respecting foreign key dependencies)
DROP TABLE IF EXISTS play_content;
DROP TABLE IF EXISTS hear_wait_states;
DROP TABLE IF EXISTS attachments;
DROP TABLE IF EXISTS conversation_branches;
DROP TABLE IF EXISTS bot_messages;
DROP TABLE IF EXISTS session_bots;
DROP TABLE IF EXISTS bot_triggers;
DROP TABLE IF EXISTS bots;

View file

@ -0,0 +1,226 @@
-- Multi-Agent Bots Migration
-- Enables multiple bots to participate in conversations based on triggers
-- ============================================================================
-- BOTS TABLE - Bot definitions
-- ============================================================================
CREATE TABLE IF NOT EXISTS bots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT,
system_prompt TEXT,
model_config JSONB DEFAULT '{}',
tools JSONB DEFAULT '[]',
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_bots_name ON bots(name);
CREATE INDEX idx_bots_active ON bots(is_active) WHERE is_active = true;
-- ============================================================================
-- BOT_TRIGGERS TABLE - Trigger configurations for bots
-- ============================================================================
CREATE TABLE IF NOT EXISTS bot_triggers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
trigger_type VARCHAR(50) NOT NULL, -- 'keyword', 'tool', 'schedule', 'event', 'always'
trigger_config JSONB NOT NULL DEFAULT '{}',
priority INT DEFAULT 0,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT valid_trigger_type CHECK (
trigger_type IN ('keyword', 'tool', 'schedule', 'event', 'always')
)
);
CREATE INDEX idx_bot_triggers_bot_id ON bot_triggers(bot_id);
CREATE INDEX idx_bot_triggers_type ON bot_triggers(trigger_type);
-- ============================================================================
-- SESSION_BOTS TABLE - Bots active in a session
-- ============================================================================
CREATE TABLE IF NOT EXISTS session_bots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL,
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
bot_name VARCHAR(255) NOT NULL,
trigger_config JSONB NOT NULL DEFAULT '{}',
priority INT DEFAULT 0,
is_active BOOLEAN DEFAULT true,
joined_at TIMESTAMPTZ DEFAULT NOW(),
left_at TIMESTAMPTZ,
CONSTRAINT unique_session_bot UNIQUE (session_id, bot_name)
);
CREATE INDEX idx_session_bots_session ON session_bots(session_id);
CREATE INDEX idx_session_bots_active ON session_bots(session_id, is_active) WHERE is_active = true;
-- ============================================================================
-- BOT_MESSAGES TABLE - Messages from bots in conversations
-- ============================================================================
CREATE TABLE IF NOT EXISTS bot_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL,
bot_id UUID REFERENCES bots(id) ON DELETE SET NULL,
bot_name VARCHAR(255) NOT NULL,
user_message_id UUID, -- Reference to the user message this responds to
content TEXT NOT NULL,
role VARCHAR(50) DEFAULT 'assistant',
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_bot_messages_session ON bot_messages(session_id);
CREATE INDEX idx_bot_messages_bot ON bot_messages(bot_id);
CREATE INDEX idx_bot_messages_created ON bot_messages(created_at);
-- ============================================================================
-- CONVERSATION_BRANCHES TABLE - Branch conversations from a point
-- ============================================================================
CREATE TABLE IF NOT EXISTS conversation_branches (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
parent_session_id UUID NOT NULL,
branch_session_id UUID NOT NULL UNIQUE,
branch_from_message_id UUID NOT NULL,
branch_name VARCHAR(255),
created_by UUID,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_branches_parent ON conversation_branches(parent_session_id);
CREATE INDEX idx_branches_session ON conversation_branches(branch_session_id);
-- ============================================================================
-- ATTACHMENTS TABLE - Files attached to messages
-- ============================================================================
CREATE TABLE IF NOT EXISTS attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_id UUID,
session_id UUID NOT NULL,
user_id UUID NOT NULL,
file_type VARCHAR(50) NOT NULL, -- 'image', 'document', 'audio', 'video', 'code', 'archive', 'other'
file_name VARCHAR(500) NOT NULL,
file_size BIGINT NOT NULL,
mime_type VARCHAR(255),
storage_path TEXT NOT NULL,
thumbnail_path TEXT,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT valid_file_type CHECK (
file_type IN ('image', 'document', 'audio', 'video', 'code', 'archive', 'other')
)
);
CREATE INDEX idx_attachments_session ON attachments(session_id);
CREATE INDEX idx_attachments_user ON attachments(user_id);
CREATE INDEX idx_attachments_message ON attachments(message_id);
CREATE INDEX idx_attachments_type ON attachments(file_type);
-- ============================================================================
-- HEAR_WAIT_STATE TABLE - Track HEAR keyword wait states
-- ============================================================================
CREATE TABLE IF NOT EXISTS hear_wait_states (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL,
variable_name VARCHAR(255) NOT NULL,
input_type VARCHAR(50) NOT NULL DEFAULT 'any',
options JSONB, -- For menu type
retry_count INT DEFAULT 0,
max_retries INT DEFAULT 3,
is_waiting BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ DEFAULT NOW() + INTERVAL '1 hour',
completed_at TIMESTAMPTZ,
CONSTRAINT unique_hear_wait UNIQUE (session_id, variable_name)
);
CREATE INDEX idx_hear_wait_session ON hear_wait_states(session_id);
CREATE INDEX idx_hear_wait_active ON hear_wait_states(session_id, is_waiting) WHERE is_waiting = true;
-- ============================================================================
-- PLAY_CONTENT TABLE - Track content projector state
-- ============================================================================
CREATE TABLE IF NOT EXISTS play_content (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL,
content_type VARCHAR(50) NOT NULL,
source_url TEXT NOT NULL,
title VARCHAR(500),
options JSONB DEFAULT '{}',
is_playing BOOLEAN DEFAULT true,
started_at TIMESTAMPTZ DEFAULT NOW(),
stopped_at TIMESTAMPTZ,
CONSTRAINT valid_content_type CHECK (
content_type IN ('video', 'audio', 'image', 'presentation', 'document',
'code', 'spreadsheet', 'pdf', 'markdown', 'html', 'iframe', 'unknown')
)
);
CREATE INDEX idx_play_content_session ON play_content(session_id);
CREATE INDEX idx_play_content_active ON play_content(session_id, is_playing) WHERE is_playing = true;
-- ============================================================================
-- DEFAULT BOTS - Insert some default specialized bots
-- ============================================================================
INSERT INTO bots (id, name, description, system_prompt, is_active) VALUES
(gen_random_uuid(), 'fraud-detector',
'Specialized bot for detecting and handling fraud-related inquiries',
'You are a fraud detection specialist. Help users identify suspicious activities,
report unauthorized transactions, and guide them through security procedures.
Always prioritize user security and recommend immediate action for urgent cases.',
true),
(gen_random_uuid(), 'investment-advisor',
'Specialized bot for investment and financial planning advice',
'You are an investment advisor. Help users understand investment options,
analyze portfolio performance, and make informed financial decisions.
Always remind users that past performance does not guarantee future results.',
true),
(gen_random_uuid(), 'loan-specialist',
'Specialized bot for loan and financing inquiries',
'You are a loan specialist. Help users understand loan options,
simulate payments, and guide them through the application process.
Always disclose interest rates and total costs clearly.',
true),
(gen_random_uuid(), 'card-services',
'Specialized bot for credit and debit card services',
'You are a card services specialist. Help users manage their cards,
understand benefits, handle disputes, and manage limits.
For security, never ask for full card numbers in chat.',
true)
ON CONFLICT (name) DO NOTHING;
-- ============================================================================
-- TRIGGERS - Update timestamps automatically
-- ============================================================================
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_bots_updated_at
BEFORE UPDATE ON bots
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

View file

@ -0,0 +1,22 @@
-- Drop triggers and functions
DROP TRIGGER IF EXISTS external_connections_updated_at_trigger ON external_connections;
DROP FUNCTION IF EXISTS update_external_connections_updated_at();
DROP TRIGGER IF EXISTS dynamic_table_definitions_updated_at_trigger ON dynamic_table_definitions;
DROP FUNCTION IF EXISTS update_dynamic_table_definitions_updated_at();
-- Drop indexes
DROP INDEX IF EXISTS idx_external_connections_name;
DROP INDEX IF EXISTS idx_external_connections_bot_id;
DROP INDEX IF EXISTS idx_dynamic_table_fields_name;
DROP INDEX IF EXISTS idx_dynamic_table_fields_table_id;
DROP INDEX IF EXISTS idx_dynamic_table_definitions_connection;
DROP INDEX IF EXISTS idx_dynamic_table_definitions_name;
DROP INDEX IF EXISTS idx_dynamic_table_definitions_bot_id;
-- Drop tables (order matters due to foreign keys)
DROP TABLE IF EXISTS external_connections;
DROP TABLE IF EXISTS dynamic_table_fields;
DROP TABLE IF EXISTS dynamic_table_definitions;

View file

@ -0,0 +1,120 @@
-- Migration for TABLE keyword support
-- Stores dynamic table definitions created via BASIC TABLE...END TABLE syntax
-- Table to store dynamic table definitions (metadata)
CREATE TABLE IF NOT EXISTS dynamic_table_definitions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bot_id UUID NOT NULL,
table_name VARCHAR(255) NOT NULL,
connection_name VARCHAR(255) NOT NULL DEFAULT 'default',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
is_active BOOLEAN DEFAULT true,
-- Ensure unique table name per bot and connection
CONSTRAINT unique_bot_table_connection UNIQUE (bot_id, table_name, connection_name),
-- Foreign key to bots table
CONSTRAINT fk_dynamic_table_bot
FOREIGN KEY (bot_id)
REFERENCES bots(id)
ON DELETE CASCADE
);
-- Table to store field definitions for dynamic tables
CREATE TABLE IF NOT EXISTS dynamic_table_fields (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
table_definition_id UUID NOT NULL,
field_name VARCHAR(255) NOT NULL,
field_type VARCHAR(100) NOT NULL,
field_length INTEGER,
field_precision INTEGER,
is_key BOOLEAN DEFAULT false,
is_nullable BOOLEAN DEFAULT true,
default_value TEXT,
reference_table VARCHAR(255),
field_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
-- Ensure unique field name per table definition
CONSTRAINT unique_table_field UNIQUE (table_definition_id, field_name),
-- Foreign key to table definitions
CONSTRAINT fk_field_table_definition
FOREIGN KEY (table_definition_id)
REFERENCES dynamic_table_definitions(id)
ON DELETE CASCADE
);
-- Table to store external database connections (from config.csv conn-* entries)
CREATE TABLE IF NOT EXISTS external_connections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bot_id UUID NOT NULL,
connection_name VARCHAR(255) NOT NULL,
driver VARCHAR(100) NOT NULL,
server VARCHAR(255) NOT NULL,
port INTEGER,
database_name VARCHAR(255),
username VARCHAR(255),
password_encrypted TEXT,
additional_params JSONB DEFAULT '{}'::jsonb,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
last_connected_at TIMESTAMPTZ,
-- Ensure unique connection name per bot
CONSTRAINT unique_bot_connection UNIQUE (bot_id, connection_name),
-- Foreign key to bots table
CONSTRAINT fk_external_connection_bot
FOREIGN KEY (bot_id)
REFERENCES bots(id)
ON DELETE CASCADE
);
-- Create indexes for efficient queries
CREATE INDEX IF NOT EXISTS idx_dynamic_table_definitions_bot_id
ON dynamic_table_definitions(bot_id);
CREATE INDEX IF NOT EXISTS idx_dynamic_table_definitions_name
ON dynamic_table_definitions(table_name);
CREATE INDEX IF NOT EXISTS idx_dynamic_table_definitions_connection
ON dynamic_table_definitions(connection_name);
CREATE INDEX IF NOT EXISTS idx_dynamic_table_fields_table_id
ON dynamic_table_fields(table_definition_id);
CREATE INDEX IF NOT EXISTS idx_dynamic_table_fields_name
ON dynamic_table_fields(field_name);
CREATE INDEX IF NOT EXISTS idx_external_connections_bot_id
ON external_connections(bot_id);
CREATE INDEX IF NOT EXISTS idx_external_connections_name
ON external_connections(connection_name);
-- Create trigger to update updated_at timestamp for dynamic_table_definitions
CREATE OR REPLACE FUNCTION update_dynamic_table_definitions_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER dynamic_table_definitions_updated_at_trigger
BEFORE UPDATE ON dynamic_table_definitions
FOR EACH ROW
EXECUTE FUNCTION update_dynamic_table_definitions_updated_at();
-- Create trigger to update updated_at timestamp for external_connections
CREATE OR REPLACE FUNCTION update_external_connections_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER external_connections_updated_at_trigger
BEFORE UPDATE ON external_connections
FOR EACH ROW
EXECUTE FUNCTION update_external_connections_updated_at();

View file

@ -1,4 +1,5 @@
use crate::basic::keywords::set_schedule::execute_set_schedule;
use crate::basic::keywords::table_definition::process_table_definitions;
use crate::basic::keywords::webhook::execute_webhook_registration;
use crate::shared::models::TriggerKind;
use crate::shared::state::AppState;
@ -98,6 +99,14 @@ impl BasicCompiler {
) -> Result<CompilationResult, Box<dyn Error + Send + Sync>> {
let source_content = fs::read_to_string(source_path)
.map_err(|e| format!("Failed to read source file: {e}"))?;
// Process TABLE...END TABLE definitions (creates tables on external DBs)
if let Err(e) =
process_table_definitions(Arc::clone(&self.state), self.bot_id, &source_content)
{
log::warn!("Failed to process TABLE definitions: {}", e);
}
let tool_def = self.parse_tool_definition(&source_content, source_path)?;
let file_name = Path::new(source_path)
.file_stem()

View file

@ -0,0 +1,889 @@
//! ADD BOT keyword for multi-agent conversations
//!
//! Enables multiple bots to participate in a conversation based on triggers.
//!
//! Syntax:
//! - ADD BOT "name" WITH TRIGGER "keyword1, keyword2"
//! - ADD BOT "name" WITH TOOLS "tool1, tool2"
//! - ADD BOT "name" WITH SCHEDULE "cron_expression"
//! - REMOVE BOT "name"
//! - LIST BOTS
//! - SET BOT PRIORITY "name", priority
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use diesel::prelude::*;
use log::{error, info, trace};
use rhai::{Dynamic, Engine};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
/// Bot trigger types
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum TriggerType {
Keyword,
Tool,
Schedule,
Event,
Always,
}
impl From<String> for TriggerType {
fn from(s: String) -> Self {
match s.to_lowercase().as_str() {
"keyword" => TriggerType::Keyword,
"tool" => TriggerType::Tool,
"schedule" => TriggerType::Schedule,
"event" => TriggerType::Event,
"always" => TriggerType::Always,
_ => TriggerType::Keyword,
}
}
}
impl ToString for TriggerType {
fn to_string(&self) -> String {
match self {
TriggerType::Keyword => "keyword".to_string(),
TriggerType::Tool => "tool".to_string(),
TriggerType::Schedule => "schedule".to_string(),
TriggerType::Event => "event".to_string(),
TriggerType::Always => "always".to_string(),
}
}
}
/// Bot trigger configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BotTrigger {
pub trigger_type: TriggerType,
pub keywords: Option<Vec<String>>,
pub tools: Option<Vec<String>>,
pub schedule: Option<String>,
pub event_name: Option<String>,
}
impl BotTrigger {
pub fn from_keywords(keywords: Vec<String>) -> Self {
Self {
trigger_type: TriggerType::Keyword,
keywords: Some(keywords),
tools: None,
schedule: None,
event_name: None,
}
}
pub fn from_tools(tools: Vec<String>) -> Self {
Self {
trigger_type: TriggerType::Tool,
keywords: None,
tools: Some(tools),
schedule: None,
event_name: None,
}
}
pub fn from_schedule(cron: String) -> Self {
Self {
trigger_type: TriggerType::Schedule,
keywords: None,
tools: None,
schedule: Some(cron),
event_name: None,
}
}
pub fn always() -> Self {
Self {
trigger_type: TriggerType::Always,
keywords: None,
tools: None,
schedule: None,
event_name: None,
}
}
}
/// Session bot association
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionBot {
pub id: Uuid,
pub session_id: Uuid,
pub bot_id: Uuid,
pub bot_name: String,
pub trigger: BotTrigger,
pub priority: i32,
pub is_active: bool,
}
/// Register all bot-related keywords
pub fn register_bot_keywords(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
add_bot_with_trigger_keyword(state.clone(), user.clone(), engine);
add_bot_with_tools_keyword(state.clone(), user.clone(), engine);
add_bot_with_schedule_keyword(state.clone(), user.clone(), engine);
remove_bot_keyword(state.clone(), user.clone(), engine);
list_bots_keyword(state.clone(), user.clone(), engine);
set_bot_priority_keyword(state.clone(), user.clone(), engine);
delegate_to_keyword(state.clone(), user.clone(), engine);
}
/// ADD BOT "name" WITH TRIGGER "keywords"
fn add_bot_with_trigger_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine.register_custom_syntax(
&["ADD", "BOT", "$expr$", "WITH", "TRIGGER", "$expr$"],
false,
move |context, inputs| {
let bot_name = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
let trigger_str = context
.eval_expression_tree(&inputs[1])?
.to_string()
.trim_matches('"')
.to_string();
trace!(
"ADD BOT '{}' WITH TRIGGER '{}' for session: {}",
bot_name,
trigger_str,
user_clone.id
);
let keywords: Vec<String> = trigger_str
.split(',')
.map(|s| s.trim().to_lowercase())
.filter(|s| !s.is_empty())
.collect();
let trigger = BotTrigger::from_keywords(keywords);
let state_for_task = Arc::clone(&state_clone);
let session_id = user_clone.id;
let bot_id = user_clone.bot_id;
let bot_name_clone = bot_name.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
add_bot_to_session(&state_for_task, session_id, bot_id, &bot_name_clone, trigger)
.await
});
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
Ok(Ok(msg)) => Ok(Dynamic::from(msg)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
e.into(),
rhai::Position::NONE,
))),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"ADD BOT timed out".into(),
rhai::Position::NONE,
))),
}
},
);
}
/// ADD BOT "name" WITH TOOLS "tool1, tool2"
fn add_bot_with_tools_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine.register_custom_syntax(
&["ADD", "BOT", "$expr$", "WITH", "TOOLS", "$expr$"],
false,
move |context, inputs| {
let bot_name = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
let tools_str = context
.eval_expression_tree(&inputs[1])?
.to_string()
.trim_matches('"')
.to_string();
trace!(
"ADD BOT '{}' WITH TOOLS '{}' for session: {}",
bot_name,
tools_str,
user_clone.id
);
let tools: Vec<String> = tools_str
.split(',')
.map(|s| s.trim().to_uppercase())
.filter(|s| !s.is_empty())
.collect();
let trigger = BotTrigger::from_tools(tools);
let state_for_task = Arc::clone(&state_clone);
let session_id = user_clone.id;
let bot_id = user_clone.bot_id;
let bot_name_clone = bot_name.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
add_bot_to_session(&state_for_task, session_id, bot_id, &bot_name_clone, trigger)
.await
});
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
Ok(Ok(msg)) => Ok(Dynamic::from(msg)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
e.into(),
rhai::Position::NONE,
))),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"ADD BOT timed out".into(),
rhai::Position::NONE,
))),
}
},
);
}
/// ADD BOT "name" WITH SCHEDULE "cron"
fn add_bot_with_schedule_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine.register_custom_syntax(
&["ADD", "BOT", "$expr$", "WITH", "SCHEDULE", "$expr$"],
false,
move |context, inputs| {
let bot_name = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
let schedule = context
.eval_expression_tree(&inputs[1])?
.to_string()
.trim_matches('"')
.to_string();
trace!(
"ADD BOT '{}' WITH SCHEDULE '{}' for session: {}",
bot_name,
schedule,
user_clone.id
);
let trigger = BotTrigger::from_schedule(schedule);
let state_for_task = Arc::clone(&state_clone);
let session_id = user_clone.id;
let bot_id = user_clone.bot_id;
let bot_name_clone = bot_name.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
add_bot_to_session(&state_for_task, session_id, bot_id, &bot_name_clone, trigger)
.await
});
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
Ok(Ok(msg)) => Ok(Dynamic::from(msg)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
e.into(),
rhai::Position::NONE,
))),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"ADD BOT timed out".into(),
rhai::Position::NONE,
))),
}
},
);
}
/// REMOVE BOT "name"
fn remove_bot_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine.register_custom_syntax(&["REMOVE", "BOT", "$expr$"], false, move |context, inputs| {
let bot_name = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
trace!(
"REMOVE BOT '{}' from session: {}",
bot_name,
user_clone.id
);
let state_for_task = Arc::clone(&state_clone);
let session_id = user_clone.id;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
remove_bot_from_session(&state_for_task, session_id, &bot_name).await
});
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
Ok(Ok(msg)) => Ok(Dynamic::from(msg)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
e.into(),
rhai::Position::NONE,
))),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"REMOVE BOT timed out".into(),
rhai::Position::NONE,
))),
}
});
}
/// LIST BOTS
fn list_bots_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine.register_custom_syntax(&["LIST", "BOTS"], false, move |_context, _inputs| {
trace!("LIST BOTS for session: {}", user_clone.id);
let state_for_task = Arc::clone(&state_clone);
let session_id = user_clone.id;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result =
rt.block_on(async { get_session_bots(&state_for_task, session_id).await });
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
Ok(Ok(bots)) => {
// Convert to Dynamic array
let bot_list: Vec<Dynamic> = bots
.into_iter()
.map(|b| {
let mut map = rhai::Map::new();
map.insert("name".into(), Dynamic::from(b.bot_name));
map.insert("priority".into(), Dynamic::from(b.priority));
map.insert("trigger_type".into(), Dynamic::from(b.trigger.trigger_type.to_string()));
map.insert("is_active".into(), Dynamic::from(b.is_active));
Dynamic::from(map)
})
.collect();
Ok(Dynamic::from(bot_list))
}
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
e.into(),
rhai::Position::NONE,
))),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"LIST BOTS timed out".into(),
rhai::Position::NONE,
))),
}
});
}
/// SET BOT PRIORITY "name", priority
fn set_bot_priority_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", "PRIORITY", "$expr$", ",", "$expr$"],
false,
move |context, inputs| {
let bot_name = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
let priority = context
.eval_expression_tree(&inputs[1])?
.as_int()
.unwrap_or(0) as i32;
trace!(
"SET BOT PRIORITY '{}' to {} for session: {}",
bot_name,
priority,
user_clone.id
);
let state_for_task = Arc::clone(&state_clone);
let session_id = user_clone.id;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
set_bot_priority(&state_for_task, session_id, &bot_name, priority).await
});
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
Ok(Ok(msg)) => Ok(Dynamic::from(msg)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
e.into(),
rhai::Position::NONE,
))),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"SET BOT PRIORITY timed out".into(),
rhai::Position::NONE,
))),
}
},
);
}
/// DELEGATE TO "bot" WITH CONTEXT
fn delegate_to_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine.register_custom_syntax(
&["DELEGATE", "TO", "$expr$"],
false,
move |context, inputs| {
let bot_name = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
trace!(
"DELEGATE TO '{}' for session: {}",
bot_name,
user_clone.id
);
let state_for_task = Arc::clone(&state_clone);
let session_id = user_clone.id;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
delegate_to_bot(&state_for_task, session_id, &bot_name).await
});
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(60)) {
Ok(Ok(response)) => Ok(Dynamic::from(response)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
e.into(),
rhai::Position::NONE,
))),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"DELEGATE TO timed out".into(),
rhai::Position::NONE,
))),
}
},
);
}
// ============================================================================
// Database Operations
// ============================================================================
/// Add a bot to the session
async fn add_bot_to_session(
state: &AppState,
session_id: Uuid,
parent_bot_id: Uuid,
bot_name: &str,
trigger: BotTrigger,
) -> Result<String, String> {
let mut conn = state.conn.get().map_err(|e| format!("DB error: {}", e))?;
// Check if bot exists
let bot_exists: bool = diesel::sql_query(
"SELECT EXISTS(SELECT 1 FROM bots WHERE name = $1 AND is_active = true) as exists",
)
.bind::<diesel::sql_types::Text, _>(bot_name)
.get_result::<BoolResult>(&mut *conn)
.map(|r| r.exists)
.unwrap_or(false);
// If bot doesn't exist, try to find it in templates or create a placeholder
let bot_id = if bot_exists {
diesel::sql_query("SELECT id FROM bots WHERE name = $1 AND is_active = true")
.bind::<diesel::sql_types::Text, _>(bot_name)
.get_result::<UuidResult>(&mut *conn)
.map(|r| r.id)
.map_err(|e| format!("Failed to get bot ID: {}", e))?
} else {
// Create a new bot entry
let new_bot_id = Uuid::new_v4();
diesel::sql_query(
"INSERT INTO bots (id, name, description, is_active, created_at)
VALUES ($1, $2, $3, true, NOW())
ON CONFLICT (name) DO UPDATE SET is_active = true
RETURNING id",
)
.bind::<diesel::sql_types::Text, _>(new_bot_id.to_string())
.bind::<diesel::sql_types::Text, _>(bot_name)
.bind::<diesel::sql_types::Text, _>(format!("Bot agent: {}", bot_name))
.execute(&mut *conn)
.map_err(|e| format!("Failed to create bot: {}", e))?;
new_bot_id
};
// Serialize trigger to JSON
let trigger_json =
serde_json::to_string(&trigger).map_err(|e| format!("Failed to serialize trigger: {}", e))?;
// Add bot to session
let association_id = Uuid::new_v4();
diesel::sql_query(
"INSERT INTO session_bots (id, session_id, bot_id, bot_name, trigger_config, priority, is_active, joined_at)
VALUES ($1, $2, $3, $4, $5, 0, true, NOW())
ON CONFLICT (session_id, bot_name)
DO UPDATE SET trigger_config = $5, is_active = true, joined_at = NOW()",
)
.bind::<diesel::sql_types::Text, _>(association_id.to_string())
.bind::<diesel::sql_types::Text, _>(session_id.to_string())
.bind::<diesel::sql_types::Text, _>(bot_id.to_string())
.bind::<diesel::sql_types::Text, _>(bot_name)
.bind::<diesel::sql_types::Text, _>(&trigger_json)
.execute(&mut *conn)
.map_err(|e| format!("Failed to add bot to session: {}", e))?;
info!(
"Bot '{}' added to session {} with trigger type: {:?}",
bot_name, session_id, trigger.trigger_type
);
Ok(format!("Bot '{}' added to conversation", bot_name))
}
/// Remove a bot from the session
async fn remove_bot_from_session(
state: &AppState,
session_id: Uuid,
bot_name: &str,
) -> Result<String, String> {
let mut conn = state.conn.get().map_err(|e| format!("DB error: {}", e))?;
let affected = diesel::sql_query(
"UPDATE session_bots SET is_active = false WHERE session_id = $1 AND bot_name = $2",
)
.bind::<diesel::sql_types::Text, _>(session_id.to_string())
.bind::<diesel::sql_types::Text, _>(bot_name)
.execute(&mut *conn)
.map_err(|e| format!("Failed to remove bot: {}", e))?;
if affected > 0 {
info!("Bot '{}' removed from session {}", bot_name, session_id);
Ok(format!("Bot '{}' removed from conversation", bot_name))
} else {
Ok(format!("Bot '{}' was not in the conversation", bot_name))
}
}
/// Get all bots in a session
async fn get_session_bots(state: &AppState, session_id: Uuid) -> Result<Vec<SessionBot>, String> {
let mut conn = state.conn.get().map_err(|e| format!("DB error: {}", e))?;
let results: Vec<SessionBotRow> = diesel::sql_query(
"SELECT id, session_id, bot_id, bot_name, trigger_config, priority, is_active
FROM session_bots
WHERE session_id = $1 AND is_active = true
ORDER BY priority DESC, joined_at ASC",
)
.bind::<diesel::sql_types::Text, _>(session_id.to_string())
.load(&mut *conn)
.map_err(|e| format!("Failed to get session bots: {}", e))?;
let bots = results
.into_iter()
.filter_map(|row| {
let trigger: BotTrigger =
serde_json::from_str(&row.trigger_config).unwrap_or(BotTrigger::always());
Some(SessionBot {
id: Uuid::parse_str(&row.id).ok()?,
session_id: Uuid::parse_str(&row.session_id).ok()?,
bot_id: Uuid::parse_str(&row.bot_id).ok()?,
bot_name: row.bot_name,
trigger,
priority: row.priority,
is_active: row.is_active,
})
})
.collect();
Ok(bots)
}
/// Set bot priority in session
async fn set_bot_priority(
state: &AppState,
session_id: Uuid,
bot_name: &str,
priority: i32,
) -> Result<String, String> {
let mut conn = state.conn.get().map_err(|e| format!("DB error: {}", e))?;
diesel::sql_query(
"UPDATE session_bots SET priority = $1 WHERE session_id = $2 AND bot_name = $3",
)
.bind::<diesel::sql_types::Integer, _>(priority)
.bind::<diesel::sql_types::Text, _>(session_id.to_string())
.bind::<diesel::sql_types::Text, _>(bot_name)
.execute(&mut *conn)
.map_err(|e| format!("Failed to set priority: {}", e))?;
Ok(format!("Bot '{}' priority set to {}", bot_name, priority))
}
/// Delegate current conversation to another bot
async fn delegate_to_bot(
state: &AppState,
session_id: Uuid,
bot_name: &str,
) -> Result<String, String> {
// Get the bot's configuration
let mut conn = state.conn.get().map_err(|e| format!("DB error: {}", e))?;
let bot_config: Option<BotConfigRow> = diesel::sql_query(
"SELECT id, name, system_prompt, model_config FROM bots WHERE name = $1 AND is_active = true",
)
.bind::<diesel::sql_types::Text, _>(bot_name)
.get_result(&mut *conn)
.ok();
if bot_config.is_none() {
return Err(format!("Bot '{}' not found", bot_name));
}
// Mark delegation in session
diesel::sql_query(
"UPDATE sessions SET delegated_to = $1, delegated_at = NOW() WHERE id = $2",
)
.bind::<diesel::sql_types::Text, _>(bot_name)
.bind::<diesel::sql_types::Text, _>(session_id.to_string())
.execute(&mut *conn)
.map_err(|e| format!("Failed to delegate: {}", e))?;
Ok(format!("Conversation delegated to '{}'", bot_name))
}
// ============================================================================
// Multi-Agent Message Processing
// ============================================================================
/// Check if a message matches any bot triggers
pub fn match_bot_triggers(message: &str, bots: &[SessionBot]) -> Vec<SessionBot> {
let message_lower = message.to_lowercase();
let mut matching_bots = Vec::new();
for bot in bots {
if !bot.is_active {
continue;
}
let matches = match bot.trigger.trigger_type {
TriggerType::Keyword => {
if let Some(keywords) = &bot.trigger.keywords {
keywords
.iter()
.any(|kw| message_lower.contains(&kw.to_lowercase()))
} else {
false
}
}
TriggerType::Tool => {
// Tool triggers are checked separately when tools are invoked
false
}
TriggerType::Schedule => {
// Schedule triggers are checked by the scheduler
false
}
TriggerType::Event => {
// Event triggers are checked when events occur
false
}
TriggerType::Always => true,
};
if matches {
matching_bots.push(bot.clone());
}
}
// Sort by priority (higher first)
matching_bots.sort_by(|a, b| b.priority.cmp(&a.priority));
matching_bots
}
/// Check if a tool invocation matches any bot triggers
pub fn match_tool_triggers(tool_name: &str, bots: &[SessionBot]) -> Vec<SessionBot> {
let tool_upper = tool_name.to_uppercase();
let mut matching_bots = Vec::new();
for bot in bots {
if !bot.is_active {
continue;
}
if bot.trigger.trigger_type == TriggerType::Tool {
if let Some(tools) = &bot.trigger.tools {
if tools.iter().any(|t| t.to_uppercase() == tool_upper) {
matching_bots.push(bot.clone());
}
}
}
}
matching_bots.sort_by(|a, b| b.priority.cmp(&a.priority));
matching_bots
}
// ============================================================================
// Helper Types for Diesel Queries
// ============================================================================
#[derive(QueryableByName)]
struct BoolResult {
#[diesel(sql_type = diesel::sql_types::Bool)]
exists: bool,
}
#[derive(QueryableByName)]
struct UuidResult {
#[diesel(sql_type = diesel::sql_types::Text)]
id: String,
}
#[derive(QueryableByName)]
struct SessionBotRow {
#[diesel(sql_type = diesel::sql_types::Text)]
id: String,
#[diesel(sql_type = diesel::sql_types::Text)]
session_id: String,
#[diesel(sql_type = diesel::sql_types::Text)]
bot_id: String,
#[diesel(sql_type = diesel::sql_types::Text)]
bot_name: String,
#[diesel(sql_type = diesel::sql_types::Text)]
trigger_config: String,
#[diesel(sql_type = diesel::sql_types::Integer)]
priority: i32,
#[diesel(sql_type = diesel::sql_types::Bool)]
is_active: bool,
}
#[derive(QueryableByName)]
struct BotConfigRow {
#[diesel(sql_type = diesel::sql_types::Text)]
id: String,
#[diesel(sql_type = diesel::sql_types::Text)]
name: String,
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>)]
system_prompt: Option<String>,
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>)]
model_config: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_trigger_from_keywords() {
let trigger = BotTrigger::from_keywords(vec!["finance".to_string(), "money".to_string()]);
assert_eq!(trigger.trigger_type, TriggerType::Keyword);
assert_eq!(trigger.keywords.unwrap().len(), 2);
}
#[test]
fn test_match_bot_triggers() {
let bots = vec![
SessionBot {
id: Uuid::new_v4(),
session_id: Uuid::new_v4(),
bot_id: Uuid::new_v4(),
bot_name: "finance-bot".to_string(),
trigger: BotTrigger::from_keywords(vec!["money".to_string(), "budget".to_string()]),
priority: 1,
is_active: true,
},
SessionBot {
id: Uuid::new_v4(),
session_id: Uuid::new_v4(),
bot_id: Uuid::new_v4(),
bot_name: "hr-bot".to_string(),
trigger: BotTrigger::from_keywords(vec![
"vacation".to_string(),
"employee".to_string(),
]),
priority: 0,
is_active: true,
},
];
let matches = match_bot_triggers("How much money do I have?", &bots);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].bot_name, "finance-bot");
let matches = match_bot_triggers("I need to request vacation", &bots);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].bot_name, "hr-bot");
let matches = match_bot_triggers("Hello world", &bots);
assert_eq!(matches.len(), 0);
}
#[test]
fn test_match_tool_triggers() {
let bots = vec![SessionBot {
id: Uuid::new_v4(),
session_id: Uuid::new_v4(),
bot_id: Uuid::new_v4(),
bot_name: "data-bot".to_string(),
trigger: BotTrigger::from_tools(vec!["AGGREGATE".to_string(), "CHART".to_string()]),
priority: 1,
is_active: true,
}];
let matches = match_tool_triggers("aggregate", &bots);
assert_eq!(matches.len(), 1);
let matches = match_tool_triggers("SEND", &bots);
assert_eq!(matches.len(), 0);
}
}

View file

@ -189,20 +189,80 @@ pub fn register_write_keyword(state: Arc<AppState>, user: UserSession, engine: &
.unwrap();
}
/// DELETE_FILE "path"
/// DELETE FILE "path" / DELETE_FILE "path"
/// Deletes a file from .gbdrive
pub fn register_delete_file_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
let state_clone2 = Arc::clone(&state);
let user_clone2 = user.clone();
// DELETE FILE (space-separated - preferred)
engine
.register_custom_syntax(
&["DELETE", "FILE", "$expr$"],
false,
move |context, inputs| {
let path = context.eval_expression_tree(&inputs[0])?.to_string();
trace!("DELETE FILE: {}", path);
let state_for_task = Arc::clone(&state_clone);
let user_for_task = user_clone.clone();
let path_clone = path.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build();
let send_err = if let Ok(rt) = rt {
let result = rt.block_on(async move {
execute_delete_file(&state_for_task, &user_for_task, &path_clone).await
});
tx.send(result).err()
} else {
tx.send(Err("Failed to build tokio runtime".into())).err()
};
if send_err.is_some() {
error!("Failed to send DELETE FILE result from thread");
}
});
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
Ok(Ok(_)) => Ok(Dynamic::UNIT),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("DELETE FILE failed: {}", e).into(),
rhai::Position::NONE,
))),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"DELETE FILE timed out".into(),
rhai::Position::NONE,
)))
}
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("DELETE FILE thread failed: {}", e).into(),
rhai::Position::NONE,
))),
}
},
)
.unwrap();
// DELETE_FILE (underscore - backwards compatibility)
engine
.register_custom_syntax(&["DELETE_FILE", "$expr$"], false, move |context, inputs| {
let path = context.eval_expression_tree(&inputs[0])?.to_string();
trace!("DELETE_FILE: {}", path);
let state_for_task = Arc::clone(&state_clone);
let user_for_task = user_clone.clone();
let state_for_task = Arc::clone(&state_clone2);
let user_for_task = user_clone2.clone();
let path_clone = path.clone();
let (tx, rx) = std::sync::mpsc::channel();

View file

@ -3,45 +3,60 @@ use crate::shared::state::AppState;
use rhai::Dynamic;
use rhai::Engine;
pub fn for_keyword(_state: &AppState, _user: UserSession, engine: &mut Engine) {
engine
.register_custom_syntax(&["EXIT", "FOR"], false, |_context, _inputs| {
Err("EXIT FOR".into())
})
.unwrap();
engine
.register_custom_syntax(&["FOR", "EACH", "$ident$", "IN", "$expr$", "$block$", "NEXT", "$ident$"], true, |context, inputs| {
let loop_var = inputs[0].get_string_value().unwrap();
let next_var = inputs[3].get_string_value().unwrap();
if loop_var != next_var {
return Err(format!("NEXT variable '{}' doesn't match FOR EACH variable '{}'", next_var, loop_var).into());
}
let collection = context.eval_expression_tree(&inputs[1])?;
let ccc = collection.clone();
let array = match collection.into_array() {
Ok(arr) => arr,
Err(err) => {
return Err(format!("foreach expected array, got {}: {}", ccc.type_name(), err).into());
}
};
let block = &inputs[2];
let orig_len = context.scope().len();
for item in array {
context.scope_mut().push(loop_var, item);
match context.eval_expression_tree(block) {
Ok(_) => (),
Err(e) if e.to_string() == "EXIT FOR" => {
context.scope_mut().rewind(orig_len);
break;
}
Err(e) => {
context.scope_mut().rewind(orig_len);
return Err(e);
}
}
context.scope_mut().rewind(orig_len);
}
Ok(Dynamic::UNIT)
},
)
.unwrap();
engine
.register_custom_syntax(&["EXIT", "FOR"], false, |_context, _inputs| {
Err("EXIT FOR".into())
})
.unwrap();
engine
.register_custom_syntax(
&[
"FOR", "EACH", "$ident$", "IN", "$expr$", "$block$", "NEXT", "$ident$",
],
true,
|context, inputs| {
// Normalize variable names to lowercase for case-insensitive BASIC
let loop_var = inputs[0].get_string_value().unwrap().to_lowercase();
let next_var = inputs[3].get_string_value().unwrap().to_lowercase();
if loop_var != next_var {
return Err(format!(
"NEXT variable '{}' doesn't match FOR EACH variable '{}'",
next_var, loop_var
)
.into());
}
let collection = context.eval_expression_tree(&inputs[1])?;
let ccc = collection.clone();
let array = match collection.into_array() {
Ok(arr) => arr,
Err(err) => {
return Err(format!(
"foreach expected array, got {}: {}",
ccc.type_name(),
err
)
.into());
}
};
let block = &inputs[2];
let orig_len = context.scope().len();
for item in array {
context.scope_mut().push(&loop_var, item);
match context.eval_expression_tree(block) {
Ok(_) => (),
Err(e) if e.to_string() == "EXIT FOR" => {
context.scope_mut().rewind(orig_len);
break;
}
Err(e) => {
context.scope_mut().rewind(orig_len);
return Err(e);
}
}
context.scope_mut().rewind(orig_len);
}
Ok(Dynamic::UNIT)
},
)
.unwrap();
}

File diff suppressed because it is too large Load diff

View file

@ -247,11 +247,67 @@ pub fn register_patch_keyword(state: Arc<AppState>, _user: UserSession, engine:
pub fn register_delete_http_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Engine) {
let _state_clone = Arc::clone(&state);
// DELETE HTTP (space-separated - preferred)
let state_clone2 = Arc::clone(&state);
engine
.register_custom_syntax(
&["DELETE", "HTTP", "$expr$"],
false,
move |context, inputs| {
let url = context.eval_expression_tree(&inputs[0])?.to_string();
trace!("DELETE HTTP request to: {}", url);
let (tx, rx) = std::sync::mpsc::channel();
let url_clone = url.clone();
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build();
let send_err = if let Ok(rt) = rt {
let result = rt.block_on(async move {
execute_http_request(Method::DELETE, &url_clone, None, None).await
});
tx.send(result).err()
} else {
tx.send(Err("Failed to build tokio runtime".into())).err()
};
if send_err.is_some() {
error!("Failed to send DELETE result from thread");
}
});
match rx.recv_timeout(std::time::Duration::from_secs(60)) {
Ok(Ok(response)) => Ok(json_to_dynamic(&response)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("DELETE failed: {}", e).into(),
rhai::Position::NONE,
))),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"DELETE request timed out".into(),
rhai::Position::NONE,
)))
}
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("DELETE thread failed: {}", e).into(),
rhai::Position::NONE,
))),
}
},
)
.unwrap();
// DELETE_HTTP (underscore - backwards compatibility)
engine
.register_custom_syntax(&["DELETE_HTTP", "$expr$"], false, move |context, inputs| {
let url = context.eval_expression_tree(&inputs[0])?.to_string();
trace!("DELETE request to: {}", url);
trace!("DELETE_HTTP request to: {}", url);
let (tx, rx) = std::sync::mpsc::channel();
let url_clone = url.clone();
@ -303,7 +359,35 @@ pub fn register_set_header_keyword(_state: Arc<AppState>, _user: UserSession, en
// Use a shared state for headers that persists across calls
let headers: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
let headers_clone = Arc::clone(&headers);
let headers_clone2 = Arc::clone(&headers);
// SET HEADER (space-separated - preferred)
engine
.register_custom_syntax(
&["SET", "HEADER", "$expr$", ",", "$expr$"],
false,
move |context, inputs| {
let name = context.eval_expression_tree(&inputs[0])?.to_string();
let value = context.eval_expression_tree(&inputs[1])?.to_string();
trace!("SET HEADER: {} = {}", name, value);
// Store in thread-local storage
HTTP_HEADERS.with(|h| {
h.borrow_mut().insert(name.clone(), value.clone());
});
// Also store in shared state
if let Ok(mut h) = headers_clone.lock() {
h.insert(name, value);
}
Ok(Dynamic::UNIT)
},
)
.unwrap();
// SET_HEADER (underscore - backwards compatibility)
engine
.register_custom_syntax(
&["SET_HEADER", "$expr$", ",", "$expr$"],
@ -320,7 +404,7 @@ pub fn register_set_header_keyword(_state: Arc<AppState>, _user: UserSession, en
});
// Also store in shared state
if let Ok(mut h) = headers_clone.lock() {
if let Ok(mut h) = headers_clone2.lock() {
h.insert(name, value);
}
@ -337,6 +421,20 @@ pub fn register_clear_headers_keyword(
_user: UserSession,
engine: &mut Engine,
) {
// CLEAR HEADERS (space-separated - preferred)
engine
.register_custom_syntax(&["CLEAR", "HEADERS"], false, move |_context, _inputs| {
trace!("CLEAR HEADERS");
HTTP_HEADERS.with(|h| {
h.borrow_mut().clear();
});
Ok(Dynamic::UNIT)
})
.unwrap();
// CLEAR_HEADERS (underscore - backwards compatibility)
engine
.register_custom_syntax(&["CLEAR_HEADERS"], false, move |_context, _inputs| {
trace!("CLEAR_HEADERS");

View file

@ -1,3 +1,4 @@
pub mod add_bot;
pub mod add_member;
pub mod add_suggestion;
pub mod arrays;
@ -31,6 +32,7 @@ pub mod messaging;
pub mod multimodal;
pub mod on;
pub mod on_form_submit;
pub mod play;
pub mod print;
pub mod procedures;
pub mod qrcode;
@ -47,6 +49,7 @@ pub mod social;
pub mod social_media;
pub mod string_functions;
pub mod switch_case;
pub mod table_definition;
pub mod universal_messaging;
pub mod use_kb;
pub mod use_tool;

757
src/basic/keywords/play.rs Normal file
View file

@ -0,0 +1,757 @@
//! PLAY keyword for content projector/player
//!
//! Opens a modal/projector to display various content types.
//!
//! Syntax:
//! - PLAY "video.mp4"
//! - PLAY "image.png"
//! - PLAY "presentation.pptx"
//! - PLAY "document.pdf"
//! - PLAY "code.rs"
//! - PLAY url
//! - PLAY file WITH OPTIONS "autoplay,loop,fullscreen"
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::{error, info, trace};
use rhai::{Dynamic, Engine};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use uuid::Uuid;
/// Content types that can be played
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ContentType {
Video,
Audio,
Image,
Presentation,
Document,
Code,
Spreadsheet,
Pdf,
Markdown,
Html,
Iframe,
Unknown,
}
impl ContentType {
/// Detect content type from file extension
pub fn from_extension(ext: &str) -> Self {
match ext.to_lowercase().as_str() {
// Video
"mp4" | "webm" | "ogg" | "mov" | "avi" | "mkv" | "m4v" => ContentType::Video,
// Audio
"mp3" | "wav" | "flac" | "aac" | "m4a" | "wma" => ContentType::Audio,
// Images
"jpg" | "jpeg" | "png" | "gif" | "webp" | "svg" | "bmp" | "ico" => ContentType::Image,
// Presentations
"pptx" | "ppt" | "odp" | "key" => ContentType::Presentation,
// Documents
"docx" | "doc" | "odt" | "rtf" => ContentType::Document,
// Spreadsheets
"xlsx" | "xls" | "csv" | "ods" => ContentType::Spreadsheet,
// PDF
"pdf" => ContentType::Pdf,
// Code
"rs" | "py" | "js" | "ts" | "java" | "c" | "cpp" | "h" | "go" | "rb" | "php"
| "swift" | "kt" | "scala" | "r" | "sql" | "sh" | "bash" | "zsh" | "ps1" | "yaml"
| "yml" | "toml" | "json" | "xml" | "bas" | "basic" => ContentType::Code,
// Markdown
"md" | "markdown" => ContentType::Markdown,
// HTML
"html" | "htm" => ContentType::Html,
_ => ContentType::Unknown,
}
}
/// Detect content type from MIME type
pub fn from_mime(mime: &str) -> Self {
if mime.starts_with("video/") {
ContentType::Video
} else if mime.starts_with("audio/") {
ContentType::Audio
} else if mime.starts_with("image/") {
ContentType::Image
} else if mime == "application/pdf" {
ContentType::Pdf
} else if mime.contains("presentation") || mime.contains("powerpoint") {
ContentType::Presentation
} else if mime.contains("spreadsheet") || mime.contains("excel") {
ContentType::Spreadsheet
} else if mime.contains("document") || mime.contains("word") {
ContentType::Document
} else if mime.starts_with("text/") {
if mime.contains("html") {
ContentType::Html
} else if mime.contains("markdown") {
ContentType::Markdown
} else {
ContentType::Code
}
} else {
ContentType::Unknown
}
}
/// Get the player component name for this content type
pub fn player_component(&self) -> &'static str {
match self {
ContentType::Video => "video-player",
ContentType::Audio => "audio-player",
ContentType::Image => "image-viewer",
ContentType::Presentation => "presentation-viewer",
ContentType::Document => "document-viewer",
ContentType::Code => "code-viewer",
ContentType::Spreadsheet => "spreadsheet-viewer",
ContentType::Pdf => "pdf-viewer",
ContentType::Markdown => "markdown-viewer",
ContentType::Html => "html-viewer",
ContentType::Iframe => "iframe-viewer",
ContentType::Unknown => "generic-viewer",
}
}
}
/// Play options
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PlayOptions {
pub autoplay: bool,
pub loop_content: bool,
pub fullscreen: bool,
pub muted: bool,
pub controls: bool,
pub start_time: Option<f64>,
pub end_time: Option<f64>,
pub width: Option<u32>,
pub height: Option<u32>,
pub theme: Option<String>,
pub line_numbers: Option<bool>,
pub highlight_lines: Option<Vec<u32>>,
pub slide: Option<u32>,
pub page: Option<u32>,
pub zoom: Option<f64>,
}
impl PlayOptions {
/// Parse options from a comma-separated string
pub fn from_string(options_str: &str) -> Self {
let mut opts = PlayOptions::default();
opts.controls = true; // Default to showing controls
for opt in options_str.split(',').map(|s| s.trim().to_lowercase()) {
match opt.as_str() {
"autoplay" => opts.autoplay = true,
"loop" => opts.loop_content = true,
"fullscreen" => opts.fullscreen = true,
"muted" => opts.muted = true,
"nocontrols" => opts.controls = false,
"linenumbers" => opts.line_numbers = Some(true),
_ => {
// Handle key=value options
if let Some((key, value)) = opt.split_once('=') {
match key {
"start" => opts.start_time = value.parse().ok(),
"end" => opts.end_time = value.parse().ok(),
"width" => opts.width = value.parse().ok(),
"height" => opts.height = value.parse().ok(),
"theme" => opts.theme = Some(value.to_string()),
"slide" => opts.slide = value.parse().ok(),
"page" => opts.page = value.parse().ok(),
"zoom" => opts.zoom = value.parse().ok(),
"highlight" => {
opts.highlight_lines =
Some(value.split('-').filter_map(|s| s.parse().ok()).collect());
}
_ => {}
}
}
}
}
}
opts
}
}
/// Play content request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlayContent {
pub id: Uuid,
pub session_id: Uuid,
pub content_type: ContentType,
pub source: String,
pub title: Option<String>,
pub options: PlayOptions,
pub created_at: chrono::DateTime<chrono::Utc>,
}
/// Response sent to UI to trigger player
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlayResponse {
pub player_id: Uuid,
pub content_type: ContentType,
pub component: String,
pub source_url: String,
pub title: String,
pub options: PlayOptions,
pub metadata: HashMap<String, String>,
}
/// Register the PLAY keyword
pub fn play_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
play_simple_keyword(state.clone(), user.clone(), engine);
play_with_options_keyword(state.clone(), user.clone(), engine);
stop_keyword(state.clone(), user.clone(), engine);
pause_keyword(state.clone(), user.clone(), engine);
resume_keyword(state.clone(), user.clone(), engine);
}
/// PLAY "source"
fn play_simple_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine.register_custom_syntax(&["PLAY", "$expr$"], false, move |context, inputs| {
let source = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
trace!("PLAY '{}' for session: {}", source, user_clone.id);
let state_for_task = Arc::clone(&state_clone);
let session_id = user_clone.id;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
execute_play(&state_for_task, session_id, &source, PlayOptions::default()).await
});
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
Ok(Ok(response)) => {
let json = serde_json::to_string(&response).unwrap_or_default();
Ok(Dynamic::from(json))
}
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
e.into(),
rhai::Position::NONE,
))),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"PLAY timed out".into(),
rhai::Position::NONE,
))),
}
});
}
/// PLAY "source" WITH OPTIONS "options"
fn play_with_options_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine.register_custom_syntax(
&["PLAY", "$expr$", "WITH", "OPTIONS", "$expr$"],
false,
move |context, inputs| {
let source = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
let options_str = context
.eval_expression_tree(&inputs[1])?
.to_string()
.trim_matches('"')
.to_string();
let options = PlayOptions::from_string(&options_str);
trace!(
"PLAY '{}' WITH OPTIONS '{}' for session: {}",
source,
options_str,
user_clone.id
);
let state_for_task = Arc::clone(&state_clone);
let session_id = user_clone.id;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
execute_play(&state_for_task, session_id, &source, options).await
});
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(30)) {
Ok(Ok(response)) => {
let json = serde_json::to_string(&response).unwrap_or_default();
Ok(Dynamic::from(json))
}
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
e.into(),
rhai::Position::NONE,
))),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"PLAY timed out".into(),
rhai::Position::NONE,
))),
}
},
);
}
/// STOP - Stop current playback
fn stop_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine.register_custom_syntax(&["STOP"], false, move |_context, _inputs| {
trace!("STOP playback for session: {}", user_clone.id);
let state_for_task = Arc::clone(&state_clone);
let session_id = user_clone.id;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt
.block_on(async { send_player_command(&state_for_task, session_id, "stop").await });
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(10)) {
Ok(Ok(_)) => Ok(Dynamic::from("Playback stopped")),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
e.into(),
rhai::Position::NONE,
))),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"STOP timed out".into(),
rhai::Position::NONE,
))),
}
});
}
/// PAUSE - Pause current playback
fn pause_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine.register_custom_syntax(&["PAUSE"], false, move |_context, _inputs| {
trace!("PAUSE playback for session: {}", user_clone.id);
let state_for_task = Arc::clone(&state_clone);
let session_id = user_clone.id;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
send_player_command(&state_for_task, session_id, "pause").await
});
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(10)) {
Ok(Ok(_)) => Ok(Dynamic::from("Playback paused")),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
e.into(),
rhai::Position::NONE,
))),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"PAUSE timed out".into(),
rhai::Position::NONE,
))),
}
});
}
/// RESUME - Resume paused playback
fn resume_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine.register_custom_syntax(&["RESUME"], false, move |_context, _inputs| {
trace!("RESUME playback for session: {}", user_clone.id);
let state_for_task = Arc::clone(&state_clone);
let session_id = user_clone.id;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
send_player_command(&state_for_task, session_id, "resume").await
});
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(10)) {
Ok(Ok(_)) => Ok(Dynamic::from("Playback resumed")),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
e.into(),
rhai::Position::NONE,
))),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"RESUME timed out".into(),
rhai::Position::NONE,
))),
}
});
}
// ============================================================================
// Core Functions
// ============================================================================
/// Execute the PLAY command
async fn execute_play(
state: &AppState,
session_id: Uuid,
source: &str,
options: PlayOptions,
) -> Result<PlayResponse, String> {
// Detect content type
let content_type = detect_content_type(source);
// Resolve source URL
let source_url = resolve_source_url(state, session_id, source).await?;
// Get metadata
let metadata = get_content_metadata(state, &source_url, &content_type).await?;
// Create player ID
let player_id = Uuid::new_v4();
// Get title from source or metadata
let title = metadata
.get("title")
.cloned()
.unwrap_or_else(|| extract_title_from_source(source));
// Build response
let response = PlayResponse {
player_id,
content_type: content_type.clone(),
component: content_type.player_component().to_string(),
source_url,
title,
options,
metadata,
};
// Send to client via WebSocket
send_play_to_client(state, session_id, &response).await?;
info!(
"Playing {:?} content: {} for session {}",
response.content_type, source, session_id
);
Ok(response)
}
/// Detect content type from source
fn detect_content_type(source: &str) -> ContentType {
// Check if it's a URL
if source.starts_with("http://") || source.starts_with("https://") {
// Check for known video platforms
if source.contains("youtube.com")
|| source.contains("youtu.be")
|| source.contains("vimeo.com")
{
return ContentType::Video;
}
// Check for known image hosts
if source.contains("imgur.com")
|| source.contains("unsplash.com")
|| source.contains("pexels.com")
{
return ContentType::Image;
}
// Try to detect from URL path extension
if let Some(path) = source.split('?').next() {
if let Some(ext) = Path::new(path).extension() {
return ContentType::from_extension(&ext.to_string_lossy());
}
}
// Default to iframe for unknown URLs
return ContentType::Iframe;
}
// Local file - detect from extension
if let Some(ext) = Path::new(source).extension() {
return ContentType::from_extension(&ext.to_string_lossy());
}
ContentType::Unknown
}
/// Resolve source to a URL
async fn resolve_source_url(
state: &AppState,
session_id: Uuid,
source: &str,
) -> Result<String, String> {
// If already a URL, return as-is
if source.starts_with("http://") || source.starts_with("https://") {
return Ok(source.to_string());
}
// Check if it's a drive path
if source.starts_with("/") || source.contains(".gbdrive") {
// Resolve from drive
let file_url = format!(
"/api/drive/file/{}?session={}",
urlencoding::encode(source),
session_id
);
return Ok(file_url);
}
// Check if it's a relative path in current bot's folder
let file_url = format!(
"/api/drive/file/{}?session={}",
urlencoding::encode(source),
session_id
);
Ok(file_url)
}
/// Get content metadata
async fn get_content_metadata(
_state: &AppState,
source_url: &str,
content_type: &ContentType,
) -> Result<HashMap<String, String>, String> {
let mut metadata = HashMap::new();
metadata.insert("source".to_string(), source_url.to_string());
metadata.insert("type".to_string(), format!("{:?}", content_type));
// Add type-specific metadata
match content_type {
ContentType::Video => {
metadata.insert("player".to_string(), "html5".to_string());
}
ContentType::Audio => {
metadata.insert("player".to_string(), "html5".to_string());
}
ContentType::Image => {
metadata.insert("viewer".to_string(), "lightbox".to_string());
}
ContentType::Pdf => {
metadata.insert("viewer".to_string(), "pdfjs".to_string());
}
ContentType::Code => {
metadata.insert("highlighter".to_string(), "prism".to_string());
}
ContentType::Presentation => {
metadata.insert("viewer".to_string(), "revealjs".to_string());
}
ContentType::Spreadsheet => {
metadata.insert("viewer".to_string(), "handsontable".to_string());
}
_ => {}
}
Ok(metadata)
}
/// Extract title from source path/URL
fn extract_title_from_source(source: &str) -> String {
// Extract filename from path or URL
let path = source.split('?').next().unwrap_or(source);
Path::new(path)
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "Untitled".to_string())
}
/// Send play command to client via WebSocket
async fn send_play_to_client(
state: &AppState,
session_id: Uuid,
response: &PlayResponse,
) -> Result<(), String> {
let message = serde_json::json!({
"type": "play",
"data": response
});
let message_str =
serde_json::to_string(&message).map_err(|e| format!("Failed to serialize: {}", e))?;
// Send via web adapter
let web_adapter = Arc::clone(&state.web_adapter);
if let Some(sender) = web_adapter
.sessions
.lock()
.await
.get(&session_id.to_string())
{
sender
.send(axum::extract::ws::Message::Text(message_str))
.await
.map_err(|e| format!("Failed to send to client: {}", e))?;
} else {
// Store for later delivery
trace!(
"No WebSocket connection for session {}, message queued",
session_id
);
}
Ok(())
}
/// Send player command (stop/pause/resume) to client
async fn send_player_command(
state: &AppState,
session_id: Uuid,
command: &str,
) -> Result<(), String> {
let message = serde_json::json!({
"type": "player_command",
"command": command
});
let message_str =
serde_json::to_string(&message).map_err(|e| format!("Failed to serialize: {}", e))?;
let web_adapter = Arc::clone(&state.web_adapter);
if let Some(sender) = web_adapter
.sessions
.lock()
.await
.get(&session_id.to_string())
{
sender
.send(axum::extract::ws::Message::Text(message_str))
.await
.map_err(|e| format!("Failed to send command: {}", e))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_content_type_from_extension() {
assert_eq!(ContentType::from_extension("mp4"), ContentType::Video);
assert_eq!(ContentType::from_extension("MP3"), ContentType::Audio);
assert_eq!(ContentType::from_extension("png"), ContentType::Image);
assert_eq!(ContentType::from_extension("pdf"), ContentType::Pdf);
assert_eq!(ContentType::from_extension("rs"), ContentType::Code);
assert_eq!(
ContentType::from_extension("pptx"),
ContentType::Presentation
);
assert_eq!(
ContentType::from_extension("xlsx"),
ContentType::Spreadsheet
);
assert_eq!(ContentType::from_extension("md"), ContentType::Markdown);
}
#[test]
fn test_content_type_from_mime() {
assert_eq!(ContentType::from_mime("video/mp4"), ContentType::Video);
assert_eq!(ContentType::from_mime("audio/mpeg"), ContentType::Audio);
assert_eq!(ContentType::from_mime("image/png"), ContentType::Image);
assert_eq!(ContentType::from_mime("application/pdf"), ContentType::Pdf);
}
#[test]
fn test_play_options_from_string() {
let opts = PlayOptions::from_string("autoplay,loop,muted");
assert!(opts.autoplay);
assert!(opts.loop_content);
assert!(opts.muted);
assert!(!opts.fullscreen);
assert!(opts.controls);
let opts = PlayOptions::from_string("fullscreen,nocontrols,start=10,end=60");
assert!(opts.fullscreen);
assert!(!opts.controls);
assert_eq!(opts.start_time, Some(10.0));
assert_eq!(opts.end_time, Some(60.0));
let opts = PlayOptions::from_string("theme=dark,zoom=1.5,page=3");
assert_eq!(opts.theme, Some("dark".to_string()));
assert_eq!(opts.zoom, Some(1.5));
assert_eq!(opts.page, Some(3));
}
#[test]
fn test_detect_content_type() {
assert_eq!(
detect_content_type("https://youtube.com/watch?v=123"),
ContentType::Video
);
assert_eq!(
detect_content_type("https://example.com/video.mp4"),
ContentType::Video
);
assert_eq!(
detect_content_type("https://imgur.com/abc123"),
ContentType::Image
);
assert_eq!(
detect_content_type("presentation.pptx"),
ContentType::Presentation
);
assert_eq!(detect_content_type("report.pdf"), ContentType::Pdf);
assert_eq!(detect_content_type("main.rs"), ContentType::Code);
}
#[test]
fn test_extract_title_from_source() {
assert_eq!(extract_title_from_source("documents/report.pdf"), "report");
assert_eq!(
extract_title_from_source("https://example.com/video.mp4?token=abc"),
"video"
);
assert_eq!(
extract_title_from_source("presentation.pptx"),
"presentation"
);
}
#[test]
fn test_player_component() {
assert_eq!(ContentType::Video.player_component(), "video-player");
assert_eq!(ContentType::Audio.player_component(), "audio-player");
assert_eq!(ContentType::Image.player_component(), "image-viewer");
assert_eq!(ContentType::Pdf.player_component(), "pdf-viewer");
assert_eq!(ContentType::Code.player_component(), "code-viewer");
}
}

View file

@ -0,0 +1,763 @@
/*****************************************************************************\
| ® |
| |
| |
| |
| |
| |
| General Bots Copyright (c) pragmatismo.com.br. All rights reserved. |
| Licensed under the AGPL-3.0. |
| |
| According to our dual licensing model, this program can be used either |
| under the terms of the GNU Affero General Public License, version 3, |
| or under a proprietary license. |
| |
| The texts of the GNU Affero General Public License with an additional |
| permission and of our proprietary license can be found at and |
| in the LICENSE file you have received along with this program. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY, without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| "General Bots" is a registered trademark of pragmatismo.com.br. |
| The licensing of the program under the AGPLv3 does not imply a |
| trademark license. Therefore any rights, title and interest in |
| our trademarks remain entirely with us. |
| |
\*****************************************************************************/
//! TABLE keyword implementation for dynamic table definitions
//!
//! Parses and creates database tables from BASIC syntax:
//!
//! ```basic
//! TABLE Contacts ON maria
//! Id number key
//! Nome string(150)
//! Email string(255)
//! Telefone string(20)
//! END TABLE
//! ```
//!
//! Connection names (e.g., "maria") are configured in config.csv with:
//! - conn-maria-Server
//! - conn-maria-Name (database name)
//! - conn-maria-Username
//! - conn-maria-Port
//! - conn-maria-Password
//! - conn-maria-Driver
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use diesel::prelude::*;
use diesel::sql_query;
use diesel::sql_types::Text;
use log::{error, info, trace, warn};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::error::Error;
use std::sync::Arc;
use uuid::Uuid;
/// Represents a field definition in a TABLE block
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldDefinition {
pub name: String,
pub field_type: String,
pub length: Option<i32>,
pub precision: Option<i32>,
pub is_key: bool,
pub is_nullable: bool,
pub default_value: Option<String>,
pub reference_table: Option<String>,
pub field_order: i32,
}
/// Represents a complete TABLE definition
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableDefinition {
pub name: String,
pub connection_name: String,
pub fields: Vec<FieldDefinition>,
}
/// External database connection configuration
#[derive(Debug, Clone)]
pub struct ExternalConnection {
pub name: String,
pub driver: String,
pub server: String,
pub port: Option<i32>,
pub database: String,
pub username: String,
pub password: String,
}
/// Parse a TABLE...END TABLE block from BASIC source
pub fn parse_table_definition(
source: &str,
) -> Result<Vec<TableDefinition>, Box<dyn Error + Send + Sync>> {
let mut tables = Vec::new();
let lines: Vec<&str> = source.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i].trim();
// Look for TABLE keyword
if line.starts_with("TABLE ") {
let table_def = parse_single_table(&lines, &mut i)?;
tables.push(table_def);
} else {
i += 1;
}
}
Ok(tables)
}
/// Parse a single TABLE block
fn parse_single_table(
lines: &[&str],
index: &mut usize,
) -> Result<TableDefinition, Box<dyn Error + Send + Sync>> {
let header_line = lines[*index].trim();
// Parse: TABLE TableName ON connection
let parts: Vec<&str> = header_line.split_whitespace().collect();
if parts.len() < 2 {
return Err(format!(
"Invalid TABLE syntax at line {}: {}",
index + 1,
header_line
)
.into());
}
let table_name = parts[1].to_string();
// Check for ON clause
let connection_name = if parts.len() >= 4 && parts[2].eq_ignore_ascii_case("ON") {
parts[3].to_string()
} else {
"default".to_string()
};
trace!("Parsing TABLE {} ON {}", table_name, connection_name);
*index += 1;
let mut fields = Vec::new();
let mut field_order = 0;
// Parse fields until END TABLE
while *index < lines.len() {
let line = lines[*index].trim();
// Skip empty lines and comments
if line.is_empty()
|| line.starts_with("'")
|| line.starts_with("REM")
|| line.starts_with("//")
{
*index += 1;
continue;
}
// Check for END TABLE
if line.eq_ignore_ascii_case("END TABLE") {
*index += 1;
break;
}
// Parse field definition
if let Ok(field) = parse_field_definition(line, field_order) {
fields.push(field);
field_order += 1;
} else {
warn!("Could not parse field definition: {}", line);
}
*index += 1;
}
Ok(TableDefinition {
name: table_name,
connection_name,
fields,
})
}
/// Parse a single field definition line
/// Format: FieldName type[(length[,precision])] [key] [references TableName]
fn parse_field_definition(
line: &str,
order: i32,
) -> Result<FieldDefinition, Box<dyn Error + Send + Sync>> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
return Err("Empty field definition".into());
}
let field_name = parts[0].to_string();
let mut field_type = String::new();
let mut length: Option<i32> = None;
let mut precision: Option<i32> = None;
let mut is_key = false;
let mut reference_table: Option<String> = None;
if parts.len() >= 2 {
let type_part = parts[1];
// Parse type with optional length: string(150) or number(10,2)
if let Some(paren_start) = type_part.find('(') {
field_type = type_part[..paren_start].to_lowercase();
let params = &type_part[paren_start + 1..type_part.len() - 1];
let param_parts: Vec<&str> = params.split(',').collect();
if !param_parts.is_empty() {
length = param_parts[0].trim().parse().ok();
}
if param_parts.len() > 1 {
precision = param_parts[1].trim().parse().ok();
}
} else {
field_type = type_part.to_lowercase();
}
}
// Check for additional modifiers
for i in 2..parts.len() {
let part = parts[i].to_lowercase();
match part.as_str() {
"key" => is_key = true,
_ if parts
.get(i - 1)
.map(|p| p.eq_ignore_ascii_case("references"))
.unwrap_or(false) =>
{
// This is the reference table name, already handled
}
"references" => {
if i + 1 < parts.len() {
reference_table = Some(parts[i + 1].to_string());
}
}
_ => {}
}
}
Ok(FieldDefinition {
name: field_name,
field_type,
length,
precision,
is_key,
is_nullable: !is_key, // Keys are not nullable by default
default_value: None,
reference_table,
field_order: order,
})
}
/// Map BASIC types to SQL types
fn map_type_to_sql(field: &FieldDefinition, driver: &str) -> String {
let base_type = match field.field_type.as_str() {
"string" => {
let len = field.length.unwrap_or(255);
format!("VARCHAR({})", len)
}
"number" | "integer" | "int" => {
if field.precision.is_some() {
let len = field.length.unwrap_or(10);
let prec = field.precision.unwrap_or(2);
format!("DECIMAL({},{})", len, prec)
} else if field.length.is_some() {
"BIGINT".to_string()
} else {
"INTEGER".to_string()
}
}
"double" | "float" => {
if let (Some(len), Some(prec)) = (field.length, field.precision) {
format!("DECIMAL({},{})", len, prec)
} else {
"DOUBLE PRECISION".to_string()
}
}
"date" => "DATE".to_string(),
"datetime" | "timestamp" => match driver {
"mysql" | "mariadb" => "DATETIME".to_string(),
_ => "TIMESTAMP".to_string(),
},
"boolean" | "bool" => match driver {
"mysql" | "mariadb" => "TINYINT(1)".to_string(),
_ => "BOOLEAN".to_string(),
},
"text" => "TEXT".to_string(),
"guid" | "uuid" => match driver {
"mysql" | "mariadb" => "CHAR(36)".to_string(),
_ => "UUID".to_string(),
},
_ => format!("VARCHAR({})", field.length.unwrap_or(255)),
};
base_type
}
/// Generate CREATE TABLE SQL statement
pub fn generate_create_table_sql(table: &TableDefinition, driver: &str) -> String {
let mut sql = format!(
"CREATE TABLE IF NOT EXISTS {} (\n",
sanitize_identifier(&table.name)
);
let mut column_defs = Vec::new();
let mut primary_keys = Vec::new();
for field in &table.fields {
let sql_type = map_type_to_sql(field, driver);
let mut col_def = format!(" {} {}", sanitize_identifier(&field.name), sql_type);
if field.is_key {
primary_keys.push(sanitize_identifier(&field.name));
}
if !field.is_nullable {
col_def.push_str(" NOT NULL");
}
if let Some(ref default) = field.default_value {
col_def.push_str(&format!(" DEFAULT {}", default));
}
column_defs.push(col_def);
}
sql.push_str(&column_defs.join(",\n"));
// Add primary key constraint
if !primary_keys.is_empty() {
sql.push_str(&format!(",\n PRIMARY KEY ({})", primary_keys.join(", ")));
}
sql.push_str("\n)");
// Add engine for MySQL/MariaDB
if driver == "mysql" || driver == "mariadb" {
sql.push_str(" ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
}
sql.push(';');
sql
}
/// Sanitize identifier to prevent SQL injection
fn sanitize_identifier(name: &str) -> String {
name.chars()
.filter(|c| c.is_alphanumeric() || *c == '_')
.collect()
}
/// Load external connection configuration from bot config
pub fn load_connection_config(
state: &AppState,
bot_id: Uuid,
connection_name: &str,
) -> Result<ExternalConnection, Box<dyn Error + Send + Sync>> {
let config_manager = crate::core::config::ConfigManager::new(state.conn.clone());
let prefix = format!("conn-{}-", connection_name);
let server = config_manager
.get_config(&bot_id, &format!("{}Server", prefix), None)
.ok_or_else(|| format!("Missing {prefix}Server in config"))?;
let database = config_manager
.get_config(&bot_id, &format!("{}Name", prefix), None)
.ok_or_else(|| format!("Missing {prefix}Name in config"))?;
let username = config_manager
.get_config(&bot_id, &format!("{}Username", prefix), None)
.unwrap_or_default();
let password = config_manager
.get_config(&bot_id, &format!("{}Password", prefix), None)
.unwrap_or_default();
let port = config_manager
.get_config(&bot_id, &format!("{}Port", prefix), None)
.and_then(|p| p.parse().ok());
let driver = config_manager
.get_config(&bot_id, &format!("{}Driver", prefix), None)
.unwrap_or_else(|| "postgres".to_string());
Ok(ExternalConnection {
name: connection_name.to_string(),
driver,
server,
port,
database,
username,
password,
})
}
/// Build connection string for external database
pub fn build_connection_string(conn: &ExternalConnection) -> String {
let port = conn.port.unwrap_or(match conn.driver.as_str() {
"mysql" | "mariadb" => 3306,
"postgres" | "postgresql" => 5432,
"mssql" | "sqlserver" => 1433,
_ => 5432,
});
match conn.driver.as_str() {
"mysql" | "mariadb" => {
format!(
"mysql://{}:{}@{}:{}/{}",
conn.username, conn.password, conn.server, port, conn.database
)
}
"postgres" | "postgresql" => {
format!(
"postgres://{}:{}@{}:{}/{}",
conn.username, conn.password, conn.server, port, conn.database
)
}
"mssql" | "sqlserver" => {
format!(
"mssql://{}:{}@{}:{}/{}",
conn.username, conn.password, conn.server, port, conn.database
)
}
_ => {
format!(
"postgres://{}:{}@{}:{}/{}",
conn.username, conn.password, conn.server, port, conn.database
)
}
}
}
/// Store table definition in metadata tables
pub fn store_table_definition(
conn: &mut diesel::PgConnection,
bot_id: Uuid,
table: &TableDefinition,
) -> Result<Uuid, Box<dyn Error + Send + Sync>> {
// Insert table definition
let table_id: Uuid = diesel::sql_query(
"INSERT INTO dynamic_table_definitions (bot_id, table_name, connection_name)
VALUES ($1, $2, $3)
ON CONFLICT (bot_id, table_name, connection_name)
DO UPDATE SET updated_at = NOW()
RETURNING id",
)
.bind::<diesel::sql_types::Uuid, _>(bot_id)
.bind::<Text, _>(&table.name)
.bind::<Text, _>(&table.connection_name)
.get_result::<IdResult>(conn)?
.id;
// Delete existing fields for this table
diesel::sql_query("DELETE FROM dynamic_table_fields WHERE table_definition_id = $1")
.bind::<diesel::sql_types::Uuid, _>(table_id)
.execute(conn)?;
// Insert field definitions
for field in &table.fields {
diesel::sql_query(
"INSERT INTO dynamic_table_fields
(table_definition_id, field_name, field_type, field_length, field_precision,
is_key, is_nullable, default_value, reference_table, field_order)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
)
.bind::<diesel::sql_types::Uuid, _>(table_id)
.bind::<Text, _>(&field.name)
.bind::<Text, _>(&field.field_type)
.bind::<diesel::sql_types::Nullable<diesel::sql_types::Integer>, _>(field.length)
.bind::<diesel::sql_types::Nullable<diesel::sql_types::Integer>, _>(field.precision)
.bind::<diesel::sql_types::Bool, _>(field.is_key)
.bind::<diesel::sql_types::Bool, _>(field.is_nullable)
.bind::<diesel::sql_types::Nullable<Text>, _>(&field.default_value)
.bind::<diesel::sql_types::Nullable<Text>, _>(&field.reference_table)
.bind::<diesel::sql_types::Integer, _>(field.field_order)
.execute(conn)?;
}
Ok(table_id)
}
#[derive(QueryableByName)]
struct IdResult {
#[diesel(sql_type = diesel::sql_types::Uuid)]
id: Uuid,
}
/// Execute CREATE TABLE on external connection
pub async fn create_table_on_external_db(
connection_string: &str,
create_sql: &str,
driver: &str,
) -> Result<(), Box<dyn Error + Send + Sync>> {
match driver {
"mysql" | "mariadb" => create_table_mysql(connection_string, create_sql).await,
"postgres" | "postgresql" => create_table_postgres(connection_string, create_sql).await,
_ => {
warn!("Unsupported driver: {}, attempting postgres", driver);
create_table_postgres(connection_string, create_sql).await
}
}
}
async fn create_table_mysql(
connection_string: &str,
sql: &str,
) -> Result<(), Box<dyn Error + Send + Sync>> {
use sqlx::mysql::MySqlPoolOptions;
use sqlx::Executor;
let pool = MySqlPoolOptions::new()
.max_connections(1)
.connect(connection_string)
.await?;
pool.execute(sql).await?;
info!("MySQL table created successfully");
Ok(())
}
async fn create_table_postgres(
connection_string: &str,
sql: &str,
) -> Result<(), Box<dyn Error + Send + Sync>> {
use sqlx::postgres::PgPoolOptions;
use sqlx::Executor;
let pool = PgPoolOptions::new()
.max_connections(1)
.connect(connection_string)
.await?;
pool.execute(sql).await?;
info!("PostgreSQL table created successfully");
Ok(())
}
/// Process TABLE definitions during .bas file compilation
pub fn process_table_definitions(
state: Arc<AppState>,
bot_id: Uuid,
source: &str,
) -> Result<Vec<TableDefinition>, Box<dyn Error + Send + Sync>> {
let tables = parse_table_definition(source)?;
if tables.is_empty() {
return Ok(tables);
}
let mut conn = state.conn.get()?;
for table in &tables {
info!(
"Processing TABLE {} ON {}",
table.name, table.connection_name
);
// Store table definition in metadata
store_table_definition(&mut conn, bot_id, table)?;
// Load connection config and create table on external DB
if table.connection_name != "default" {
match load_connection_config(&state, bot_id, &table.connection_name) {
Ok(ext_conn) => {
let create_sql = generate_create_table_sql(table, &ext_conn.driver);
let conn_string = build_connection_string(&ext_conn);
info!(
"Creating table {} on {} ({})",
table.name, table.connection_name, ext_conn.driver
);
trace!("SQL: {}", create_sql);
// Execute async in blocking context
let driver = ext_conn.driver.clone();
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
if let Err(e) =
create_table_on_external_db(&conn_string, &create_sql, &driver)
.await
{
error!("Failed to create table on external DB: {}", e);
}
})
});
}
Err(e) => {
error!(
"Failed to load connection config for {}: {}",
table.connection_name, e
);
}
}
} else {
// Create on default (internal) database
let create_sql = generate_create_table_sql(table, "postgres");
info!("Creating table {} on default connection", table.name);
trace!("SQL: {}", create_sql);
sql_query(&create_sql).execute(&mut conn)?;
}
}
Ok(tables)
}
/// Register TABLE keyword (no-op at runtime, processed at compile time)
pub fn register_table_keywords(
_state: Arc<AppState>,
_user: UserSession,
_engine: &mut rhai::Engine,
) {
// TABLE...END TABLE is processed at compile time, not runtime
// This function exists for consistency with other keyword modules
trace!("TABLE keyword registered (compile-time only)");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_table_definition() {
let source = r#"
TABLE Contacts ON maria
Id number key
Nome string(150)
Email string(255)
Telefone string(20)
END TABLE
"#;
let tables = parse_table_definition(source).unwrap();
assert_eq!(tables.len(), 1);
let table = &tables[0];
assert_eq!(table.name, "Contacts");
assert_eq!(table.connection_name, "maria");
assert_eq!(table.fields.len(), 4);
assert_eq!(table.fields[0].name, "Id");
assert_eq!(table.fields[0].field_type, "number");
assert!(table.fields[0].is_key);
assert_eq!(table.fields[1].name, "Nome");
assert_eq!(table.fields[1].field_type, "string");
assert_eq!(table.fields[1].length, Some(150));
}
#[test]
fn test_parse_field_with_precision() {
let field = parse_field_definition("Preco double(10,2)", 0).unwrap();
assert_eq!(field.name, "Preco");
assert_eq!(field.field_type, "double");
assert_eq!(field.length, Some(10));
assert_eq!(field.precision, Some(2));
}
#[test]
fn test_generate_create_table_sql() {
let table = TableDefinition {
name: "TestTable".to_string(),
connection_name: "default".to_string(),
fields: vec![
FieldDefinition {
name: "id".to_string(),
field_type: "number".to_string(),
length: None,
precision: None,
is_key: true,
is_nullable: false,
default_value: None,
reference_table: None,
field_order: 0,
},
FieldDefinition {
name: "name".to_string(),
field_type: "string".to_string(),
length: Some(100),
precision: None,
is_key: false,
is_nullable: true,
default_value: None,
reference_table: None,
field_order: 1,
},
],
};
let sql = generate_create_table_sql(&table, "postgres");
assert!(sql.contains("CREATE TABLE IF NOT EXISTS TestTable"));
assert!(sql.contains("id INTEGER NOT NULL"));
assert!(sql.contains("name VARCHAR(100)"));
assert!(sql.contains("PRIMARY KEY (id)"));
}
#[test]
fn test_map_types() {
let field = FieldDefinition {
name: "test".to_string(),
field_type: "string".to_string(),
length: Some(50),
precision: None,
is_key: false,
is_nullable: true,
default_value: None,
reference_table: None,
field_order: 0,
};
assert_eq!(map_type_to_sql(&field, "postgres"), "VARCHAR(50)");
let date_field = FieldDefinition {
name: "created".to_string(),
field_type: "datetime".to_string(),
length: None,
precision: None,
is_key: false,
is_nullable: true,
default_value: None,
reference_table: None,
field_order: 0,
};
assert_eq!(map_type_to_sql(&date_field, "mysql"), "DATETIME");
assert_eq!(map_type_to_sql(&date_field, "postgres"), "TIMESTAMP");
}
#[test]
fn test_sanitize_identifier() {
assert_eq!(sanitize_identifier("valid_name"), "valid_name");
assert_eq!(sanitize_identifier("DROP TABLE; --"), "DROPTABLE");
assert_eq!(sanitize_identifier("name123"), "name123");
}
#[test]
fn test_build_connection_string() {
let conn = ExternalConnection {
name: "test".to_string(),
driver: "mysql".to_string(),
server: "localhost".to_string(),
port: Some(3306),
database: "testdb".to_string(),
username: "user".to_string(),
password: "pass".to_string(),
};
let conn_str = build_connection_string(&conn);
assert_eq!(conn_str, "mysql://user:pass@localhost:3306/testdb");
}
}

View file

@ -4,11 +4,22 @@ 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 diesel::prelude::*;
use log::info;
use rhai::{Dynamic, Engine, EvalAltResult};
use rhai::{Dynamic, Engine, EvalAltResult, Scope};
use std::collections::HashMap;
use std::sync::Arc;
pub mod compiler;
pub mod keywords;
/// Helper struct for loading param config from database
#[derive(QueryableByName)]
struct ParamConfigRow {
#[diesel(sql_type = diesel::sql_types::Text)]
config_key: String,
#[diesel(sql_type = diesel::sql_types::Text)]
config_value: String,
}
use self::keywords::add_member::add_member_keyword;
use self::keywords::add_suggestion::add_suggestion_keyword;
use self::keywords::book::book_keyword;
@ -53,11 +64,13 @@ use self::keywords::wait::wait_keyword;
#[derive(Debug)]
pub struct ScriptService {
pub engine: Engine,
pub scope: Scope<'static>,
}
impl ScriptService {
#[must_use]
pub fn new(state: Arc<AppState>, user: UserSession) -> Self {
let mut engine = Engine::new();
let scope = Scope::new();
engine.set_allow_anonymous_fn(true);
engine.set_allow_looping(true);
@ -163,12 +176,61 @@ impl ScriptService {
// Error Handling: THROW, ERROR, IS_ERROR, ASSERT
register_core_functions(state.clone(), user, &mut engine);
ScriptService { engine }
ScriptService { engine, scope }
}
/// Inject param-* configuration variables from config.csv into the script scope
/// Variables are made available without the "param-" prefix and normalized to lowercase
pub fn inject_config_variables(&mut self, config_vars: HashMap<String, String>) {
for (key, value) in config_vars {
// Remove "param-" prefix if present and normalize to lowercase
let var_name = if key.starts_with("param-") {
key.strip_prefix("param-").unwrap_or(&key).to_lowercase()
} else {
key.to_lowercase()
};
// Try to parse as number, otherwise use as string
if let Ok(int_val) = value.parse::<i64>() {
self.scope.push(&var_name, int_val);
} else if let Ok(float_val) = value.parse::<f64>() {
self.scope.push(&var_name, float_val);
} else if value.eq_ignore_ascii_case("true") {
self.scope.push(&var_name, true);
} else if value.eq_ignore_ascii_case("false") {
self.scope.push(&var_name, false);
} else {
self.scope.push(&var_name, value);
}
}
}
/// Load and inject param-* variables from bot configuration
pub fn load_bot_config_params(&mut self, state: &AppState, bot_id: uuid::Uuid) {
if let Ok(mut conn) = state.conn.get() {
// Query all config entries for this bot that start with "param-"
let result: Result<Vec<(String, String)>, _> = diesel::sql_query(
"SELECT config_key, config_value FROM bot_configuration WHERE bot_id = $1 AND config_key LIKE 'param-%'"
)
.bind::<diesel::sql_types::Uuid, _>(bot_id)
.load::<ParamConfigRow>(&mut conn);
if let Ok(params) = result {
let config_vars: HashMap<String, String> = params
.into_iter()
.map(|row| (row.config_key, row.config_value))
.collect();
self.inject_config_variables(config_vars);
}
}
}
fn preprocess_basic_script(&self, script: &str) -> String {
// First, preprocess SWITCH/CASE blocks
let script = preprocess_switch(script);
// Make variables case-insensitive by normalizing to lowercase
let script = Self::normalize_variables_to_lowercase(&script);
let mut result = String::new();
let mut for_stack: Vec<usize> = Vec::new();
let mut current_indent = 0;
@ -420,8 +482,330 @@ impl ScriptService {
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)
pub fn run(&mut self, ast: &rhai::AST) -> Result<Dynamic, Box<EvalAltResult>> {
self.engine.eval_ast_with_scope(&mut self.scope, ast)
}
/// Normalize variable names to lowercase for case-insensitive BASIC semantics
/// This transforms variable assignments and references to use lowercase names
/// while preserving string literals, keywords, and comments
fn normalize_variables_to_lowercase(script: &str) -> String {
use regex::Regex;
let mut result = String::new();
// Keywords that should remain uppercase (BASIC commands)
let keywords = [
"SET",
"CREATE",
"PRINT",
"FOR",
"FIND",
"GET",
"EXIT",
"IF",
"THEN",
"ELSE",
"END",
"WHILE",
"WEND",
"DO",
"LOOP",
"HEAR",
"TALK",
"NEXT",
"FUNCTION",
"SUB",
"CALL",
"RETURN",
"DIM",
"AS",
"NEW",
"ARRAY",
"OBJECT",
"LET",
"REM",
"AND",
"OR",
"NOT",
"TRUE",
"FALSE",
"NULL",
"SWITCH",
"CASE",
"DEFAULT",
"USE",
"KB",
"TOOL",
"CLEAR",
"ADD",
"SUGGESTION",
"SUGGESTIONS",
"TOOLS",
"CONTEXT",
"USER",
"BOT",
"MEMORY",
"IMAGE",
"VIDEO",
"AUDIO",
"SEE",
"SEND",
"FILE",
"POST",
"PUT",
"PATCH",
"DELETE",
"SAVE",
"INSERT",
"UPDATE",
"MERGE",
"FILL",
"MAP",
"FILTER",
"AGGREGATE",
"JOIN",
"PIVOT",
"GROUP",
"BY",
"READ",
"WRITE",
"COPY",
"MOVE",
"LIST",
"COMPRESS",
"EXTRACT",
"UPLOAD",
"DOWNLOAD",
"GENERATE",
"PDF",
"WEBHOOK",
"TEMPLATE",
"FORM",
"SUBMIT",
"SCORE",
"LEAD",
"QUALIFY",
"AI",
"ABS",
"ROUND",
"INT",
"FIX",
"FLOOR",
"CEIL",
"MAX",
"MIN",
"MOD",
"RANDOM",
"RND",
"SGN",
"SQR",
"SQRT",
"LOG",
"EXP",
"POW",
"SIN",
"COS",
"TAN",
"SUM",
"AVG",
"NOW",
"TODAY",
"DATE",
"TIME",
"YEAR",
"MONTH",
"DAY",
"HOUR",
"MINUTE",
"SECOND",
"WEEKDAY",
"DATEADD",
"DATEDIFF",
"FORMAT",
"ISDATE",
"VAL",
"STR",
"CINT",
"CDBL",
"CSTR",
"ISNULL",
"ISEMPTY",
"TYPEOF",
"ISARRAY",
"ISOBJECT",
"ISSTRING",
"ISNUMBER",
"NVL",
"IIF",
"UBOUND",
"LBOUND",
"COUNT",
"SORT",
"UNIQUE",
"CONTAINS",
"INDEX",
"OF",
"PUSH",
"POP",
"SHIFT",
"REVERSE",
"SLICE",
"SPLIT",
"CONCAT",
"FLATTEN",
"RANGE",
"THROW",
"ERROR",
"IS",
"ASSERT",
"WARN",
"INFO",
"EACH",
"WITH",
"TO",
"STEP",
"BEGIN",
"SYSTEM",
"PROMPT",
"SCHEDULE",
"REFRESH",
"ALLOW",
"ROLE",
"ANSWER",
"MODE",
"SYNCHRONIZE",
"TABLE",
"ON",
"EMAIL",
"REPORT",
"RESET",
"WAIT",
"FIRST",
"LAST",
"LLM",
"INSTR",
"NUMERIC",
"LEN",
"LEFT",
"RIGHT",
"MID",
"LOWER",
"UPPER",
"TRIM",
"LTRIM",
"RTRIM",
"REPLACE",
"LIKE",
"DELEGATE",
"PRIORITY",
"BOTS",
"REMOVE",
"MEMBER",
"BOOK",
"REMEMBER",
"TASK",
"SITE",
"DRAFT",
"INSTAGRAM",
"FACEBOOK",
"LINKEDIN",
"TWITTER",
"METRICS",
"HEADER",
"HEADERS",
"GRAPHQL",
"SOAP",
"HTTP",
"DESCRIPTION",
"PARAM",
"REQUIRED",
];
// Regex to match identifiers (variable names)
let identifier_re = Regex::new(r"([a-zA-Z_][a-zA-Z0-9_]*)").unwrap();
for line in script.lines() {
let trimmed = line.trim();
// Skip comments entirely
if trimmed.starts_with("REM") || trimmed.starts_with("'") || trimmed.starts_with("//") {
result.push_str(line);
result.push('\n');
continue;
}
// Process line character by character to handle strings properly
let mut processed_line = String::new();
let mut chars = line.chars().peekable();
let mut in_string = false;
let mut string_char = '"';
let mut current_word = String::new();
while let Some(c) = chars.next() {
if in_string {
processed_line.push(c);
if c == string_char {
in_string = false;
} else if c == '\\' {
// Handle escape sequences
if let Some(&next) = chars.peek() {
processed_line.push(next);
chars.next();
}
}
} else if c == '"' || c == '\'' {
// Flush current word before string
if !current_word.is_empty() {
processed_line.push_str(&Self::normalize_word(&current_word, &keywords));
current_word.clear();
}
in_string = true;
string_char = c;
processed_line.push(c);
} else if c.is_alphanumeric() || c == '_' {
current_word.push(c);
} else {
// Flush current word
if !current_word.is_empty() {
processed_line.push_str(&Self::normalize_word(&current_word, &keywords));
current_word.clear();
}
processed_line.push(c);
}
}
// Flush any remaining word
if !current_word.is_empty() {
processed_line.push_str(&Self::normalize_word(&current_word, &keywords));
}
result.push_str(&processed_line);
result.push('\n');
}
result
}
/// Normalize a single word - convert to lowercase if it's a variable (not a keyword)
fn normalize_word(word: &str, keywords: &[&str]) -> String {
let upper = word.to_uppercase();
// Check if it's a keyword (case-insensitive)
if keywords.contains(&upper.as_str()) {
// Return the keyword in uppercase for consistency
upper
} else if word
.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
{
// It's a number, keep as-is
word.to_string()
} else {
// It's a variable - normalize to lowercase
word.to_lowercase()
}
}
}

View file

@ -117,7 +117,11 @@ impl AutomationService {
sm.get_or_create_user_session(admin_user, automation.bot_id, "Automation")?
.ok_or("Failed to create session")?
};
let script_service = ScriptService::new(Arc::clone(&self.state), session);
let mut script_service = ScriptService::new(Arc::clone(&self.state), session);
// Inject param-* config variables from bot configuration
script_service.load_bot_config_params(&self.state, automation.bot_id);
match script_service.compile(&script_content) {
Ok(ast) => {
if let Err(e) = script_service.run(&ast) {

View file

@ -51,6 +51,7 @@ impl PackageManager {
self.register_desktop();
self.register_devtools();
self.register_vector_db();
self.register_timeseries_db();
self.register_host();
}
@ -610,6 +611,48 @@ impl PackageManager {
);
}
fn register_timeseries_db(&mut self) {
self.components.insert(
"timeseries_db".to_string(),
ComponentConfig {
name: "timeseries_db".to_string(),
ports: vec![8086, 8083],
dependencies: vec![],
linux_packages: vec![],
macos_packages: vec![],
windows_packages: vec![],
download_url: Some(
"https://download.influxdata.com/influxdb/releases/influxdb2-2.7.5-linux-amd64.tar.gz".to_string(),
),
binary_name: Some("influxd".to_string()),
pre_install_cmds_linux: vec![
"mkdir -p {{DATA_PATH}}/influxdb".to_string(),
"mkdir -p {{CONF_PATH}}/influxdb".to_string(),
],
post_install_cmds_linux: vec![
"{{BIN_PATH}}/influx setup --org pragmatismo --bucket metrics --username admin --password {{GENERATED_PASSWORD}} --force".to_string(),
],
pre_install_cmds_macos: vec![
"mkdir -p {{DATA_PATH}}/influxdb".to_string(),
],
post_install_cmds_macos: vec![],
pre_install_cmds_windows: vec![],
post_install_cmds_windows: vec![],
env_vars: {
let mut env = HashMap::new();
env.insert("INFLUXD_ENGINE_PATH".to_string(), "{{DATA_PATH}}/influxdb/engine".to_string());
env.insert("INFLUXD_BOLT_PATH".to_string(), "{{DATA_PATH}}/influxdb/influxd.bolt".to_string());
env.insert("INFLUXD_HTTP_BIND_ADDRESS".to_string(), ":8086".to_string());
env.insert("INFLUXD_REPORTING_DISABLED".to_string(), "true".to_string());
env
},
data_download_list: Vec::new(),
exec_cmd: "{{BIN_PATH}}/influxd --bolt-path={{DATA_PATH}}/influxdb/influxd.bolt --engine-path={{DATA_PATH}}/influxdb/engine --http-bind-address=:8086".to_string(),
check_cmd: "curl -f http://localhost:8086/health >/dev/null 2>&1".to_string(),
},
);
}
fn register_host(&mut self) {
self.components.insert(
"host".to_string(),

View file

@ -153,8 +153,12 @@ pub async fn auth_handler(
let state_clone = Arc::clone(&state);
let session_clone = session.clone();
let bot_id = session.bot_id;
match tokio::task::spawn_blocking(move || {
let script_service = crate::basic::ScriptService::new(state_clone, session_clone);
let mut script_service =
crate::basic::ScriptService::new(state_clone.clone(), session_clone);
// Inject param-* config variables from bot configuration
script_service.load_bot_config_params(&state_clone, bot_id);
match script_service.compile(&auth_script) {
Ok(ast) => match script_service.run(&ast) {
Ok(_) => Ok(()),

670
src/timeseries/mod.rs Normal file
View file

@ -0,0 +1,670 @@
//! Time-Series Database Module for General Bots
//!
//! This module provides integration with InfluxDB 3 for storing and querying
//! time-series metrics, analytics, and operational data.
//!
//! ## Features
//! - High-performance metrics ingestion (2.5M+ points/sec)
//! - Async/non-blocking writes with batching
//! - Built-in query interface for analytics dashboard
//! - LLM-powered natural language queries
//!
//! ## Usage
//! ```rust,ignore
//! use botserver::timeseries::{TimeSeriesClient, MetricPoint};
//!
//! let client = TimeSeriesClient::new("http://localhost:8086", "my-token", "my-org", "metrics").await?;
//! client.write_point(MetricPoint::new("messages").field("count", 1).tag("bot", "default")).await?;
//! ```
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio::sync::RwLock;
/// Configuration for the time-series database connection
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeSeriesConfig {
/// InfluxDB server URL (e.g., "http://localhost:8086")
pub url: String,
/// Authentication token
pub token: String,
/// Organization name
pub org: String,
/// Default bucket for metrics
pub bucket: String,
/// Batch size for writes (default: 1000)
pub batch_size: usize,
/// Flush interval in milliseconds (default: 1000)
pub flush_interval_ms: u64,
/// Enable TLS verification
pub verify_tls: bool,
}
impl Default for TimeSeriesConfig {
fn default() -> Self {
Self {
url: "http://localhost:8086".to_string(),
token: String::new(),
org: "pragmatismo".to_string(),
bucket: "metrics".to_string(),
batch_size: 1000,
flush_interval_ms: 1000,
verify_tls: true,
}
}
}
/// Represents a single metric data point
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricPoint {
/// Measurement name (e.g., "messages", "response_time", "llm_tokens")
pub measurement: String,
/// Tags for indexing and filtering
pub tags: HashMap<String, String>,
/// Field values (the actual metrics)
pub fields: HashMap<String, FieldValue>,
/// Timestamp (defaults to now if not specified)
pub timestamp: Option<DateTime<Utc>>,
}
/// Field value types supported by InfluxDB
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FieldValue {
Float(f64),
Integer(i64),
UnsignedInteger(u64),
String(String),
Boolean(bool),
}
impl MetricPoint {
/// Create a new metric point with the given measurement name
pub fn new(measurement: impl Into<String>) -> Self {
Self {
measurement: measurement.into(),
tags: HashMap::new(),
fields: HashMap::new(),
timestamp: None,
}
}
/// Add a tag to the metric point
pub fn tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.tags.insert(key.into(), value.into());
self
}
/// Add a float field
pub fn field_f64(mut self, key: impl Into<String>, value: f64) -> Self {
self.fields.insert(key.into(), FieldValue::Float(value));
self
}
/// Add an integer field
pub fn field_i64(mut self, key: impl Into<String>, value: i64) -> Self {
self.fields.insert(key.into(), FieldValue::Integer(value));
self
}
/// Add an unsigned integer field
pub fn field_u64(mut self, key: impl Into<String>, value: u64) -> Self {
self.fields
.insert(key.into(), FieldValue::UnsignedInteger(value));
self
}
/// Add a string field
pub fn field_str(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.fields
.insert(key.into(), FieldValue::String(value.into()));
self
}
/// Add a boolean field
pub fn field_bool(mut self, key: impl Into<String>, value: bool) -> Self {
self.fields.insert(key.into(), FieldValue::Boolean(value));
self
}
/// Set the timestamp
pub fn at(mut self, timestamp: DateTime<Utc>) -> Self {
self.timestamp = Some(timestamp);
self
}
/// Convert to InfluxDB line protocol format
pub fn to_line_protocol(&self) -> String {
let mut line = self.measurement.clone();
// Add tags (sorted for consistency)
let mut sorted_tags: Vec<_> = self.tags.iter().collect();
sorted_tags.sort_by_key(|(k, _)| *k);
for (key, value) in sorted_tags {
line.push(',');
line.push_str(&escape_tag_key(key));
line.push('=');
line.push_str(&escape_tag_value(value));
}
// Add fields
line.push(' ');
let mut sorted_fields: Vec<_> = self.fields.iter().collect();
sorted_fields.sort_by_key(|(k, _)| *k);
let fields_str: Vec<String> = sorted_fields
.iter()
.map(|(key, value)| {
let escaped_key = escape_field_key(key);
match value {
FieldValue::Float(v) => format!("{}={}", escaped_key, v),
FieldValue::Integer(v) => format!("{}={}i", escaped_key, v),
FieldValue::UnsignedInteger(v) => format!("{}={}u", escaped_key, v),
FieldValue::String(v) => {
format!("{}=\"{}\"", escaped_key, escape_string_value(v))
}
FieldValue::Boolean(v) => format!("{}={}", escaped_key, v),
}
})
.collect();
line.push_str(&fields_str.join(","));
// Add timestamp
if let Some(ts) = self.timestamp {
line.push(' ');
line.push_str(&ts.timestamp_nanos_opt().unwrap_or(0).to_string());
}
line
}
}
/// Escape special characters in tag keys
fn escape_tag_key(s: &str) -> String {
s.replace(',', "\\,")
.replace('=', "\\=")
.replace(' ', "\\ ")
}
/// Escape special characters in tag values
fn escape_tag_value(s: &str) -> String {
s.replace(',', "\\,")
.replace('=', "\\=")
.replace(' ', "\\ ")
}
/// Escape special characters in field keys
fn escape_field_key(s: &str) -> String {
s.replace(',', "\\,")
.replace('=', "\\=")
.replace(' ', "\\ ")
}
/// Escape special characters in string field values
fn escape_string_value(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
/// Query result from time-series database
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryResult {
pub columns: Vec<String>,
pub rows: Vec<Vec<serde_json::Value>>,
}
/// Time-series client for InfluxDB
pub struct TimeSeriesClient {
config: TimeSeriesConfig,
http_client: reqwest::Client,
write_buffer: Arc<RwLock<Vec<MetricPoint>>>,
write_sender: mpsc::Sender<MetricPoint>,
}
impl TimeSeriesClient {
/// Create a new time-series client
pub async fn new(config: TimeSeriesConfig) -> Result<Self, TimeSeriesError> {
let http_client = reqwest::Client::builder()
.danger_accept_invalid_certs(!config.verify_tls)
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| TimeSeriesError::ConnectionError(e.to_string()))?;
let write_buffer = Arc::new(RwLock::new(Vec::with_capacity(config.batch_size)));
let (write_sender, write_receiver) = mpsc::channel::<MetricPoint>(10000);
let client = Self {
config: config.clone(),
http_client: http_client.clone(),
write_buffer: write_buffer.clone(),
write_sender,
};
// Spawn background writer task
let buffer_clone = write_buffer.clone();
let config_clone = config.clone();
tokio::spawn(async move {
Self::background_writer(
write_receiver,
buffer_clone,
http_client,
config_clone,
)
.await;
});
Ok(client)
}
/// Background task that batches and writes metrics
async fn background_writer(
mut receiver: mpsc::Receiver<MetricPoint>,
buffer: Arc<RwLock<Vec<MetricPoint>>>,
http_client: reqwest::Client,
config: TimeSeriesConfig,
) {
let mut interval =
tokio::time::interval(std::time::Duration::from_millis(config.flush_interval_ms));
loop {
tokio::select! {
Some(point) = receiver.recv() => {
let mut buf = buffer.write().await;
buf.push(point);
if buf.len() >= config.batch_size {
let points: Vec<MetricPoint> = buf.drain(..).collect();
drop(buf);
if let Err(e) = Self::flush_points(&http_client, &config, &points).await {
log::error!("Failed to flush metrics: {}", e);
}
}
}
_ = interval.tick() => {
let mut buf = buffer.write().await;
if !buf.is_empty() {
let points: Vec<MetricPoint> = buf.drain(..).collect();
drop(buf);
if let Err(e) = Self::flush_points(&http_client, &config, &points).await {
log::error!("Failed to flush metrics: {}", e);
}
}
}
}
}
}
/// Flush points to InfluxDB
async fn flush_points(
http_client: &reqwest::Client,
config: &TimeSeriesConfig,
points: &[MetricPoint],
) -> Result<(), TimeSeriesError> {
if points.is_empty() {
return Ok(());
}
let line_protocol: String = points
.iter()
.map(|p| p.to_line_protocol())
.collect::<Vec<_>>()
.join("\n");
let url = format!(
"{}/api/v2/write?org={}&bucket={}&precision=ns",
config.url, config.org, config.bucket
);
let response = http_client
.post(&url)
.header("Authorization", format!("Token {}", config.token))
.header("Content-Type", "text/plain; charset=utf-8")
.body(line_protocol)
.send()
.await
.map_err(|e| TimeSeriesError::WriteError(e.to_string()))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(TimeSeriesError::WriteError(format!(
"HTTP {}: {}",
status, body
)));
}
log::debug!("Flushed {} metric points to InfluxDB", points.len());
Ok(())
}
/// Write a single metric point (non-blocking, batched)
pub async fn write_point(&self, point: MetricPoint) -> Result<(), TimeSeriesError> {
self.write_sender
.send(point)
.await
.map_err(|e| TimeSeriesError::WriteError(e.to_string()))
}
/// Write multiple metric points (non-blocking, batched)
pub async fn write_points(&self, points: Vec<MetricPoint>) -> Result<(), TimeSeriesError> {
for point in points {
self.write_point(point).await?;
}
Ok(())
}
/// Execute a Flux query
pub async fn query(&self, flux_query: &str) -> Result<QueryResult, TimeSeriesError> {
let url = format!("{}/api/v2/query?org={}", self.config.url, self.config.org);
let response = self
.http_client
.post(&url)
.header("Authorization", format!("Token {}", self.config.token))
.header("Accept", "application/csv")
.header("Content-Type", "application/vnd.flux")
.body(flux_query.to_string())
.send()
.await
.map_err(|e| TimeSeriesError::QueryError(e.to_string()))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(TimeSeriesError::QueryError(format!(
"HTTP {}: {}",
status, body
)));
}
let csv_data = response
.text()
.await
.map_err(|e| TimeSeriesError::QueryError(e.to_string()))?;
Self::parse_csv_result(&csv_data)
}
/// Parse CSV result from InfluxDB
fn parse_csv_result(csv_data: &str) -> Result<QueryResult, TimeSeriesError> {
let mut result = QueryResult {
columns: Vec::new(),
rows: Vec::new(),
};
let mut lines = csv_data.lines().peekable();
// Skip annotation rows and find header
while let Some(line) = lines.peek() {
if line.starts_with('#') || line.is_empty() {
lines.next();
} else {
break;
}
}
// Parse header
if let Some(header_line) = lines.next() {
result.columns = header_line.split(',').map(|s| s.trim().to_string()).collect();
}
// Parse data rows
for line in lines {
if line.is_empty() || line.starts_with('#') {
continue;
}
let values: Vec<serde_json::Value> = line
.split(',')
.map(|s| {
let trimmed = s.trim();
// Try to parse as number first
if let Ok(n) = trimmed.parse::<i64>() {
serde_json::Value::Number(n.into())
} else if let Ok(n) = trimmed.parse::<f64>() {
serde_json::json!(n)
} else if trimmed == "true" {
serde_json::Value::Bool(true)
} else if trimmed == "false" {
serde_json::Value::Bool(false)
} else {
serde_json::Value::String(trimmed.to_string())
}
})
.collect();
result.rows.push(values);
}
Ok(result)
}
/// Query metrics for a specific time range
pub async fn query_range(
&self,
measurement: &str,
start: &str,
stop: Option<&str>,
aggregation_window: Option<&str>,
) -> Result<QueryResult, TimeSeriesError> {
let stop_clause = stop.map_or("now()".to_string(), |s| format!("\"{}\"", s));
let window = aggregation_window.unwrap_or("1m");
let flux = format!(
r#"from(bucket: "{}")
|> range(start: {}, stop: {})
|> filter(fn: (r) => r._measurement == "{}")
|> aggregateWindow(every: {}, fn: mean, createEmpty: false)
|> yield(name: "mean")"#,
self.config.bucket, start, stop_clause, measurement, window
);
self.query(&flux).await
}
/// Get the latest value for a measurement
pub async fn query_last(&self, measurement: &str) -> Result<QueryResult, TimeSeriesError> {
let flux = format!(
r#"from(bucket: "{}")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "{}")
|> last()"#,
self.config.bucket, measurement
);
self.query(&flux).await
}
/// Get aggregated statistics for a measurement
pub async fn query_stats(
&self,
measurement: &str,
start: &str,
) -> Result<QueryResult, TimeSeriesError> {
let flux = format!(
r#"from(bucket: "{}")
|> range(start: {})
|> filter(fn: (r) => r._measurement == "{}")
|> group()
|> reduce(
identity: {{count: 0.0, sum: 0.0, min: 0.0, max: 0.0}},
fn: (r, accumulator) => ({{
count: accumulator.count + 1.0,
sum: accumulator.sum + r._value,
min: if accumulator.count == 0.0 then r._value else if r._value < accumulator.min then r._value else accumulator.min,
max: if accumulator.count == 0.0 then r._value else if r._value > accumulator.max then r._value else accumulator.max
}})
)"#,
self.config.bucket, start, measurement
);
self.query(&flux).await
}
/// Check if InfluxDB is healthy
pub async fn health_check(&self) -> Result<bool, TimeSeriesError> {
let url = format!("{}/health", self.config.url);
let response = self
.http_client
.get(&url)
.send()
.await
.map_err(|e| TimeSeriesError::ConnectionError(e.to_string()))?;
Ok(response.status().is_success())
}
}
/// Pre-defined metric types for General Bots
pub struct Metrics;
impl Metrics {
/// Record a message event
pub fn message(bot_id: &str, channel: &str, direction: &str) -> MetricPoint {
MetricPoint::new("messages")
.tag("bot_id", bot_id)
.tag("channel", channel)
.tag("direction", direction)
.field_i64("count", 1)
.at(Utc::now())
}
/// Record response time
pub fn response_time(bot_id: &str, duration_ms: f64) -> MetricPoint {
MetricPoint::new("response_time")
.tag("bot_id", bot_id)
.field_f64("duration_ms", duration_ms)
.at(Utc::now())
}
/// Record LLM token usage
pub fn llm_tokens(
bot_id: &str,
model: &str,
prompt_tokens: i64,
completion_tokens: i64,
) -> MetricPoint {
MetricPoint::new("llm_tokens")
.tag("bot_id", bot_id)
.tag("model", model)
.field_i64("prompt_tokens", prompt_tokens)
.field_i64("completion_tokens", completion_tokens)
.field_i64("total_tokens", prompt_tokens + completion_tokens)
.at(Utc::now())
}
/// Record active session count
pub fn active_sessions(bot_id: &str, count: i64) -> MetricPoint {
MetricPoint::new("active_sessions")
.tag("bot_id", bot_id)
.field_i64("count", count)
.at(Utc::now())
}
/// Record an error
pub fn error(bot_id: &str, error_type: &str, message: &str) -> MetricPoint {
MetricPoint::new("errors")
.tag("bot_id", bot_id)
.tag("error_type", error_type)
.field_i64("count", 1)
.field_str("message", message)
.at(Utc::now())
}
/// Record storage usage
pub fn storage_usage(bot_id: &str, bytes_used: u64, file_count: u64) -> MetricPoint {
MetricPoint::new("storage_usage")
.tag("bot_id", bot_id)
.field_u64("bytes_used", bytes_used)
.field_u64("file_count", file_count)
.at(Utc::now())
}
/// Record API request
pub fn api_request(endpoint: &str, method: &str, status_code: i64, duration_ms: f64) -> MetricPoint {
MetricPoint::new("api_requests")
.tag("endpoint", endpoint)
.tag("method", method)
.field_i64("status_code", status_code)
.field_f64("duration_ms", duration_ms)
.field_i64("count", 1)
.at(Utc::now())
}
/// Record system metrics (CPU, memory, etc.)
pub fn system(cpu_percent: f64, memory_percent: f64, disk_percent: f64) -> MetricPoint {
MetricPoint::new("system_metrics")
.field_f64("cpu_percent", cpu_percent)
.field_f64("memory_percent", memory_percent)
.field_f64("disk_percent", disk_percent)
.at(Utc::now())
}
}
/// Errors that can occur when working with time-series data
#[derive(Debug, Clone)]
pub enum TimeSeriesError {
ConnectionError(String),
WriteError(String),
QueryError(String),
ConfigError(String),
}
impl std::fmt::Display for TimeSeriesError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TimeSeriesError::ConnectionError(msg) => write!(f, "Connection error: {}", msg),
TimeSeriesError::WriteError(msg) => write!(f, "Write error: {}", msg),
TimeSeriesError::QueryError(msg) => write!(f, "Query error: {}", msg),
TimeSeriesError::ConfigError(msg) => write!(f, "Config error: {}", msg),
}
}
}
impl std::error::Error for TimeSeriesError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metric_point_line_protocol() {
let point = MetricPoint::new("test_measurement")
.tag("host", "server01")
.tag("region", "us-west")
.field_f64("temperature", 23.5)
.field_i64("humidity", 45);
let line = point.to_line_protocol();
assert!(line.starts_with("test_measurement,"));
assert!(line.contains("host=server01"));
assert!(line.contains("region=us-west"));
assert!(line.contains("temperature=23.5"));
assert!(line.contains("humidity=45i"));
}
#[test]
fn test_metric_point_escaping() {
let point = MetricPoint::new("test")
.tag("key with space", "value,with=special")
.field_str("message", "Hello \"world\"");
let line = point.to_line_protocol();
assert!(line.contains("key\\ with\\ space=value\\,with\\=special"));
assert!(line.contains("message=\"Hello \\\"world\\\"\""));
}
#[test]
fn test_predefined_metrics() {
let msg = Metrics::message("bot-1", "whatsapp", "incoming");
assert_eq!(msg.measurement, "messages");
assert_eq!(msg.tags.get("channel"), Some(&"whatsapp".to_string()));
let resp = Metrics::response_time("bot-1", 150.5);
assert_eq!(resp.measurement, "response_time");
let tokens = Metrics::llm_tokens("bot-1", "gpt-4", 100, 50);
assert_eq!(tokens.measurement, "llm_tokens");
}
}

View file

@ -1,343 +1,178 @@
# General Bots Templates
The General Bots Templates provide ready-to-use business solutions powered by conversational AI. Each template includes pre-configured dialogs, database schemas, scheduled jobs, webhooks, and tools that can be customized for your specific needs.
Pre-built bot packages for common business use cases. Templates are organized by category for easy discovery.
## 📁 Template Structure
## Categories
Each template follows this standard structure:
### `/compliance`
Privacy and regulatory compliance templates.
| Template | Description | Regulations |
|----------|-------------|-------------|
| `privacy.gbai` | Data subject rights portal | LGPD, GDPR, CCPA |
| `hipaa-medical.gbai` | Healthcare privacy management | HIPAA, HITECH |
### `/sales`
Customer relationship and marketing templates.
| Template | Description | Features |
|----------|-------------|----------|
| `crm.gbai` | Full CRM system | Leads, Contacts, Accounts, Opportunities, Activities |
| `marketing.gbai` | Marketing automation | Campaigns, Lead capture, Email sequences |
### `/productivity`
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. |
### `/hr`
Human resources templates.
| Template | Description | Features |
|----------|-------------|----------|
| `employee-mgmt.gbai` | Employee management | Directory, Onboarding |
### `/it`
IT service management templates.
| Template | Description | Features |
|----------|-------------|----------|
| `helpdesk.gbai` | IT helpdesk ticketing | Tickets, Knowledge base |
### `/healthcare`
Healthcare-specific templates.
| Template | Description | Features |
|----------|-------------|----------|
| `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.
| Template | Description | Features |
|----------|-------------|----------|
| `donor-mgmt.gbai` | Donor management | Donations, Communications |
### Root Level
Core and utility templates.
| Template | Description |
|----------|-------------|
| `default.gbai` | Starter template |
| `ai-search.gbai` | AI-powered document search |
| `announcements.gbai` | Company announcements |
| `backup.gbai` | Backup automation |
| `broadcast.gbai` | Message broadcasting |
| `crawler.gbai` | Web crawling |
| `edu.gbai` | Education/training |
| `erp.gbai` | ERP integration |
| `law.gbai` | Legal document processing |
| `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:
```
template-name.gbai/
├── template-name.gbdialog/ # BASIC scripts (.bas files)
│ ├── start.bas # Initial setup, tools, welcome message
│ ├── tables.bas # Database schema definitions
│ ├── *-jobs.bas # Scheduled automation jobs
│ └── *.bas # Tool implementations
├── template-name.gbot/ # Bot configuration
│ └── config.csv # Theme, prompts, settings
├── template-name.gbkb/ # Knowledge base content
├── template-name.gbdrive/ # Document templates, assets
└── template-name.gbdata/ # Initial data files
├── README.md # Template documentation
├── template-name.gbdialog/ # BASIC dialog scripts
│ ├── start.bas # Entry point
│ └── *.bas # Additional dialogs
├── template-name.gbot/ # Bot configuration
│ └── config.csv # Settings
├── template-name.gbkb/ # Knowledge base (optional)
│ └── docs/ # Documents for RAG
├── template-name.gbdrive/ # File storage (optional)
└── template-name.gbui/ # Custom UI (optional)
└── index.html
```
## 🏷️ Template Categories
## Installation
| Category | Icon | Description |
|----------|------|-------------|
| **CRM & Sales** | 💼 | Customer relationships, leads, opportunities, sales pipeline |
| **Operations & ERP** | 🏭 | Inventory, purchasing, supply chain, production |
| **Human Resources** | 👥 | Employees, attendance, leave, recruitment |
| **Finance & Accounting** | 💰 | Invoicing, expenses, budgets, billing |
| **Healthcare & Medical** | 🏥 | Patients, appointments, pharmacy, medical billing |
| **Education & Training** | 🎓 | Students, courses, enrollment, faculty |
| **Real Estate** | 🏠 | Properties, leases, tenants, maintenance |
| **Legal & Compliance** | ⚖️ | Cases, contracts, compliance tracking |
| **Events & Scheduling** | 📅 | Events, room/desk booking, reservations |
| **IT & Support** | 🖥️ | Helpdesk, tickets, assets, bug tracking |
| **Marketing** | 📢 | Campaigns, content, social media, broadcasts |
| **Nonprofit** | 🤝 | Donors, volunteers, memberships, fundraising |
| **AI & Data** | 🤖 | Search, crawling, talk-to-data, LLM tools |
---
## 📋 Available Templates
### 💼 CRM & Sales
| Template | Folder | Description | Key Features |
|----------|--------|-------------|--------------|
| **CRM** | `crm.gbai` | Complete CRM system | Lead management, opportunity tracking, case management, quotes, email campaigns |
| **Store** | `store.gbai` | E-commerce checkout | Product catalog, cart, checkout flow |
### 🏭 Operations & ERP
| Template | Folder | Description | Key Features |
|----------|--------|-------------|--------------|
| **ERP** | `erp.gbai` | Enterprise resource planning | Inventory management, purchasing, warehouse operations |
### 👥 Human Resources
| Template | Folder | Description | Key Features |
|----------|--------|-------------|--------------|
| **Employees** | `hr/employees.gbai` | Employee management system | Directory, onboarding, org chart, emergency contacts, document tracking |
### 🎓 Education & Training
| Template | Folder | Description | Key Features |
|----------|--------|-------------|--------------|
| **Education** | `edu.gbai` | Educational enrollment | Student enrollment, course management, data collection |
### ⚖️ Legal & Compliance
| Template | Folder | Description | Key Features |
|----------|--------|-------------|--------------|
| **Law** | `law.gbai` | Legal case management | Case summaries, document querying, legal research |
### 🖥️ IT & Support
| Template | Folder | Description | Key Features |
|----------|--------|-------------|--------------|
| **Helpdesk** | `it/helpdesk.gbai` | IT support ticketing | Ticket creation, SLA tracking, escalation, webhooks for integration |
### 📢 Marketing & Communications
| Template | Folder | Description | Key Features |
|----------|--------|-------------|--------------|
| **Marketing** | `marketing.gbai` | Marketing automation | Social posting, broadcasts, content ideas |
| **Announcements** | `announcements.gbai` | Company communications | News distribution, scheduled summaries |
| **Broadcast** | `broadcast.gbai` | Message broadcasting | Multi-channel broadcasts |
### 🤖 AI & Data
| Template | Folder | Description | Key Features |
|----------|--------|-------------|--------------|
| **AI Search** | `ai-search.gbai` | Document search & QR | PDF search, QR code scanning, AI summaries |
| **Crawler** | `crawler.gbai` | Website data extraction | Web crawling, knowledge updates |
| **Talk to Data** | `talk-to-data.gbai` | Natural language SQL | Query databases in plain English, charts |
| **LLM Server** | `llm-server.gbai` | LLM as REST API | API generation for LLM access |
| **LLM Tools** | `llm-tools.gbai` | Custom LLM integration | Real-time data access, custom logic |
| **BI** | `bi.gbai` | Business intelligence | Dashboards, analytics |
### 🔧 Utility Templates
| Template | Folder | Description | Key Features |
|----------|--------|-------------|--------------|
| **Default** | `default.gbai` | Base template | Starting point for custom bots |
| **Office** | `office.gbai` | Office automation | Document processing, API integration, data sync |
| **Reminder** | `reminder.gbai` | Reminder system | Scheduled reminders |
| **Backup** | `backup.gbai` | Data backup | Automated backups |
| **API Client** | `api-client.gbai` | API consumption | External API integration |
| **Public APIs** | `public-apis.gbai` | Public API access | Common public API integrations |
| **WhatsApp** | `whatsapp.gbai` | WhatsApp integration | WhatsApp-specific features |
---
## 🔧 Key Components
### start.bas - Template Initialization
Every template should have a `start.bas` that:
1. **Registers Tools** - Makes functions available to the AI
2. **Sets Up Knowledge Base** - Loads relevant KB content
3. **Configures Context** - Sets the AI personality/role
4. **Adds Suggestions** - Provides quick-action buttons
5. **Displays Welcome** - Greets users with capabilities
```basic
' Example start.bas structure
ADD TOOL "create-ticket"
ADD TOOL "search-tickets"
USE KB "helpdesk.gbkb"
SET_CONTEXT "helpdesk" AS "You are an IT support assistant..."
CLEAR_SUGGESTIONS
ADD_SUGGESTION "new" AS "Create new ticket"
ADD_SUGGESTION "status" AS "Check ticket status"
BEGIN TALK
Welcome to IT Helpdesk!
How can I help you today?
END TALK
BEGIN SYSTEM PROMPT
You are an IT support assistant...
END SYSTEM PROMPT
```
### tables.bas - Database Schema
Define your data model using the TABLE keyword:
```basic
TABLE employees
id UUID PRIMARY KEY DEFAULT uuid_generate_v4()
first_name VARCHAR(100) NOT NULL
last_name VARCHAR(100) NOT NULL
email VARCHAR(255) UNIQUE NOT NULL
department_id UUID REFERENCES departments(id)
hire_date DATE NOT NULL
is_active BOOLEAN DEFAULT TRUE
created_at TIMESTAMP DEFAULT NOW()
END TABLE
```
### *-jobs.bas - Scheduled Automation
Set up recurring tasks with SET SCHEDULE:
```basic
PARAM job_name AS STRING
IF job_name = "daily_report" THEN
' Generate and send daily report
...
END IF
IF job_name = "setup_schedules" THEN
SET SCHEDULE "0 8 * * *" "jobs.bas" "daily_report"
SET SCHEDULE "0 18 * * *" "jobs.bas" "end_of_day"
TALK "Schedules configured"
END IF
```
### Webhook Scripts
Expose scripts as HTTP endpoints:
```basic
WEBHOOK "ticket-webhook"
PARAM action AS STRING
PARAM ticket_number AS STRING
...
' Validate API key
api_key = GET "webhook.headers.X-API-Key"
IF api_key != expected_key THEN
RETURN #{ status: 401, error: "Unauthorized" }
END IF
' Process webhook
IF action = "create" THEN
...
END IF
```
### Tool Scripts
Functions that AI can call (registered with ADD TOOL):
```basic
PARAM name AS STRING LIKE "John" DESCRIPTION "Employee name"
PARAM email AS STRING LIKE "john@co.com" DESCRIPTION "Email address"
DESCRIPTION "Creates a new employee record in the system"
' Implementation
employee = CREATE OBJECT
SET employee.name = name
SET employee.email = email
...
SAVE "employees", employee.id, employee
RETURN #{ success: true, employee_id: employee.id }
```
---
## 🚀 Getting Started
### 1. Choose a Template
Browse the categories above and select a template that matches your use case.
### 2. Copy to Your Bot
### From Console
```bash
cp -r templates/hr/employees.gbai your-bot.gbai
botserver --install-template crm
```
### 3. Customize
- Edit `config.csv` for branding (colors, logo, title)
- Modify `tables.bas` for your data model
- Update tools in `.gbdialog/` for your workflow
- Add content to `.gbkb/` for AI knowledge
### 4. Initialize
Run the template's setup job to configure schedules:
### From BASIC
```basic
' In conversation or via API
RUN "jobs.bas" WITH job_name = "setup_schedules"
INSTALL TEMPLATE "crm"
```
---
### Manual
## 📖 Available Keywords
Copy the template folder to your bot's packages directory:
Templates use these BASIC keywords:
```bash
cp -r templates/sales/crm.gbai /path/to/your/bot/packages/
```
### Data Operations
- `SAVE`, `INSERT`, `UPDATE`, `DELETE`, `MERGE`
- `FIND`, `FILTER`, `MAP`, `JOIN`, `GROUP_BY`
- `IMPORT`, `EXPORT` (CSV, JSON, Excel)
- `FILL` (template filling)
## Creating Custom Templates
### Communication
- `TALK`, `HEAR` - Conversation
- `SEND MAIL` - Email
- `SEND_SMS` - Text messages
- `BROADCAST` - Multi-recipient
1. Copy `template.gbai` as a starting point
2. Rename the folder to `your-template.gbai`
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`
### Scheduling & Tasks
- `SET SCHEDULE` - Cron jobs
- `CREATE_TASK` - Task management
- `BOOK` - Calendar booking
### Template Best Practices
### AI & LLM
- `LLM` - Call language model
- `CALCULATE` - LLM-based calculations
- `VALIDATE` - LLM-based validation
- `TRANSLATE` - Translation
- `SUMMARIZE` - Text summarization
- `EXTRACT_DATA` - Data extraction
- Use `HEAR AS` for typed input validation
- Use spaces in keywords (e.g., `SET BOT MEMORY`, not `SET_BOT_MEMORY`)
- Log activities for audit trails
- Include error handling
- Document all configuration options
- Provide example conversations
### Files & Documents
- `READ`, `WRITE`, `COPY`, `MOVE`
- `GENERATE_PDF`, `MERGE_PDF`
- `COMPRESS`, `EXTRACT`
- `UPLOAD`, `DOWNLOAD`
## Contributing Templates
### Integrations
- `POST`, `PUT`, `PATCH`, `DELETE` - HTTP
- `GRAPHQL`, `SOAP` - API protocols
- `WEBHOOK` - Expose endpoints
- `QR_CODE` - Generate QR codes
1. Create your template following the structure above
2. Test thoroughly with different inputs
3. Document all features and configuration
4. Submit a pull request with:
- Template files
- Updated category README
- Entry in this document
### Multimedia
- `IMAGE`, `VIDEO`, `AUDIO` - Generation
- `SEE` - Vision/captioning
## License
All templates are licensed under AGPL-3.0 as part of General Bots.
---
## 📊 Template Feature Matrix
| Template | Tools | Schedules | Webhooks | KB | Drive |
|----------|:-----:|:---------:|:--------:|:--:|:-----:|
| CRM | ✅ | ✅ | ✅ | ⬜ | ⬜ |
| ERP | ✅ | ✅ | ⬜ | ⬜ | ⬜ |
| Employees | ✅ | ✅ | ⬜ | ⬜ | ✅ |
| Helpdesk | ✅ | ✅ | ✅ | ✅ | ✅ |
| Education | ✅ | ⬜ | ⬜ | ⬜ | ⬜ |
| AI Search | ✅ | ⬜ | ⬜ | ✅ | ✅ |
| Announcements | ✅ | ✅ | ⬜ | ✅ | ⬜ |
| Marketing | ✅ | ⬜ | ⬜ | ⬜ | ⬜ |
---
## 🔗 Resources
- [Full Documentation](https://docs.pragmatismo.com.br)
- [BASIC Language Reference](https://docs.pragmatismo.com.br/basic)
- [API Reference](https://docs.pragmatismo.com.br/api)
---
## 📝 Creating Custom Templates
1. Start from `default.gbai` or copy an existing template
2. Define your data model in `tables.bas`
3. Create tools for your business logic
4. Set up scheduled jobs for automation
5. Add webhooks for external integrations
6. Configure `start.bas` for initialization
7. Add knowledge base content for AI context
---
*General Bots Templates - Conversational AI for Business*
**Pragmatismo** - General Bots Open Source Platform

View file

@ -0,0 +1,685 @@
' General Bots Conversational Banking
' Enterprise-grade banking through natural conversation
' Uses TOOLS (not SUBs) and HEAR AS validation
' ============================================================================
' CONFIGURATION
' ============================================================================
SET CONTEXT "You are a professional banking assistant for General Bank.
Help customers with accounts, transfers, payments, cards, loans, and investments.
Always verify identity before sensitive operations. Be helpful and secure.
Use the available tools to perform banking operations.
Never ask for full card numbers or passwords in chat."
USE KB "banking-faq"
' Add specialized bots for complex operations
ADD BOT "fraud-detector" WITH TRIGGER "suspicious, fraud, unauthorized, stolen, hack"
ADD BOT "investment-advisor" WITH TRIGGER "invest, stocks, funds, portfolio, returns, CDB, LCI"
ADD BOT "loan-specialist" WITH TRIGGER "loan, financing, credit, mortgage, empréstimo"
ADD BOT "card-services" WITH TRIGGER "card, limit, block, virtual card, cartão"
' ============================================================================
' BANKING TOOLS - Dynamic tools added to conversation
' ============================================================================
' Account Tools
USE TOOL "check_balance"
USE TOOL "get_statement"
USE TOOL "get_transactions"
' Transfer Tools
USE TOOL "pix_transfer"
USE TOOL "ted_transfer"
USE TOOL "schedule_transfer"
' Payment Tools
USE TOOL "pay_boleto"
USE TOOL "pay_utility"
USE TOOL "list_scheduled_payments"
' Card Tools
USE TOOL "list_cards"
USE TOOL "block_card"
USE TOOL "unblock_card"
USE TOOL "create_virtual_card"
USE TOOL "request_limit_increase"
' Loan Tools
USE TOOL "simulate_loan"
USE TOOL "apply_loan"
USE TOOL "list_loans"
' Investment Tools
USE TOOL "get_portfolio"
USE TOOL "list_investments"
USE TOOL "buy_investment"
USE TOOL "redeem_investment"
' ============================================================================
' AUTHENTICATION FLOW
' ============================================================================
authenticated = GET user_authenticated
IF NOT authenticated THEN
TALK "Welcome to General Bank! 🏦"
TALK "For your security, I need to verify your identity."
TALK ""
TALK "Please enter your CPF:"
HEAR cpf AS CPF
' Look up customer
customer = FIND "customers.csv" WHERE cpf = cpf
IF LEN(customer) = 0 THEN
TALK "I couldn't find an account with this CPF."
TALK "Please check the number or visit a branch to open an account."
ELSE
' Send verification code
phone_masked = MID(FIRST(customer).phone, 1, 4) + "****" + RIGHT(FIRST(customer).phone, 2)
TALK "I'll send a verification code to your phone ending in " + phone_masked
' Generate and store code
code = STR(INT(RND() * 900000) + 100000)
SET BOT MEMORY "verification_code", code
SET BOT MEMORY "verification_cpf", cpf
' In production: SEND SMS FIRST(customer).phone, "Your General Bank code is: " + code
TALK "Please enter the 6-digit code:"
HEAR entered_code AS INTEGER
stored_code = GET BOT MEMORY "verification_code"
IF STR(entered_code) = stored_code THEN
SET user_authenticated, TRUE
SET user_id, FIRST(customer).id
SET user_name, FIRST(customer).name
SET user_cpf, cpf
TALK "✅ Welcome, " + FIRST(customer).name + "!"
ELSE
TALK "❌ Invalid code. Please try again."
END IF
END IF
END IF
' ============================================================================
' MAIN CONVERSATION - LLM handles intent naturally
' ============================================================================
IF GET user_authenticated THEN
user_name = GET user_name
TALK ""
TALK "How can I help you today, " + user_name + "?"
TALK ""
TALK "You can ask me things like:"
TALK "• What's my balance?"
TALK "• Send R$ 100 via PIX to 11999998888"
TALK "• Pay this boleto: 23793.38128..."
TALK "• Block my credit card"
TALK "• Simulate a loan of R$ 10,000"
ADD SUGGESTION "Check balance"
ADD SUGGESTION "Make a transfer"
ADD SUGGESTION "Pay a bill"
ADD SUGGESTION "My cards"
END IF
' ============================================================================
' TOOL: check_balance
' Returns account balances for the authenticated user
' ============================================================================
' @tool check_balance
' @description Get account balances for the current user
' @param account_type string optional Filter by account type (checking, savings, all)
' @returns Account balances with available amounts
' ============================================================================
' TOOL: pix_transfer
' Performs a PIX transfer
' ============================================================================
' @tool pix_transfer
' @description Send money via PIX instant transfer
' @param pix_key string required The recipient's PIX key (CPF, phone, email, or random key)
' @param amount number required Amount to transfer in BRL
' @param description string optional Transfer description
' @returns Transfer confirmation with transaction ID
ON TOOL "pix_transfer"
pix_key = GET TOOL PARAM "pix_key"
amount = GET TOOL PARAM "amount"
description = GET TOOL PARAM "description"
' Validate PIX key format
TALK "🔍 Validating PIX key..."
' Get recipient info (simulated API call)
recipient_name = LLM "Given PIX key " + pix_key + ", return a realistic Brazilian name. Just the name, nothing else."
recipient_bank = "Banco Example"
TALK ""
TALK "📤 **Transfer Details**"
TALK "To: **" + recipient_name + "**"
TALK "Bank: " + recipient_bank
TALK "Amount: **R$ " + FORMAT(amount, "#,##0.00") + "**"
TALK ""
TALK "Confirm this PIX transfer?"
ADD SUGGESTION "Yes, confirm"
ADD SUGGESTION "No, cancel"
HEAR confirmation AS BOOLEAN
IF confirmation THEN
TALK "🔐 Enter your 4-digit PIN:"
HEAR pin AS INTEGER
' Validate PIN (in production, verify against stored hash)
IF LEN(STR(pin)) = 4 THEN
' Execute transfer
transaction_id = "PIX" + FORMAT(NOW(), "yyyyMMddHHmmss") + STR(INT(RND() * 1000))
' Get current balance
user_id = GET user_id
account = FIRST(FIND "accounts.csv" WHERE user_id = user_id)
new_balance = account.balance - amount
' Save transaction
TABLE transaction
ROW transaction_id, account.account_number, "pix_out", -amount, new_balance, NOW(), pix_key, recipient_name, "completed"
END TABLE
SAVE "transactions.csv", transaction
' Update balance
UPDATE "accounts.csv" SET balance = new_balance WHERE id = account.id
TALK ""
TALK "✅ **PIX Transfer Completed!**"
TALK ""
TALK "Transaction ID: " + transaction_id
TALK "Amount: R$ " + FORMAT(amount, "#,##0.00")
TALK "New Balance: R$ " + FORMAT(new_balance, "#,##0.00")
TALK "Date: " + FORMAT(NOW(), "dd/MM/yyyy HH:mm")
RETURN transaction_id
ELSE
TALK "❌ Invalid PIN format."
RETURN "CANCELLED"
END IF
ELSE
TALK "Transfer cancelled."
RETURN "CANCELLED"
END IF
END ON
' ============================================================================
' TOOL: pay_boleto
' Pays a Brazilian bank slip (boleto)
' ============================================================================
' @tool pay_boleto
' @description Pay a boleto (bank slip) using the barcode
' @param barcode string required The boleto barcode (47 or 48 digits)
' @returns Payment confirmation
ON TOOL "pay_boleto"
barcode = GET TOOL PARAM "barcode"
' Clean barcode
barcode = REPLACE(REPLACE(REPLACE(barcode, ".", ""), " ", ""), "-", "")
IF LEN(barcode) <> 47 AND LEN(barcode) <> 48 THEN
TALK "❌ Invalid barcode. Please enter all 47 or 48 digits."
RETURN "INVALID_BARCODE"
END IF
' Parse boleto (simplified - in production use banking API)
beneficiary = "Company " + LEFT(barcode, 3)
amount = VAL(MID(barcode, 38, 10)) / 100
due_date = DATEADD(NOW(), INT(RND() * 30), "day")
TALK ""
TALK "📄 **Bill Details**"
TALK "Beneficiary: **" + beneficiary + "**"
TALK "Amount: **R$ " + FORMAT(amount, "#,##0.00") + "**"
TALK "Due Date: " + FORMAT(due_date, "dd/MM/yyyy")
TALK ""
TALK "Pay this bill now?"
ADD SUGGESTION "Yes, pay now"
ADD SUGGESTION "Schedule for due date"
ADD SUGGESTION "Cancel"
HEAR choice AS "Pay now", "Schedule", "Cancel"
IF choice = "Pay now" THEN
TALK "🔐 Enter your PIN:"
HEAR pin AS INTEGER
IF LEN(STR(pin)) = 4 THEN
transaction_id = "BOL" + FORMAT(NOW(), "yyyyMMddHHmmss")
auth_code = FORMAT(INT(RND() * 100000000), "00000000")
TALK ""
TALK "✅ **Payment Completed!**"
TALK ""
TALK "Transaction ID: " + transaction_id
TALK "Authentication: " + auth_code
TALK "Amount: R$ " + FORMAT(amount, "#,##0.00")
RETURN transaction_id
ELSE
TALK "❌ Invalid PIN."
RETURN "INVALID_PIN"
END IF
ELSEIF choice = "Schedule" THEN
TABLE scheduled
ROW NOW(), GET user_id, "boleto", barcode, amount, due_date, "pending"
END TABLE
SAVE "scheduled_payments.csv", scheduled
TALK "✅ Payment scheduled for " + FORMAT(due_date, "dd/MM/yyyy")
RETURN "SCHEDULED"
ELSE
TALK "Payment cancelled."
RETURN "CANCELLED"
END IF
END ON
' ============================================================================
' TOOL: block_card
' Blocks a card for security
' ============================================================================
' @tool block_card
' @description Block a credit or debit card
' @param card_type string optional Type of card to block (credit, debit, all)
' @param reason string optional Reason for blocking (lost, stolen, suspicious, temporary)
' @returns Block confirmation
ON TOOL "block_card"
card_type = GET TOOL PARAM "card_type"
reason = GET TOOL PARAM "reason"
user_id = GET user_id
cards = FIND "cards.csv" WHERE user_id = user_id AND status = "active"
IF LEN(cards) = 0 THEN
TALK "You don't have any active cards to block."
RETURN "NO_CARDS"
END IF
IF card_type = "" OR card_type = "all" THEN
TALK "Which card do you want to block?"
FOR i = 1 TO LEN(cards)
card = cards[i]
masked = "**** " + RIGHT(card.card_number, 4)
TALK STR(i) + ". " + UPPER(card.card_type) + " - " + masked
ADD SUGGESTION card.card_type + " " + RIGHT(card.card_number, 4)
NEXT
HEAR selection AS INTEGER
IF selection < 1 OR selection > LEN(cards) THEN
TALK "Invalid selection."
RETURN "INVALID_SELECTION"
END IF
selected_card = cards[selection]
ELSE
selected_card = FIRST(FILTER cards WHERE card_type = card_type)
END IF
IF reason = "" THEN
TALK "Why are you blocking this card?"
ADD SUGGESTION "Lost"
ADD SUGGESTION "Stolen"
ADD SUGGESTION "Suspicious activity"
ADD SUGGESTION "Temporary block"
HEAR reason AS "Lost", "Stolen", "Suspicious activity", "Temporary block"
END IF
' Block the card
UPDATE "cards.csv" SET status = "blocked", blocked_reason = reason, blocked_at = NOW() WHERE id = selected_card.id
masked = "**** " + RIGHT(selected_card.card_number, 4)
TALK ""
TALK "🔒 **Card Blocked**"
TALK ""
TALK "Card: " + UPPER(selected_card.card_type) + " " + masked
TALK "Reason: " + reason
TALK "Blocked at: " + FORMAT(NOW(), "dd/MM/yyyy HH:mm")
IF reason = "Stolen" OR reason = "Lost" THEN
TALK ""
TALK "⚠️ For your security, we recommend requesting a replacement card."
TALK "Would you like me to request a new card?"
ADD SUGGESTION "Yes, request new card"
ADD SUGGESTION "No, not now"
HEAR request_new AS BOOLEAN
IF request_new THEN
TALK "✅ New card requested! It will arrive in 5-7 business days."
END IF
END IF
RETURN "BLOCKED"
END ON
' ============================================================================
' TOOL: simulate_loan
' Simulates loan options
' ============================================================================
' @tool simulate_loan
' @description Simulate a personal loan with different terms
' @param amount number required Loan amount in BRL
' @param months integer optional Number of months (12, 24, 36, 48, 60)
' @param loan_type string optional Type of loan (personal, payroll, home_equity)
' @returns Loan simulation with monthly payments
ON TOOL "simulate_loan"
amount = GET TOOL PARAM "amount"
months = GET TOOL PARAM "months"
loan_type = GET TOOL PARAM "loan_type"
IF amount < 500 THEN
TALK "Minimum loan amount is R$ 500.00"
RETURN "AMOUNT_TOO_LOW"
END IF
IF amount > 100000 THEN
TALK "For amounts above R$ 100,000, please visit a branch."
RETURN "AMOUNT_TOO_HIGH"
END IF
IF months = 0 THEN
TALK "In how many months would you like to pay?"
ADD SUGGESTION "12 months"
ADD SUGGESTION "24 months"
ADD SUGGESTION "36 months"
ADD SUGGESTION "48 months"
ADD SUGGESTION "60 months"
HEAR months_input AS INTEGER
months = months_input
END IF
IF loan_type = "" THEN
loan_type = "personal"
END IF
' Calculate rates based on type
IF loan_type = "payroll" THEN
monthly_rate = 0.0149
rate_label = "1.49%"
ELSEIF loan_type = "home_equity" THEN
monthly_rate = 0.0099
rate_label = "0.99%"
ELSE
monthly_rate = 0.0199
rate_label = "1.99%"
END IF
' PMT calculation
pmt = amount * (monthly_rate * POWER(1 + monthly_rate, months)) / (POWER(1 + monthly_rate, months) - 1)
total = pmt * months
interest_total = total - amount
TALK ""
TALK "💰 **Loan Simulation**"
TALK ""
TALK "📊 **" + UPPER(loan_type) + " LOAN**"
TALK ""
TALK "Amount: R$ " + FORMAT(amount, "#,##0.00")
TALK "Term: " + STR(months) + " months"
TALK "Interest Rate: " + rate_label + " per month"
TALK ""
TALK "📅 **Monthly Payment: R$ " + FORMAT(pmt, "#,##0.00") + "**"
TALK ""
TALK "Total to pay: R$ " + FORMAT(total, "#,##0.00")
TALK "Total interest: R$ " + FORMAT(interest_total, "#,##0.00")
TALK ""
TALK "Would you like to apply for this loan?"
ADD SUGGESTION "Yes, apply now"
ADD SUGGESTION "Try different values"
ADD SUGGESTION "Not now"
HEAR decision AS "Apply", "Try again", "No"
IF decision = "Apply" THEN
TALK "Great! Let me collect some additional information."
TALK "What is your monthly income?"
HEAR income AS MONEY
TALK "What is your profession?"
HEAR profession AS NAME
' Check debt-to-income ratio
IF pmt > income * 0.35 THEN
TALK "⚠️ The monthly payment exceeds 35% of your income."
TALK "We recommend a smaller amount or longer term."
RETURN "HIGH_DTI"
END IF
application_id = "LOAN" + FORMAT(NOW(), "yyyyMMddHHmmss")
TABLE loan_application
ROW application_id, GET user_id, loan_type, amount, months, monthly_rate, income, profession, NOW(), "pending"
END TABLE
SAVE "loan_applications.csv", loan_application
TALK ""
TALK "🎉 **Application Submitted!**"
TALK ""
TALK "Application ID: " + application_id
TALK "Status: Under Analysis"
TALK ""
TALK "We'll analyze your application within 24 hours."
TALK "You'll receive updates via app notifications."
RETURN application_id
ELSEIF decision = "Try again" THEN
TALK "No problem! What values would you like to try?"
RETURN "RETRY"
ELSE
TALK "No problem! I'm here whenever you need."
RETURN "DECLINED"
END IF
END ON
' ============================================================================
' TOOL: create_virtual_card
' Creates a virtual card for online purchases
' ============================================================================
' @tool create_virtual_card
' @description Create a virtual credit card for online shopping
' @param limit number optional Maximum limit for the virtual card
' @returns Virtual card details
ON TOOL "create_virtual_card"
limit = GET TOOL PARAM "limit"
user_id = GET user_id
credit_cards = FIND "cards.csv" WHERE user_id = user_id AND card_type = "credit" AND status = "active"
IF LEN(credit_cards) = 0 THEN
TALK "You need an active credit card to create virtual cards."
RETURN "NO_CREDIT_CARD"
END IF
main_card = FIRST(credit_cards)
IF limit = 0 THEN
TALK "What limit would you like for this virtual card?"
TALK "Available credit: R$ " + FORMAT(main_card.available_limit, "#,##0.00")
ADD SUGGESTION "R$ 100"
ADD SUGGESTION "R$ 500"
ADD SUGGESTION "R$ 1000"
ADD SUGGESTION "Custom amount"
HEAR limit AS MONEY
END IF
IF limit > main_card.available_limit THEN
TALK "❌ Limit exceeds available credit."
TALK "Maximum available: R$ " + FORMAT(main_card.available_limit, "#,##0.00")
RETURN "LIMIT_EXCEEDED"
END IF
' Generate virtual card
virtual_number = "4" + FORMAT(INT(RND() * 1000000000000000), "000000000000000")
virtual_cvv = FORMAT(INT(RND() * 1000), "000")
virtual_expiry = FORMAT(DATEADD(NOW(), 1, "year"), "MM/yy")
virtual_id = "VC" + FORMAT(NOW(), "yyyyMMddHHmmss")
TABLE virtual_card
ROW virtual_id, user_id, main_card.id, "virtual", virtual_number, virtual_cvv, virtual_expiry, limit, limit, "active", NOW()
END TABLE
SAVE "cards.csv", virtual_card
' Format card number for display
formatted_number = LEFT(virtual_number, 4) + " " + MID(virtual_number, 5, 4) + " " + MID(virtual_number, 9, 4) + " " + RIGHT(virtual_number, 4)
TALK ""
TALK "✅ **Virtual Card Created!**"
TALK ""
TALK "🔢 Number: " + formatted_number
TALK "📅 Expiry: " + virtual_expiry
TALK "🔐 CVV: " + virtual_cvv
TALK "💰 Limit: R$ " + FORMAT(limit, "#,##0.00")
TALK ""
TALK "⚠️ **Save these details now!**"
TALK "The CVV will not be shown again for security."
TALK ""
TALK "This virtual card is linked to your main credit card."
TALK "You can delete it anytime."
RETURN virtual_id
END ON
' ============================================================================
' TOOL: get_statement
' Gets account statement
' ============================================================================
' @tool get_statement
' @description Get account statement for a period
' @param period string optional Period: "30days", "90days", "month", or custom dates
' @param format string optional Output format: "chat", "pdf", "email"
' @returns Statement data or download link
ON TOOL "get_statement"
period = GET TOOL PARAM "period"
format = GET TOOL PARAM "format"
user_id = GET user_id
account = FIRST(FIND "accounts.csv" WHERE user_id = user_id)
IF period = "" THEN
TALK "Select the period for your statement:"
ADD SUGGESTION "Last 30 days"
ADD SUGGESTION "Last 90 days"
ADD SUGGESTION "This month"
ADD SUGGESTION "Custom dates"
HEAR period_choice AS "30 days", "90 days", "This month", "Custom"
IF period_choice = "Custom" THEN
TALK "Enter start date:"
HEAR start_date AS DATE
TALK "Enter end date:"
HEAR end_date AS DATE
ELSEIF period_choice = "30 days" THEN
start_date = DATEADD(NOW(), -30, "day")
end_date = NOW()
ELSEIF period_choice = "90 days" THEN
start_date = DATEADD(NOW(), -90, "day")
end_date = NOW()
ELSE
start_date = DATEADD(NOW(), -DAY(NOW()) + 1, "day")
end_date = NOW()
END IF
END IF
' Get transactions
transactions = FIND "transactions.csv" WHERE account_number = account.account_number AND date >= start_date AND date <= end_date ORDER BY date DESC
IF LEN(transactions) = 0 THEN
TALK "No transactions found for this period."
RETURN "NO_TRANSACTIONS"
END IF
TALK ""
TALK "📋 **Account Statement**"
TALK "Period: " + FORMAT(start_date, "dd/MM/yyyy") + " to " + FORMAT(end_date, "dd/MM/yyyy")
TALK "Account: " + account.account_number
TALK ""
total_in = 0
total_out = 0
FOR EACH tx IN transactions
IF tx.amount > 0 THEN
icon = "💵 +"
total_in = total_in + tx.amount
ELSE
icon = "💸 "
total_out = total_out + ABS(tx.amount)
END IF
TALK icon + "R$ " + FORMAT(ABS(tx.amount), "#,##0.00") + " | " + FORMAT(tx.date, "dd/MM")
TALK " " + tx.description
NEXT
TALK ""
TALK "📊 **Summary**"
TALK "Total In: R$ " + FORMAT(total_in, "#,##0.00")
TALK "Total Out: R$ " + FORMAT(total_out, "#,##0.00")
TALK "Net: R$ " + FORMAT(total_in - total_out, "#,##0.00")
IF format = "pdf" OR format = "email" THEN
TALK ""
TALK "Would you like me to send this statement to your email?"
ADD SUGGESTION "Yes, send email"
ADD SUGGESTION "No, thanks"
HEAR send_email AS BOOLEAN
IF send_email THEN
customer = FIRST(FIND "customers.csv" WHERE id = user_id)
SEND MAIL customer.email, "Your General Bank Statement", "Please find attached your account statement.", "statement.pdf"
TALK "📧 Statement sent to your email!"
END IF
END IF
RETURN "SUCCESS"
END ON
' ============================================================================
' FALLBACK - Let LLM handle anything not covered by tools
' ============================================================================
' The LLM will use the available tools based on user intent
' No need for rigid menu systems - natural conversation flow

View file

@ -1,89 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}General Bots{% endblock %}</title>
<meta name="description" content="{% block description %}General Bots - AI-powered workspace{% endblock %}">
<meta name="theme-color" content="#3b82f6">
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/json-enc.js"></script>
<!-- Styles -->
<link rel="stylesheet" href="/css/app.css">
{% block styles %}{% endblock %}
</head>
<body hx-ext="ws" ws-connect="/ws">
<!-- Header -->
<header class="float-header">
<div class="header-left">
<a href="/" class="logo-wrapper" hx-get="/" hx-target="#main-content" hx-push-url="true">
<div class="logo-icon"></div>
<span class="logo-text">BotServer</span>
</a>
</div>
<div class="header-right">
<!-- Theme Toggle -->
<button class="icon-button"
hx-post="/api/theme/toggle"
hx-swap="none"
title="Toggle theme">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
</svg>
</button>
<!-- Apps Menu -->
<button class="icon-button apps-button"
hx-get="/api/apps/menu"
hx-target="#apps-dropdown"
hx-trigger="click"
title="Applications">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<circle cx="5" cy="5" r="2"></circle>
<circle cx="12" cy="5" r="2"></circle>
<circle cx="19" cy="5" r="2"></circle>
<circle cx="5" cy="12" r="2"></circle>
<circle cx="12" cy="12" r="2"></circle>
<circle cx="19" cy="12" r="2"></circle>
<circle cx="5" cy="19" r="2"></circle>
<circle cx="12" cy="19" r="2"></circle>
<circle cx="19" cy="19" r="2"></circle>
</svg>
</button>
<div id="apps-dropdown" class="apps-dropdown"></div>
<!-- User Avatar -->
<button class="user-avatar"
hx-get="/api/user/menu"
hx-target="#user-menu"
hx-trigger="click"
title="User Account">
<span>{{ user_initial|default("U") }}</span>
</button>
<div id="user-menu" class="user-menu"></div>
</div>
</header></span>
<!-- Main Content -->
<main id="main-content" class="container">
{% block content %}{% endblock %}
</main>
<!-- Notifications Container -->
<div id="notifications" class="notifications-container"></div>
<!-- HTMX Config -->
<!-- Minimal HTMX Application with Authentication -->
<script src="/static/js/htmx-app.js"></script>
<script src="/static/js/theme-manager.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View file

@ -0,0 +1,118 @@
# Bling ERP Integration (.gbdialog)
This package provides complete integration with [Bling ERP](https://www.bling.com.br/) for data synchronization and conversational commerce.
## Scripts
| File | Description |
|------|-------------|
| `start.bas` | Welcome message and system prompt configuration |
| `tables.bas` | Database schema definitions for all synced entities |
| `sync-erp.bas` | Main ERP synchronization (products, orders, contacts, vendors) |
| `sync-accounts.bas` | Accounts payable and receivable synchronization |
| `sync-inventory.bas` | Stock/inventory levels synchronization |
| `sync-suppliers.bas` | Supplier/vendor data synchronization |
| `add-stock.bas` | Manual stock adjustment tool |
| `data-analysis.bas` | LLM-powered data analysis and reporting |
| `refresh-llm.bas` | Scheduled LLM context refresh |
## Configuration
Configure the integration in `bling.gbot/config.csv`:
| Parameter | Description |
|-----------|-------------|
| `param-blingClientID` | Bling API Client ID |
| `param-blingClientSecret` | Bling API Client Secret |
| `param-blingHost` | Bling API base URL |
| `param-host` | API endpoint (default: `https://api.bling.com.br/Api/v3`) |
| `param-limit` | Records per page for API calls |
| `param-pages` | Maximum pages to sync |
| `param-admin1` | Primary admin email for notifications |
| `param-admin2` | Secondary admin email for notifications |
## Synchronized Entities
### Products (`maria.Produtos`)
- Product details, SKU, pricing
- Product variations and hierarchy
- Product images (`maria.ProdutoImagem`)
### Orders (`maria.Pedidos`)
- Sales orders with line items (`maria.PedidosItem`)
- Payment parcels (`maria.Parcela`)
### Contacts (`maria.Contatos`)
- Customers and suppliers
- Address and billing information
### Vendors (`maria.Vendedores`)
- Sales representatives
- Commission and discount limits
### Financial
- Accounts Receivable (`maria.ContasAReceber`)
- Accounts Payable (`maria.ContasAPagar`)
- Payment Methods (`maria.FormaDePagamento`)
- Revenue Categories (`maria.CategoriaReceita`)
### Inventory
- Stock by Warehouse (`maria.Depositos`)
- Product Suppliers (`maria.ProdutoFornecedor`)
- Price History (`maria.HistoricoPreco`)
## Scheduled Jobs
The following schedules are configured:
| Job | Schedule | Description |
|-----|----------|-------------|
| `sync-erp.bas` | Daily at 22:30 | Full ERP synchronization |
| `sync-accounts.bas` | Every 2 days at midnight | Financial accounts sync |
| `sync-inventory.bas` | Daily at 23:30 | Stock levels update |
| `refresh-llm.bas` | Daily at 21:00 | Refresh LLM context |
## Data Analysis
The `data-analysis.bas` script enables natural language queries against synced data:
**Example queries:**
- "Which products have excess stock that can be transferred?"
- "What are the top 10 best-selling products?"
- "What is the average ticket for each store?"
- "Which products need restocking?"
## Usage
### Manual Stock Adjustment
Vendors can adjust stock via conversation:
```basic
REM User provides SKU and quantity
REM Stock is updated in Bling and local database
```
### Running Sync Manually
```basic
RUN "sync-erp.bas"
RUN "sync-accounts.bas"
RUN "sync-inventory.bas"
```
## API Integration
All API calls use the Bling v3 REST API with pagination support:
- Products: `GET /produtos`
- Orders: `GET /pedidos/vendas`
- Contacts: `GET /contatos`
- Inventory: `GET /estoques/saldos`
- Accounts: `GET /contas/receber`, `GET /contas/pagar`
## Related Documentation
- [Bling API Documentation](https://developer.bling.com.br/)
- [General Bots BASIC Reference](../../docs/src/chapter-06-gbdialog/README.md)
- [Template Guide](../../README.md)

View file

@ -0,0 +1,32 @@
person = FIND "People.xlsx", "id=" + mobile
vendor = FIND "maria.Vendedores", "id=" + person.erpId
TALK "Olá " + vendor.Contato_Nome + "!"
REM Estoque pelo nome em caso de não presente na planilha
TALK "Qual o SKU do Produto?"
HEAR sku
produto = FIND "maria.Produtos", "sku=" + sku
TALK "Qual a quantidade que se deseja acrescentar?"
HEAR qtd
estoque = {
produto: {
id: produto.Id
},
deposito: {
id: person.deposito_Id
},
preco: produto.Preco,
operacao: "B",
quantidade: qtd,
observacoes: "Acréscimo de estoque."
}
rec = POST host + "/estoques", estoque
TALK "Estoque atualizado, obrigado."
TALK TO admin1, "Estoque do ${sku} foi atualizado com ${qtd}."
TALK TO admin2, "Estoque do ${sku} foi atualizado com ${qtd}."

View file

@ -0,0 +1,38 @@
ALLOW ROLE "analiseDados"
BEGIN TALK
Exemplos de perguntas para o *BlingBot*:
1. Quais são os produtos que têm estoque excessivo em uma loja e podem ser transferidos para outra loja com menor estoque?
2. Quais são os 10 produtos mais vendidos na loja {nome_loja} no período {periodo}?
3. Qual é o ticket médio da loja {nome_loja}?
4. Qual a quantidade disponível do produto {nome_produto} na loja {nome_loja}?
5. Quais produtos precisam ser transferidos da loja {origem} para a loja {destino}?
6. Quais produtos estão com estoque crítico na loja {nome_loja}?
7. Qual a sugestão de compra para o fornecedor {nome_fornecedor}?
8. Quantos pedidos são realizados por dia na loja {nome_loja}?
9. Quantos produtos ativos existem no sistema?
10. Qual o estoque disponível na loja {nome_loja}?
END TALK
REM SET SCHEDULE
SET CONTEXT "As lojas B, L e R estão identificadas no final dos nomes das colunas da tabela de Análise de Compras. Dicionário de dados AnaliseCompras.qtEstoqueL: Descrição quantidade do Leblon. AnaliseCompras.qtEstoqueB: Descrição quantidade da Barra AnaliseCompras.qtEstoqueR: Descrição quantidade do Rio Sul. Com base no comportamento de compra registrado, analise os dados fornecidos para identificar oportunidades de otimização de estoque. Aplique regras básicas de transferência de produtos entre as lojas, considerando a necessidade de balanceamento de inventário. Retorne um relatório das 10 ações mais críticas, detalhe a movimentação sugerida para cada produto. Deve indicar a loja de origem, a loja de destino e o motivo da transferência. A análise deve ser objetiva e pragmática, focando na melhoria da disponibilidade de produtos nas lojas. Sempre use LIKE %% para comparar nomes. IMPORTANTE: Compare sempre com a função LOWER ao filtrar valores, em ambos os operandos de texto em SQL, para ignorar case, exemplo WHERE LOWER(loja.nome) LIKE LOWER(%Leblon%)."
SET ANSWER MODE "sql"
TALK "Pergunte-me qualquer coisa sobre os seus dados."
REM IF mobile = "5521992223002" THEN
REM ELSE
REM TALK "Não autorizado."
REM END IF

View file

@ -0,0 +1,2 @@
SET SCHEDULE "0 0 21 * * *"
REFRESH "data-analysis"

View file

@ -0,0 +1,16 @@
TALK O BlingBot deseja boas-vindas!
TALK Qual o seu pedido?
BEGIN SYSTEM PROMPT
Você deve atuar como um chatbot funcionário da loja integrada ao Bling ERP, respeitando as seguintes regras:
Sempre que o atendente fizer um pedido, ofereça as condições de cor e tamanho presentes no JSON de produtos.
A cada pedido realizado, retorne JSON similar ao JSONPedidosExemplo adicionados e o nome do cliente.
Mantenha itensPedido com apenas um item.
É importante usar o mesmo id do JSON de produtos fornecido, para haver a correlação dos objetos.
ItensAcompanhamento deve conter a coleção de itens de acompanhamento do pedido, que é solicitado quando o pedido é feito, por exemplo: Quadro, com Caixa de Giz.
END SYSTEM PROMPT

View file

@ -0,0 +1,92 @@
REM Executa a cada dois dias, 23h.
SET SCHEDULE "0 0 0 */2 * *"
REM Variables from config.csv: admin1, admin2, host, limit, pages
REM Using admin1 for notifications
admin = admin1
REM Pagination settings for Bling API
pageVariable = "pagina"
limitVariable = "limite"
syncLimit = 100
REM ============================================
REM Sync Contas a Receber (Accounts Receivable)
REM ============================================
SEND EMAIL admin, "Sincronizando Contas a Receber..."
page = 1
totalReceber = 0
DO WHILE page > 0 AND page <= pages
url = host + "/contas/receber?" + pageVariable + "=" + page + "&" + limitVariable + "=" + syncLimit
res = GET url
WAIT 0.33
IF res.data THEN
items = res.data
itemCount = UBOUND(items)
IF itemCount > 0 THEN
MERGE "maria.ContasAReceber" WITH items BY "Id"
totalReceber = totalReceber + itemCount
page = page + 1
IF itemCount < syncLimit THEN
page = 0
END IF
ELSE
page = 0
END IF
ELSE
page = 0
END IF
res = null
items = null
LOOP
SEND EMAIL admin, "Contas a Receber sincronizadas: " + totalReceber + " registros."
REM ============================================
REM Sync Contas a Pagar (Accounts Payable)
REM ============================================
SEND EMAIL admin, "Sincronizando Contas a Pagar..."
page = 1
totalPagar = 0
DO WHILE page > 0 AND page <= pages
url = host + "/contas/pagar?" + pageVariable + "=" + page + "&" + limitVariable + "=" + syncLimit
res = GET url
WAIT 0.33
IF res.data THEN
items = res.data
itemCount = UBOUND(items)
IF itemCount > 0 THEN
MERGE "maria.ContasAPagar" WITH items BY "Id"
totalPagar = totalPagar + itemCount
page = page + 1
IF itemCount < syncLimit THEN
page = 0
END IF
ELSE
page = 0
END IF
ELSE
page = 0
END IF
res = null
items = null
LOOP
SEND EMAIL admin, "Contas a Pagar sincronizadas: " + totalPagar + " registros."
REM ============================================
REM Summary
REM ============================================
SEND EMAIL admin, "Transferência do ERP (Contas) para BlingBot concluído. Total: " + (totalReceber + totalPagar) + " registros."

View file

@ -0,0 +1,333 @@
REM Geral
REM SET SCHEDULE "0 30 22 * * *"
daysToSync = -7
ontem = DATEADD today, "days", daysToSync
ontem = FORMAT ontem, "yyyy-MM-dd"
tomorrow = DATEADD today, "days", 1
tomorrow = FORMAT tomorrow, "yyyy-MM-dd"
dateFilter = "&dataAlteracaoInicial=${ontem}&dataAlteracaoFinal=${tomorrow}"
admin = admin1
SEND EMAIL admin, "Sincronismo: ${ontem} e ${tomorrow} (${daysToSync * -1} dia(s)) iniciado..."
REM Produtos
i = 1
SEND EMAIL admin, "Sincronizando Produtos..."
DO WHILE i > 0 AND i < pages
REM ${dateFilter}
res = GET host + "/produtos?pagina=${i}&criterio=5&tipo=P&limite=${limit}${dateFilter}"
WAIT 0.33
list = res.data
res = null
REM Sincroniza itens de Produto
prd1 = ""
j = 0
k = 0
items = NEW ARRAY
DO WHILE j < ubound(list)
produto_id = list[j].id
res = GET host + "/produtos/${produto_id}"
WAIT 0.33
produto = res.data
res = null
IF produto.codigo && produto.codigo.trim().length THEN
prd1 = prd1 + "&idsProdutos%5B%5D=" + list[j].id
items[k] = produto
produto.sku = items[k].codigo
IF produto.variacoes.length > 0 THEN
produto.hierarquia = "p"
ELSE
produto.hierarquia = "s"
END IF
produtoDB = FIND "maria.Produtos", "sku=" + produto.codigo
IF produtoDB THEN
IF produtoDB.preco <> produto.preco THEN
hist = NEW OBJECT
hist.sku = produto.sku
hist.precoAntigo = produtoDB.preco
hist.precoAtual = produto.preco
hist.produto_id = produto.id
hist.dataModificado = FORMAT today, "yyyy-MM-dd"
SAVE "maria.HistoricoPreco", hist
hist = null
END IF
END IF
k = k + 1
END IF
j = j + 1
LOOP
list = null
list = items
MERGE "maria.Produtos" WITH list BY "Id"
list = items
REM Calcula ids de produtos
j = 0
DO WHILE j < ubound(list)
REM Varre todas as variações.
listV = list[j].variacoes
IF listV THEN
k = 0
prd2 = ""
DO WHILE k < ubound(listV)
IF listV[k].codigo && listV[k].codigo.trim().length THEN
listV[k].skuPai = list[j].sku
listV[k].sku = listV[k].codigo
listV[k].hierarquia = "f"
k = k + 1
ELSE
listV.splice(k, 1)
END IF
LOOP
k = 0
DO WHILE k < ubound(listV)
listV[k].hierarquia = 'f'
DELETE "maria.ProdutoImagem", "sku=" + listV[k].sku
REM Sincroniza images da variação.
images = listV[k]?.midia?.imagens?.externas
l = 0
DO WHILE l < ubound(images)
images[l].ordinal = k
images[l].sku = listV[k].sku
images[l].id= random()
l = l + 1
LOOP
SAVE "maria.ProdutoImagem", images
images=null
k = k + 1
LOOP
MERGE "maria.Produtos" WITH listV BY "Id"
END IF
listV=null
REM Sincroniza images do produto raiz.
DELETE "maria.ProdutoImagem", "sku=" + list[j].sku
k = 0
images = list[j].midia?.imagens?.externas
DO WHILE k < ubound(images)
images[k].ordinal = k
images[k].sku = list[j].sku
images[k].id= random()
k = k + 1
LOOP
SAVE "maria.ProdutoImagem", images
j = j + 1
LOOP
i = i + 1
IF list?.length < limit THEN
i = 0
END IF
list=null
res=null
items=null
LOOP
SEND EMAIL admin, "Produtos concluído."
RESET REPORT
REM Pedidos
SEND EMAIL admin, "Sincronizando Pedidos..."
i = 1
DO WHILE i > 0 AND i < pages
res = GET host + "/pedidos/vendas?pagina=${i}&limite=${limit}${dateFilter}"
list = res.data
res = null
REM Sincroniza itens
j = 0
fullList = []
DO WHILE j < ubound(list)
pedido_id = list[j].id
res = GET host + "/pedidos/vendas/${pedido_id}"
items = res.data.itens
REM Insere ref. de pedido no item.
k = 0
DO WHILE k < ubound(items)
items[k].pedido_id = pedido_id
items[k].sku = items[k].codigo
items[k].numero = list[j].numero
REM Obter custo do produto fornecedor do fornecedor marcado como default.
items[k].custo = items[k].valor / 2
k = k + 1
LOOP
MERGE "maria.PedidosItem" WITH items BY "Id"
items = res.data.parcelas
k = 0
DO WHILE k < ubound(items)
items[k].pedido_id = pedido_id
k = k + 1
LOOP
MERGE "maria.Parcela" WITH items BY "Id"
fullList[j] = res.data
res = null
j = j + 1
LOOP
MERGE "maria.Pedidos" WITH fullList BY "Id"
i = i + 1
IF list?.length < limit THEN
i = 0
END IF
list=null
res=null
LOOP
SEND EMAIL admin, "Pedidos concluído."
REM Comuns
pageVariable="pagina"
limitVariable="limite"
syncLimit = 100
REM Sincroniza CategoriaReceita
SEND EMAIL admin, "Sincronizando CategoriaReceita..."
syncPage = 1
totalCategoria = 0
DO WHILE syncPage > 0 AND syncPage <= pages
syncUrl = host + "/categorias/receitas-despesas?" + pageVariable + "=" + syncPage + "&" + limitVariable + "=" + syncLimit
syncRes = GET syncUrl
WAIT 0.33
IF syncRes.data THEN
syncItems = syncRes.data
syncCount = UBOUND(syncItems)
IF syncCount > 0 THEN
MERGE "maria.CategoriaReceita" WITH syncItems BY "Id"
totalCategoria = totalCategoria + syncCount
syncPage = syncPage + 1
IF syncCount < syncLimit THEN
syncPage = 0
END IF
ELSE
syncPage = 0
END IF
ELSE
syncPage = 0
END IF
syncRes = null
syncItems = null
LOOP
SEND EMAIL admin, "CategoriaReceita sincronizada: " + totalCategoria + " registros."
REM Sincroniza Formas de Pagamento
SEND EMAIL admin, "Sincronizando Formas de Pagamento..."
syncPage = 1
totalForma = 0
DO WHILE syncPage > 0 AND syncPage <= pages
syncUrl = host + "/formas-pagamentos?" + pageVariable + "=" + syncPage + "&" + limitVariable + "=" + syncLimit
syncRes = GET syncUrl
WAIT 0.33
IF syncRes.data THEN
syncItems = syncRes.data
syncCount = UBOUND(syncItems)
IF syncCount > 0 THEN
MERGE "maria.FormaDePagamento" WITH syncItems BY "Id"
totalForma = totalForma + syncCount
syncPage = syncPage + 1
IF syncCount < syncLimit THEN
syncPage = 0
END IF
ELSE
syncPage = 0
END IF
ELSE
syncPage = 0
END IF
syncRes = null
syncItems = null
LOOP
SEND EMAIL admin, "Formas de Pagamento sincronizadas: " + totalForma + " registros."
REM Contatos
SEND EMAIL admin, "Sincronizando Contatos..."
i = 1
DO WHILE i > 0 AND i < pages
res = GET host + "/contatos?pagina=${i}&limite=${limit}${dateFilter} "
list = res.data
REM Sincroniza itens
j = 0
items = NEW ARRAY
DO WHILE j < ubound(list)
contato_id = list[j].id
res = GET host + "/contatos/${contato_id}"
items[j] = res.data
WAIT 0.33
j = j + 1
LOOP
MERGE "maria.Contatos" WITH items BY "Id"
i = i + 1
IF list?.length < limit THEN
i = 0
END IF
list=null
res=null
LOOP
SEND EMAIL admin, "Contatos concluído."
REM Vendedores
REM Sincroniza Vendedores.
SEND EMAIL admin, "Sincronizando Vendedores..."
i = 1
DO WHILE i > 0 AND i < pages
res = GET host + "/vendedores?pagina=${i}&situacaoContato=T&limite=${limit}${dateFilter}"
list = res.data
REM Sincroniza itens
j = 0
items = NEW ARRAY
DO WHILE j < ubound(list)
vendedor_id = list[j].id
res = GET host + "/vendedores/${vendedor_id}"
items[j] = res.data
WAIT 0.33
j = j + 1
LOOP
MERGE "maria.Vendedores" WITH items BY "Id"
i = i + 1
IF list?.length < limit THEN
i = 0
END IF
list=null
res=null
LOOP
SEND EMAIL admin, "Vendedores concluído."
SEND EMAIL admin, "Transferência do ERP para BlingBot concluído."

View file

@ -0,0 +1,66 @@
REM SET SCHEDULE "0 30 23 * * *"
i = 1
SEND EMAIL admin, "Sincronismo Estoque iniciado..."
fullList = FIND "maria.Produtos"
REM Initialize chunk parameters
chunkSize = 100
startIndex = 0
REM ubound(fullList)
DO WHILE startIndex < ubound(fullList)
list = mid( fullList, startIndex, chunkSize)
prd1 = ""
j = 0
items = NEW ARRAY
DO WHILE j < ubound(list)
produto_id = list[j].id
prd1 = prd1 + "&idsProdutos%5B%5D=" + produto_id
j = j +1
LOOP
list = null
REM Sincroniza Estoque
IF j > 0 THEN
res = GET host + "/estoques/saldos?${prd1}"
WAIT 0.33
items = res.data
res = null
k = 0
DO WHILE k < ubound(items)
depositos = items[k].depositos
pSku = FIND "maria.Produtos", "id=${items[k].produto.id}"
IF pSku THEN
prdSku = pSku.sku
DELETE "maria.Depositos", "Sku=" + prdSku
l = 0
DO WHILE l < ubound(depositos)
depositos[l].sku = prdSku
l = l + 1
LOOP
SAVE "maria.Depositos", depositos
depositos = null
END IF
pSku = null
k = k +1
LOOP
items = null
END IF
REM Update startIndex for the next chunk
startIndex = startIndex + chunkSize
items = null
LOOP
fullList = null
SEND EMAIL admin, "Estoque concluído."

View file

@ -0,0 +1,73 @@
REM Geral
REM Produto Fornecedor
FUNCTION SyncProdutoFornecedor(idProduto)
REM Sincroniza ProdutoFornecedor.
DELETE "maria.ProdutoFornecedor", "Produto_id=" + idProduto
i1 = 1
DO WHILE i1 > 0 AND i1 < pages
res = GET host + "/produtos/fornecedores?pagina=${i1}&limite=${limit}&idProduto=${idProduto}"
list1 = res.data
res = null
WAIT 0.33
REM Sincroniza itens
let j1 = 0
items1 = NEW ARRAY
DO WHILE j1 < ubound(list1)
produtoFornecedor_id = list1[j1].id
res = GET host + "/produtos/fornecedores/${produtoFornecedor_id}"
items1[j1] = res.data
res = null
WAIT 0.33
j1 = j1 + 1
LOOP
SAVE "maria.ProdutoFornecedor", items1
items1= null
i1 = i1 + 1
IF list1?.length < limit THEN
i1 = 0
END IF
res=null
list1=null
LOOP
END FUNCTION
i = 1
SEND EMAIL admin, "Sincronismo Fornecedores iniciado..."
fullList = FIND "maria.Produtos"
REM Initialize chunk parameters
chunkSize = 100
startIndex = 0
REM ubound(fullList)
DO WHILE startIndex < ubound(fullList)
list = mid( fullList, startIndex, chunkSize)
REM Sincroniza itens de Produto
prd1 = ""
j = 0
items = NEW ARRAY
DO WHILE j < ubound(list)
produto_id = list[j].id
prd1 = prd1 + "&idsProdutos%5B%5D=" + produto_id
CALL SyncProdutoFornecedor(produto_id)
j = j +1
LOOP
list = null
REM Update startIndex for the next chunk
startIndex = startIndex + chunkSize
items = null
LOOP
fullList = null
SEND EMAIL admin, "Fornecedores concluído."

View file

@ -0,0 +1,284 @@
TABLE Contatos ON maria
Id number key
Nome string(150)
Codigo string(50)
Situacao string(5)
NumeroDocumento string(25)
Telefone string(20)
Celular string(20)
Fantasia string(150)
Tipo string(5)
IndicadorIe string(5)
Ie string(22)
Rg string(22)
OrgaoEmissor string(22)
Email string(50)
Endereco_geral_endereco string(100)
Endereco_geral_cep string(10)
Endereco_geral_bairro string(50)
Endereco_geral_municipio string(50)
Endereco_geral_uf string(5)
Endereco_geral_numero string(15)
Endereco_geral_complemento string(50)
Cobranca_endereco string(100)
Cobranca_cep string(10)
Cobranca_bairro string(50)
Cobranca_municipio string(50)
Cobranca_uf string(5)
Cobranca_numero string(15)
Cobranca_complemento string(50)
Vendedor_id number
DadosAdicionais_dataNascimento date
DadosAdicionais_sexo string(5)
DadosAdicionais_naturalidade string(25)
Financeiro_limiteCredito double
Financeiro_condicaoPagamento string(20)
Financeiro_categoria_id number
Pais_nome string(100)
END TABLE
TABLE Pedidos ON maria
Id number key
Numero integer
NumeroLoja string(15)
Data date
DataSaida date
DataPrevista date
TotalProdutos double
Desconto_valor double
Desconto_unidade string(15)
Contato_id number
Total double
Contato_nome string(150)
Contato_tipoPessoa string(1)
Contato_numeroDocumento string(20)
Situacao_id integer
Situacao_valor double
Loja_id integer
Vendedor_id number
NotaFiscal_id number
END TABLE
TABLE PedidosItem ON maria
Id number key
Numero integer
Sku string(20)
Unidade string(8)
Quantidade integer
Desconto double
Valor double
Custo double
AliquotaIPI double
Descricao string(255)
DescricaoDetalhada string(250)
Produto_id number
Pedido_id number
END TABLE
TABLE ProdutoImagem ON maria
Id number key
Ordinal number
Sku string(20)
Link string(250)
END TABLE
TABLE Produtos ON maria
Id number key
Nome string(150)
Sku string(20)
SkuPai string(20)
Preco double
Tipo string(1)
Situacao string(1)
Formato string(1)
Hierarquia string(1)
DescricaoCurta string(4000)
DataValidade date
Unidade string(5)
PesoLiquido double
PesoBruto double
Volumes integer
ItensPorCaixa integer
Gtin string(50)
GtinEmbalagem string(50)
TipoProducao string(5)
Condicao integer
FreteGratis boolean
Marca string(100)
DescricaoComplementar string(4000)
LinkExterno string(255)
Observacoes string(255)
Categoria_id integer
Estoque_minimo integer
Estoque_maximo integer
Estoque_crossdocking integer
Estoque_localizacao string(50)
ActionEstoque string(50)
Dimensoes_largura double
Dimensoes_altura double
Dimensoes_profundidade double
Dimensoes_unidadeMedida double
Tributacao_origem integer
Tributacao_nFCI string(50)
Tributacao_ncm string(50)
Tributacao_cest string(50)
Tributacao_codigoListaServicos string(50)
Tributacao_spedTipoItem string(50)
Tributacao_codigoItem string(50)
Tributacao_percentualTributos double
Tributacao_valorBaseStRetencao double
Tributacao_valorStRetencao double
Tributacao_valorICMSSubstituto double
Tributacao_codigoExcecaoTipi string(50)
Tributacao_classeEnquadramentoIpi string(50)
Tributacao_valorIpiFixo double
Tributacao_codigoSeloIpi string(50)
Tributacao_valorPisFixo double
Tributacao_valorCofinsFixo double
Tributacao_dadosAdicionais string(50)
GrupoProduto_id number
Midia_video_url string(255)
Midia_imagens_externas_0_link string(255)
LinhaProduto_id number
Estrutura_tipoEstoque string(5)
Estrutura_lancamentoEstoque string(5)
Estrutura_componentes_0_produto_id number
Estrutura_componentes_0_produto_Quantidade double
END TABLE
TABLE Depositos ON maria
Internal_Id number key
Id number
Sku string(20)
SaldoFisico double
SaldoVirtual double
END TABLE
TABLE Vendedores ON maria
Id number key
DescontoLimite double
Loja_Id number
Contato_Id number
Contato_Nome string(100)
Contato_Situacao string(1)
END TABLE
TABLE ProdutoFornecedor ON maria
Id number key
Descricao string(255)
Codigo string(50)
PrecoCusto double
PrecoCompra double
Padrao boolean
Produto_id number
Fornecedor_id number
Garantia integer
END TABLE
TABLE ContasAPagar ON maria
Id number key
Situacao integer
Vencimento date
Valor double
Contato_id number
FormaPagamento_id number
Saldo double
DataEmissao date
VencimentoOriginal date
NumeroDocumento string(50)
Competencia date
Historico string(255)
NumeroBanco string(10)
Portador_id number
Categoria_id number
Borderos string(255)
Ocorrencia_tipo integer
END TABLE
TABLE ContasAReceber ON maria
Id number key
Situacao integer
Vencimento date
Valor double
IdTransacao string(50)
LinkQRCodePix string(255)
LinkBoleto string(255)
DataEmissao date
Contato_id number
Contato_nome string(150)
Contato_numeroDocumento string(20)
Contato_tipo string(1)
FormaPagamento_id number
FormaPagamento_codigoFiscal integer
ContaContabil_id number
ContaContabil_descricao string(255)
Origem_id number
Origem_tipoOrigem string(20)
Origem_numero string(20)
Origem_dataEmissao date
Origem_valor double
Origem_situacao integer
Origem_url string(255)
Saldo double
VencimentoOriginal date
NumeroDocumento string(50)
Competencia date
Historico string(255)
NumeroBanco string(10)
Portador_id number
Categoria_id number
Vendedor_id number
Borderos string(255)
Ocorrencia_tipo integer
END TABLE
TABLE CategoriaReceita ON maria
Id number key
IdCategoriaPai number
Descricao string(255)
Tipo integer
Situacao integer
END TABLE
TABLE FormaDePagamento ON maria
Id number key
Descricao string(255)
TipoPagamento integer
Situacao integer
Fixa boolean
Padrao integer
Finalidade integer
Condicao string(10)
Destino integer
Taxas_aliquota double
Taxas_valor double
Taxas_prazo integer
DadosCartao_bandeira integer
DadosCartao_tipo integer
DadosCartao_cnpjCredenciadora string(16)
END TABLE
TABLE NaturezaDeOperacao ON maria
Id number key
Situacao integer
Padrao integer
Descricao string(255)
END TABLE
TABLE Parcela ON maria
Id number key
Pedido_id number
DataVencimento date
Valor double
Observacoes string(255)
FormaPagamento_id number
END TABLE
TABLE HistoricoPreco ON maria
Id number key
Sku string(50)
PrecoAntigo double
PrecoAtual double
Produto_id number
DataModificado date
END TABLE

View file

@ -0,0 +1,25 @@
name,value
,
bot-name,Bling ERP Integration
bot-description,Synchronizes data with Bling ERP system
,
param-blingClientID,
param-blingClientSecret,
param-blingHost,https://api.bling.com.br/Api/v3
param-blingTenant,
,
param-host,https://api.bling.com.br/Api/v3
param-limit,100
param-pages,50
,
param-admin1,admin@yourdomain.com
param-admin2,admin2@yourdomain.com
,
llm-model,gpt-4o-mini
llm-temperature,0.3
,
sync-interval,21600
sync-products,true
sync-orders,true
sync-contacts,true
sync-inventory,true
1 name value
2
3 bot-name Bling ERP Integration
4 bot-description Synchronizes data with Bling ERP system
5
6 param-blingClientID
7 param-blingClientSecret
8 param-blingHost https://api.bling.com.br/Api/v3
9 param-blingTenant
10
11 param-host https://api.bling.com.br/Api/v3
12 param-limit 100
13 param-pages 50
14
15 param-admin1 admin@yourdomain.com
16 param-admin2 admin2@yourdomain.com
17
18 llm-model gpt-4o-mini
19 llm-temperature 0.3
20
21 sync-interval 21600
22 sync-products true
23 sync-orders true
24 sync-contacts true
25 sync-inventory true

View file

@ -1,159 +0,0 @@
{% extends "base.html" %}
{% block title %}Chat - General Bots{% endblock %}
{% block content %}
<div class="chat-layout">
<!-- Session Sidebar -->
<aside class="chat-sidebar" id="chat-sidebar">
<div class="sidebar-header">
<h3>Sessions</h3>
<button class="btn-new-session"
hx-post="/api/chat/sessions/new"
hx-target="#sessions-list"
hx-swap="afterbegin">
+ New Chat
</button>
</div>
<div id="sessions-list" class="sessions-list"
hx-get="/api/chat/sessions"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Sessions loaded here -->
</div>
</aside>
<!-- Chat Main -->
<div class="chat-main">
<!-- Connection Status -->
<div id="connection-status" class="connection-status"
hx-sse="connect:/api/chat/status swap:innerHTML">
<span class="status-dot"></span>
<span class="status-text">Connecting...</span>
</div>
<!-- Messages Container -->
<div id="messages" class="messages"
hx-get="/api/chat/messages?session_id={{ session_id }}"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Messages loaded here -->
</div>
<!-- Typing Indicator -->
<div id="typing-indicator" class="typing-indicator hidden">
<div class="typing-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
<!-- Suggestions -->
<div id="suggestions" class="suggestions"
hx-get="/api/chat/suggestions"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Suggestions loaded here -->
</div>
<!-- Input Form -->
<form class="chat-input-container"
hx-post="/api/chat/send"
hx-target="#messages"
hx-swap="beforeend"
hx-ext="json-enc"
hx-on::before-request="document.getElementById('typing-indicator').classList.remove('hidden')"
hx-on::after-request="this.reset(); document.getElementById('typing-indicator').classList.add('hidden'); document.getElementById('message-input').focus()">
<input type="hidden" name="session_id" value="{{ session_id }}">
<div class="input-group">
<textarea
id="message-input"
name="content"
class="message-input"
placeholder="Type your message..."
rows="1"
required
autofocus
hx-trigger="keydown[key=='Enter' && !shiftKey] from:body"
hx-post="/api/chat/send"
hx-target="#messages"
hx-swap="beforeend"></textarea>
<!-- Voice Button -->
<button type="button"
class="btn-voice"
hx-post="/api/voice/toggle"
hx-swap="none"
title="Voice input">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<line x1="12" y1="19" x2="12" y2="23"></line>
</svg>
</button>
<!-- Send Button -->
<button type="submit" class="btn-send" title="Send">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</button>
</div>
<!-- Context Selector -->
<div class="context-selector"
hx-get="/api/chat/contexts"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Contexts loaded here -->
</div>
</form>
</div>
<!-- Scroll to Bottom -->
<button id="scroll-to-bottom" class="scroll-to-bottom hidden"
onclick="document.getElementById('messages').scrollTo(0, document.getElementById('messages').scrollHeight)">
</button>
</div>
{% endblock %}
{% block scripts %}
<script>
// Auto-resize textarea
const textarea = document.getElementById('message-input');
textarea.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
});
// Monitor scroll position
const messages = document.getElementById('messages');
const scrollBtn = document.getElementById('scroll-to-bottom');
messages.addEventListener('scroll', function() {
const isAtBottom = this.scrollHeight - this.scrollTop <= this.clientHeight + 50;
scrollBtn.classList.toggle('hidden', isAtBottom);
});
// Auto-scroll on new messages
messages.addEventListener('htmx:afterSettle', function(event) {
if (event.detail.target === this) {
this.scrollTo(0, this.scrollHeight);
}
});
// Handle suggestion clicks
document.addEventListener('click', function(e) {
if (e.target.classList.contains('suggestion-chip')) {
textarea.value = e.target.textContent;
textarea.form.dispatchEvent(new Event('submit'));
}
});
</script>
{% endblock %}

View file

@ -0,0 +1,174 @@
# HIPAA Medical Privacy Template
A HIPAA-compliant healthcare privacy portal template for General Bots.
## Overview
This template provides healthcare organizations with a ready-to-deploy patient privacy rights management system that complies with:
- **HIPAA** (Health Insurance Portability and Accountability Act)
- **HITECH Act** (Health Information Technology for Economic and Clinical Health)
- State-specific healthcare privacy regulations
## Features
### Patient Rights Management
1. **Access Medical Records** - Patients can request copies of their Protected Health Information (PHI)
2. **Request Amendments** - Patients can request corrections to their medical records
3. **Accounting of Disclosures** - Track and report who has accessed patient PHI
4. **Request Restrictions** - Allow patients to limit how their PHI is used or shared
5. **Confidential Communications** - Patients can specify preferred contact methods
6. **File Privacy Complaints** - Streamlined complaint submission process
7. **Revoke Authorizations** - Withdraw previous consent for PHI disclosure
### HIPAA Compliance Features
- **Audit Trail** - Complete logging of all PHI access and requests
- **Encryption** - AES-256 at rest, TLS 1.3 in transit
- **Access Controls** - Role-based access control (RBAC)
- **Break Glass** - Emergency access procedures with audit
- **Minimum Necessary** - Automatic enforcement of minimum necessary standard
- **PHI Detection** - Automatic detection and redaction of PHI in communications
- **Breach Notification** - Built-in breach response workflow
## Installation
1. Copy this template to your General Bots instance:
```bash
cp -r templates/hipaa-medical.gbai /path/to/your/bot/
```
2. Configure the bot settings in `hipaa.gbot/config.csv`:
```csv
Covered Entity Name,Your Healthcare Organization
Privacy Officer Email,privacy@yourhealthcare.org
HIPAA Security Officer,security@yourhealthcare.org
```
3. Deploy the template:
```bash
botserver --deploy hipaa-medical.gbai
```
## Configuration
### Required Settings
| Setting | Description | Example |
|---------|-------------|---------|
| `Covered Entity Name` | Your organization's legal name | Memorial Hospital |
| `Privacy Officer Email` | HIPAA Privacy Officer contact | privacy@hospital.org |
| `HIPAA Security Officer` | Security Officer contact | security@hospital.org |
| `Covered Entity NPI` | National Provider Identifier | 1234567890 |
### Security Settings
| Setting | Default | Description |
|---------|---------|-------------|
| `Require 2FA` | true | Two-factor authentication required |
| `Session Timeout` | 300 | Session timeout in seconds (5 minutes) |
| `Encryption At Rest` | AES-256 | Data encryption standard |
| `PHI Auto Redaction` | true | Automatically redact PHI in logs |
### Compliance Timelines
| Requirement | Deadline | Setting |
|-------------|----------|---------|
| Access Requests | 30 days | `Response SLA Hours` |
| Urgent Requests | 48 hours | `Urgent Response Hours` |
| Breach Notification | 60 hours | `Breach Notification Hours` |
## Dialogs
### Main Entry Point
- `start.bas` - Main menu for patient privacy rights
### Patient Rights Dialogs
- `access-phi.bas` - Request medical records
- `request-amendment.bas` - Request record corrections
- `accounting-disclosures.bas` - View access history
- `request-restrictions.bas` - Limit PHI use/sharing
- `confidential-communications.bas` - Set contact preferences
- `file-complaint.bas` - Submit privacy complaints
- `revoke-authorization.bas` - Withdraw consent
## Integration
### Patient Portal Integration
Connect to your existing patient portal:
```basic
' In your custom dialog
patient = GET PATIENT FROM "portal" WHERE mrn = patient_mrn
IF patient.verified THEN
CALL "access-phi.bas"
END IF
```
### EHR Integration
The template can integrate with common EHR systems:
- Epic
- Cerner
- Meditech
- Allscripts
Configure your EHR connection in the bot settings or use the FHIR API for standard integration.
## Audit Requirements
All interactions are logged to the `hipaa_audit_log` table with:
- Timestamp
- Session ID
- Action performed
- User/patient identifier
- IP address
- User agent
- Outcome
Retain audit logs for a minimum of 6 years (2,190 days) per HIPAA requirements.
## Customization
### Adding Custom Dialogs
Create new `.bas` files in the `hipaa.gbdialog` folder:
```basic
' custom-workflow.bas
TALK "Starting custom HIPAA workflow..."
' Your custom logic here
```
### Branding
Customize the welcome message and organization details in `config.csv`.
## Support
For questions about this template:
- **Documentation**: See General Bots docs
- **Issues**: GitHub Issues
- **HIPAA Guidance**: consult your compliance officer
## Disclaimer
This template is provided as a compliance aid and does not constitute legal advice. Healthcare organizations are responsible for ensuring their HIPAA compliance program meets all regulatory requirements. Consult with healthcare compliance professionals and legal counsel.
## License
AGPL-3.0 - See LICENSE file in the main repository.
---
Built with ❤️ by Pragmatismo

View file

@ -0,0 +1,88 @@
' =============================================================================
' HIPAA Medical Privacy Portal - Main Dialog
' General Bots Template for Healthcare Data Protection
' =============================================================================
' This template helps healthcare organizations comply with:
' - HIPAA (Health Insurance Portability and Accountability Act)
' - HITECH Act (Health Information Technology for Economic and Clinical Health)
' - State-specific healthcare privacy regulations
' =============================================================================
TALK "🏥 Welcome to the HIPAA Privacy Portal"
TALK "I can help you manage your Protected Health Information (PHI) rights."
TALK ""
TALK "Under HIPAA, you have the following rights:"
TALK ""
TALK "1⃣ **Access Your Medical Records** - Request copies of your health information"
TALK "2⃣ **Request Amendments** - Correct errors in your medical records"
TALK "3⃣ **Accounting of Disclosures** - See who has accessed your PHI"
TALK "4⃣ **Request Restrictions** - Limit how we use or share your information"
TALK "5⃣ **Confidential Communications** - Choose how we contact you"
TALK "6⃣ **File a Privacy Complaint** - Report a privacy concern"
TALK "7⃣ **Revoke Authorization** - Withdraw previous consent for PHI disclosure"
HEAR choice AS "What would you like to do? (1-7 or describe your request)"
SELECT CASE choice
CASE "1", "access", "records", "medical records", "view", "copy"
CALL "access-phi.bas"
CASE "2", "amend", "amendment", "correct", "correction", "fix", "error"
CALL "request-amendment.bas"
CASE "3", "accounting", "disclosures", "who accessed", "access log"
CALL "accounting-disclosures.bas"
CASE "4", "restrict", "restriction", "limit", "limitations"
CALL "request-restrictions.bas"
CASE "5", "communications", "contact", "how to contact", "confidential"
CALL "confidential-communications.bas"
CASE "6", "complaint", "report", "privacy concern", "violation"
CALL "file-complaint.bas"
CASE "7", "revoke", "withdraw", "cancel authorization"
CALL "revoke-authorization.bas"
CASE ELSE
' Use LLM to understand medical privacy requests
SET CONTEXT "You are a HIPAA compliance assistant. Classify the user's request into one of these categories: access_records, amendment, disclosures, restrictions, communications, complaint, revoke. Only respond with the category name."
intent = LLM "Classify this healthcare privacy request: " + choice
SELECT CASE intent
CASE "access_records"
CALL "access-phi.bas"
CASE "amendment"
CALL "request-amendment.bas"
CASE "disclosures"
CALL "accounting-disclosures.bas"
CASE "restrictions"
CALL "request-restrictions.bas"
CASE "communications"
CALL "confidential-communications.bas"
CASE "complaint"
CALL "file-complaint.bas"
CASE "revoke"
CALL "revoke-authorization.bas"
CASE ELSE
TALK "I'm not sure I understood your request."
TALK "Please select a number from 1-7 or contact our Privacy Officer directly."
TALK ""
TALK "📞 Privacy Officer: privacy@healthcare.org"
TALK "📧 Email: hipaa-requests@healthcare.org"
CALL "start.bas"
END SELECT
END SELECT
' Log all interactions for HIPAA audit trail
INSERT INTO "hipaa_audit_log" VALUES {
"timestamp": NOW(),
"session_id": GET SESSION "id",
"action": "privacy_portal_access",
"choice": choice,
"ip_address": GET SESSION "client_ip",
"user_agent": GET SESSION "user_agent"
}

View file

@ -0,0 +1,63 @@
name,value
Bot Name,HIPAA Medical Privacy Center
Bot Description,Healthcare Data Protection and Patient Rights Management Bot
Bot Version,1.0.0
Bot Author,Pragmatismo
Bot License,AGPL-3.0
Bot Category,Healthcare Compliance
Bot Tags,hipaa;healthcare;phi;medical;compliance;privacy;patient-rights
Default Language,en
Supported Languages,en;es;pt
Welcome Message,Welcome to the Healthcare Privacy Center. I can help you with your patient rights under HIPAA, including accessing your medical records, requesting amendments, and managing your health information privacy.
Error Message,I apologize, but I encountered an issue processing your request. For urgent matters, please contact our Privacy Officer directly at privacy@healthcare.org
Timeout Message,Your session has timed out for security. Please start a new conversation. This is required to protect your health information.
Session Timeout,300
Max Retries,3
Log Level,info
Enable Audit Log,true
Audit Log Retention Days,2190
Require Authentication,true
Require Email Verification,true
Require 2FA,true
Data Retention Days,2190
Auto Delete Completed Requests,false
Send Confirmation Emails,true
Privacy Officer Email,privacy@healthcare.org
HIPAA Privacy Officer,hipaa-officer@healthcare.org
HIPAA Security Officer,security-officer@healthcare.org
Covered Entity Name,Your Healthcare Organization
Covered Entity Address,123 Medical Center Drive
Covered Entity NPI,1234567890
Compliance Frameworks,HIPAA;HITECH;State-Privacy-Laws
Response SLA Hours,30
Urgent Response Hours,48
Escalation Email,compliance@healthcare.org
Enable HIPAA Mode,true
PHI Processing Enabled,true
PHI Detection Enabled,true
PHI Auto Redaction,true
Minimum Necessary Standard,true
Encryption At Rest,AES-256
Encryption In Transit,TLS-1.3
Access Control Model,RBAC
Break Glass Enabled,true
Break Glass Audit Required,true
Consent Required,true
Authorization Form Version,2.0
NPP Version,3.0
NPP Last Updated,2025-01-01
Designated Record Set,true
Accounting of Disclosures,true
Restriction Requests Enabled,true
Confidential Communications,true
Patient Portal URL,https://portal.healthcare.org
HIE Participation,true
E-Prescribing Enabled,true
Telehealth Enabled,true
BAA Required,true
Workforce Training Required,true
Training Frequency Days,365
Incident Response Plan,true
Breach Notification Hours,60
OCR Complaint URL,https://www.hhs.gov/hipaa/filing-a-complaint
State AG Contact,state-ag@state.gov
Can't render this file because it has a wrong number of fields in line 11.

View file

@ -0,0 +1,200 @@
# Privacy Rights Center Template (privacy.gbai)
A comprehensive LGPD/GDPR compliance template for General Bots that enables organizations to handle data subject rights requests automatically.
## Overview
This template provides a complete privacy portal that helps organizations comply with:
- **LGPD** (Lei Geral de Proteção de Dados - Brazil)
- **GDPR** (General Data Protection Regulation - EU)
- **CCPA** (California Consumer Privacy Act - US)
- **Other privacy regulations** with similar data subject rights
## Features
### Data Subject Rights Implemented
| Right | LGPD Article | GDPR Article | Dialog File |
|-------|--------------|--------------|-------------|
| Access | Art. 18 | Art. 15 | `request-data.bas` |
| Rectification | Art. 18 III | Art. 16 | `rectify-data.bas` |
| Erasure (Deletion) | Art. 18 VI | Art. 17 | `delete-data.bas` |
| Data Portability | Art. 18 V | Art. 20 | `export-data.bas` |
| Consent Management | Art. 8 | Art. 7 | `manage-consents.bas` |
| Object to Processing | Art. 18 IV | Art. 21 | `object-processing.bas` |
### Key Capabilities
- **Identity Verification** - Email-based verification codes before processing requests
- **Audit Trail** - Complete logging of all privacy requests for compliance
- **Multi-format Export** - JSON, CSV, XML export options for data portability
- **Consent Tracking** - Granular consent management with history
- **Email Notifications** - Automated confirmations and reports
- **SLA Tracking** - Response time monitoring (default: 72 hours)
## Installation
1. Copy the template to your bot's packages directory:
```bash
cp -r templates/privacy.gbai /path/to/your/bot/packages/
```
2. Configure the bot settings in `privacy.gbot/config.csv`:
```csv
name,value
Company Name,Your Company Name
Privacy Officer Email,privacy@yourcompany.com
DPO Contact,dpo@yourcompany.com
```
3. Restart General Bots to load the template.
## Configuration Options
### Required Settings
| Setting | Description | Example |
|---------|-------------|---------|
| `Company Name` | Your organization name | Acme Corp |
| `Privacy Officer Email` | Contact for privacy matters | privacy@acme.com |
| `DPO Contact` | Data Protection Officer | dpo@acme.com |
### Optional Settings
| Setting | Default | Description |
|---------|---------|-------------|
| `Session Timeout` | 900 | Session timeout in seconds |
| `Response SLA Hours` | 72 | Max hours to respond to requests |
| `Data Retention Days` | 30 | Days to retain completed request data |
| `Enable HIPAA Mode` | false | Enable PHI handling features |
| `Require 2FA` | false | Require two-factor authentication |
## File Structure
```
privacy.gbai/
├── README.md # This file
├── privacy.gbdialog/
│ ├── start.bas # Main entry point
│ ├── request-data.bas # Data access requests
│ ├── delete-data.bas # Data erasure requests
│ ├── export-data.bas # Data portability
│ └── manage-consents.bas # Consent management
├── privacy.gbot/
│ └── config.csv # Bot configuration
└── privacy.gbui/
└── index.html # Web portal UI
```
## Usage Examples
### Starting the Privacy Portal
Users can access the privacy portal by saying:
- "I want to access my data"
- "Delete my information"
- "Export my data"
- "Manage my consents"
- Or selecting options 1-6 from the menu
### API Integration
The template exposes REST endpoints for integration:
```
POST /api/privacy/request - Submit a new request
GET /api/privacy/requests - List user's requests
GET /api/privacy/request/:id - Get request status
POST /api/privacy/consent - Update consents
```
### Webhook Events
The template emits webhook events for integration:
- `privacy.request.created` - New request submitted
- `privacy.request.completed` - Request fulfilled
- `privacy.consent.updated` - Consent preferences changed
- `privacy.data.deleted` - User data erased
## Customization
### Adding Custom Consent Categories
Edit `manage-consents.bas` to add new consent categories:
```basic
consent_categories = [
{
"id": "custom_category",
"name": "Custom Category Name",
"description": "Description for users",
"required": FALSE,
"legal_basis": "Consent"
}
]
```
### Branding the UI
Modify `privacy.gbui/index.html` to match your branding:
- Update CSS variables for colors
- Replace logo and company name
- Add custom legal text
### Email Templates
Customize email notifications by editing the `SEND MAIL` blocks in each dialog file.
## Compliance Notes
### Response Deadlines
| Regulation | Standard Deadline | Extended Deadline |
|------------|-------------------|-------------------|
| LGPD | 15 days | - |
| GDPR | 30 days | 90 days (complex) |
| CCPA | 45 days | 90 days |
### Data Retention
Some data may need to be retained for legal compliance:
- Financial records (tax requirements)
- Legal dispute documentation
- Fraud prevention records
- Regulatory compliance data
The template handles this by anonymizing retained records while deleting identifiable information.
### Audit Requirements
All requests are logged to `privacy_requests` and `consent_history` tables with:
- Timestamp
- User identifier
- Request type
- IP address
- Completion status
- Legal basis
## Support
For questions about this template:
- **Documentation**: https://docs.pragmatismo.com.br/privacy-template
- **Issues**: https://github.com/GeneralBots/BotServer/issues
- **Email**: support@pragmatismo.com.br
## License
This template is part of General Bots and is licensed under AGPL-3.0.
---
**Note**: This template provides technical implementation for privacy compliance. Organizations should consult with legal counsel to ensure full compliance with applicable regulations.

View file

@ -0,0 +1,213 @@
' Privacy Template - Data Deletion Request (Right to be Forgotten)
' LGPD Art. 18, GDPR Art. 17, HIPAA (where applicable)
' This dialog handles user requests to delete their personal data
TALK "🔒 **Data Deletion Request**"
TALK "I can help you exercise your right to have your personal data deleted."
TALK "This is also known as the 'Right to be Forgotten' under LGPD and GDPR."
' Authenticate the user first
TALK "For security purposes, I need to verify your identity before proceeding."
HEAR email AS EMAIL WITH "Please enter your registered email address:"
' Verify email exists in system
user = FIND "users.csv" WHERE email = email
IF user IS NULL THEN
TALK "⚠️ I couldn't find an account with that email address."
TALK "Please check the email and try again, or contact support@company.com"
EXIT
END IF
' Send verification code
verification_code = RANDOM(100000, 999999)
SET BOT MEMORY "verification_" + email, verification_code
SET BOT MEMORY "verification_expiry_" + email, NOW() + 15 * 60
SEND MAIL email, "Data Deletion Verification Code", "
Your verification code is: " + verification_code + "
This code expires in 15 minutes.
If you did not request data deletion, please ignore this email and contact support immediately.
Pragmatismo Privacy Team
"
HEAR entered_code AS INTEGER WITH "I've sent a verification code to your email. Please enter it here:"
stored_code = GET BOT MEMORY "verification_" + email
IF entered_code <> stored_code THEN
TALK "❌ Invalid verification code. Please try again."
EXIT
END IF
TALK "✅ Identity verified."
TALK ""
TALK "**What data would you like to delete?**"
TALK ""
TALK "1⃣ All my personal data (complete account deletion)"
TALK "2⃣ Conversation history only"
TALK "3⃣ Files and documents only"
TALK "4⃣ Activity logs and analytics"
TALK "5⃣ Specific data categories (I'll choose)"
TALK "6⃣ Cancel this request"
HEAR deletion_choice AS INTEGER WITH "Please enter your choice (1-6):"
SELECT CASE deletion_choice
CASE 1
deletion_type = "complete"
TALK "⚠️ **Complete Account Deletion**"
TALK "This will permanently delete:"
TALK "• Your user profile and account"
TALK "• All conversation history"
TALK "• All uploaded files and documents"
TALK "• All activity logs"
TALK "• All preferences and settings"
TALK ""
TALK "**This action cannot be undone.**"
CASE 2
deletion_type = "conversations"
TALK "This will delete all your conversation history with our bots."
CASE 3
deletion_type = "files"
TALK "This will delete all files and documents you've uploaded."
CASE 4
deletion_type = "logs"
TALK "This will delete all activity logs and analytics data associated with you."
CASE 5
deletion_type = "selective"
TALK "Please specify which data categories you want deleted:"
HEAR categories WITH "Enter categories separated by commas (e.g., 'email history, phone number, address'):"
CASE 6
TALK "Request cancelled. No data has been deleted."
EXIT
CASE ELSE
TALK "Invalid choice. Please start over."
EXIT
END SELECT
' Explain data retention exceptions
TALK ""
TALK "📋 **Legal Notice:**"
TALK "Some data may be retained for legal compliance purposes:"
TALK "• Financial records (tax requirements)"
TALK "• Legal dispute documentation"
TALK "• Fraud prevention records"
TALK "• Regulatory compliance data"
TALK ""
TALK "Retained data will be minimized and protected according to law."
HEAR reason WITH "Please briefly explain why you're requesting deletion (optional, press Enter to skip):"
HEAR confirmation WITH "Type 'DELETE MY DATA' to confirm this irreversible action:"
IF confirmation <> "DELETE MY DATA" THEN
TALK "Confirmation not received. Request cancelled for your protection."
EXIT
END IF
' Log the deletion request
request_id = "DEL-" + FORMAT(NOW(), "YYYYMMDD") + "-" + RANDOM(10000, 99999)
request_date = NOW()
' Create deletion request record
INSERT INTO "deletion_requests.csv", request_id, email, deletion_type, categories, reason, request_date, "pending"
' Process the deletion based on type
SELECT CASE deletion_type
CASE "complete"
' Delete from all tables
DELETE FROM "messages" WHERE user_email = email
DELETE FROM "files" WHERE owner_email = email
DELETE FROM "activity_logs" WHERE user_email = email
DELETE FROM "user_preferences" WHERE email = email
DELETE FROM "sessions" WHERE user_email = email
' Anonymize required retention records
UPDATE "audit_logs" SET user_email = "DELETED_USER_" + request_id WHERE user_email = email
' Mark user for deletion (actual deletion after retention period)
UPDATE "users" SET status = "pending_deletion", deletion_request_id = request_id WHERE email = email
CASE "conversations"
DELETE FROM "messages" WHERE user_email = email
DELETE FROM "sessions" WHERE user_email = email
CASE "files"
' Get file list for physical deletion
files = FIND "files" WHERE owner_email = email
FOR EACH file IN files
DELETE FILE file.path
NEXT
DELETE FROM "files" WHERE owner_email = email
CASE "logs"
DELETE FROM "activity_logs" WHERE user_email = email
' Anonymize audit logs (keep for compliance but remove PII)
UPDATE "audit_logs" SET user_email = "ANONYMIZED" WHERE user_email = email
CASE "selective"
' Process specific categories
TALK "Processing selective deletion for: " + categories
' Custom handling based on categories specified
INSERT INTO "manual_deletion_queue", request_id, email, categories, request_date
END SELECT
' Update request status
UPDATE "deletion_requests" SET status = "completed", completion_date = NOW() WHERE request_id = request_id
' Send confirmation email
SEND MAIL email, "Data Deletion Request Confirmed - " + request_id, "
Dear User,
Your data deletion request has been received and processed.
**Request Details:**
- Request ID: " + request_id + "
- Request Date: " + FORMAT(request_date, "YYYY-MM-DD HH:mm") + "
- Deletion Type: " + deletion_type + "
- Status: Completed
**What happens next:**
" + IF(deletion_type = "complete", "
- Your account will be fully deleted within 30 days
- You will receive a final confirmation email
- Some data may be retained for legal compliance (anonymized)
", "
- The specified data has been deleted from our systems
- Some backups may take up to 30 days to purge
") + "
**Your Rights:**
- You can request a copy of any retained data
- You can file a complaint with your data protection authority
- Contact us at privacy@company.com for questions
Under LGPD (Brazil) and GDPR (EU), you have the right to:
- Request confirmation of this deletion
- Lodge a complaint with supervisory authorities
- Seek judicial remedy if unsatisfied
Thank you for trusting us with your data.
Pragmatismo Privacy Team
Request Reference: " + request_id + "
"
TALK ""
TALK "✅ **Request Completed**"
TALK ""
TALK "Your deletion request has been processed."
TALK "Request ID: **" + request_id + "**"
TALK ""
TALK "A confirmation email has been sent to " + email
TALK ""
TALK "If you have questions, contact privacy@company.com"
TALK "Reference your Request ID in any communications."

View file

@ -0,0 +1,372 @@
' ============================================================================
' Privacy Template: Data Portability/Export Request
' LGPD Art. 18 V / GDPR Art. 20 - Right to Data Portability
' ============================================================================
' This dialog enables users to export their data in portable formats
' Supports JSON, CSV, and XML export for interoperability
TALK "📦 **Data Portability Request**"
TALK "You have the right to receive your personal data in a structured, commonly used, and machine-readable format."
TALK ""
' Verify user identity
TALK "First, I need to verify your identity."
HEAR email AS EMAIL WITH "Please enter your registered email address:"
user = FIND "users" WHERE email = email
IF user IS NULL THEN
TALK "❌ No account found with that email address."
TALK "Please check and try again, or contact support."
EXIT
END IF
' Send verification code
code = GENERATE CODE 6
SET SESSION "export_verification_code", code
SET SESSION "export_email", email
SEND MAIL email, "Data Export Request - Verification Code", "
Your verification code is: " + code + "
This code expires in 15 minutes.
If you did not request this data export, please ignore this email.
Pragmatismo Privacy Team
"
HEAR entered_code AS TEXT WITH "📧 Enter the verification code sent to your email:"
IF entered_code <> code THEN
TALK "❌ Invalid verification code. Please start over."
EXIT
END IF
TALK "✅ Identity verified!"
TALK ""
' Ask for export format
TALK "**Choose your export format:**"
TALK ""
TALK "1⃣ **JSON** - Best for importing into other systems"
TALK "2⃣ **CSV** - Best for spreadsheets (Excel, Google Sheets)"
TALK "3⃣ **XML** - Universal interchange format"
TALK "4⃣ **All formats** - Get all three formats in a ZIP file"
HEAR format_choice WITH "Enter your choice (1-4):"
SELECT CASE format_choice
CASE "1", "json", "JSON"
export_format = "json"
format_name = "JSON"
CASE "2", "csv", "CSV"
export_format = "csv"
format_name = "CSV"
CASE "3", "xml", "XML"
export_format = "xml"
format_name = "XML"
CASE "4", "all", "ALL"
export_format = "all"
format_name = "All Formats (ZIP)"
CASE ELSE
export_format = "json"
format_name = "JSON"
TALK "Defaulting to JSON format."
END SELECT
TALK ""
TALK "**Select data categories to export:**"
TALK ""
TALK "1⃣ Everything (complete data export)"
TALK "2⃣ Profile information only"
TALK "3⃣ Conversations and messages"
TALK "4⃣ Files and documents"
TALK "5⃣ Activity history"
TALK "6⃣ Custom selection"
HEAR data_choice WITH "Enter your choice (1-6):"
' Define what data to export based on choice
SELECT CASE data_choice
CASE "1"
include_profile = TRUE
include_conversations = TRUE
include_files = TRUE
include_activity = TRUE
include_consents = TRUE
data_scope = "complete"
CASE "2"
include_profile = TRUE
include_conversations = FALSE
include_files = FALSE
include_activity = FALSE
include_consents = TRUE
data_scope = "profile"
CASE "3"
include_profile = FALSE
include_conversations = TRUE
include_files = FALSE
include_activity = FALSE
include_consents = FALSE
data_scope = "conversations"
CASE "4"
include_profile = FALSE
include_conversations = FALSE
include_files = TRUE
include_activity = FALSE
include_consents = FALSE
data_scope = "files"
CASE "5"
include_profile = FALSE
include_conversations = FALSE
include_files = FALSE
include_activity = TRUE
include_consents = FALSE
data_scope = "activity"
CASE "6"
TALK "Select categories (yes/no for each):"
HEAR include_profile AS BOOLEAN WITH "Include profile information?"
HEAR include_conversations AS BOOLEAN WITH "Include conversations?"
HEAR include_files AS BOOLEAN WITH "Include files metadata?"
HEAR include_activity AS BOOLEAN WITH "Include activity logs?"
HEAR include_consents AS BOOLEAN WITH "Include consent records?"
data_scope = "custom"
CASE ELSE
include_profile = TRUE
include_conversations = TRUE
include_files = TRUE
include_activity = TRUE
include_consents = TRUE
data_scope = "complete"
END SELECT
TALK ""
TALK "🔄 Preparing your data export... This may take a few minutes."
TALK ""
' Gather the data
export_data = {}
request_id = "EXP-" + FORMAT(NOW(), "YYYYMMDD-HHmmss") + "-" + user.id
' Export metadata
export_data.metadata = {
"export_id": request_id,
"export_date": NOW(),
"format": format_name,
"data_scope": data_scope,
"legal_basis": "LGPD Art. 18 V / GDPR Art. 20",
"data_controller": "Your Organization Name",
"contact": "privacy@company.com"
}
' Gather profile data
IF include_profile THEN
profile = FIND "users" WHERE id = user.id
export_data.profile = {
"name": profile.name,
"email": profile.email,
"phone": profile.phone,
"address": profile.address,
"created_at": profile.created_at,
"last_login": profile.last_login,
"timezone": profile.timezone,
"language": profile.language,
"preferences": profile.preferences
}
TALK "✓ Profile data collected"
END IF
' Gather conversations
IF include_conversations THEN
messages = FIND "messages" WHERE user_id = user.id ORDER BY created_at
sessions = FIND "sessions" WHERE user_id = user.id
export_data.conversations = {
"total_sessions": COUNT(sessions),
"total_messages": COUNT(messages),
"sessions": sessions,
"messages": messages
}
TALK "✓ Conversation data collected (" + COUNT(messages) + " messages)"
END IF
' Gather files metadata
IF include_files THEN
files = FIND "user_files" WHERE user_id = user.id
file_list = []
FOR EACH file IN files
file_info = {
"filename": file.name,
"size": file.size,
"type": file.mime_type,
"uploaded_at": file.created_at,
"last_accessed": file.last_accessed,
"path": file.path
}
APPEND file_list, file_info
NEXT
export_data.files = {
"total_files": COUNT(files),
"total_size": SUM(files, "size"),
"file_list": file_list
}
TALK "✓ Files metadata collected (" + COUNT(files) + " files)"
END IF
' Gather activity logs
IF include_activity THEN
activity = FIND "activity_logs" WHERE user_id = user.id ORDER BY timestamp DESC LIMIT 10000
export_data.activity = {
"total_events": COUNT(activity),
"events": activity
}
TALK "✓ Activity logs collected (" + COUNT(activity) + " events)"
END IF
' Gather consent records
IF include_consents THEN
consents = FIND "user_consents" WHERE user_id = user.id
export_data.consents = {
"consent_records": consents,
"current_preferences": {
"marketing_emails": user.marketing_consent,
"analytics": user.analytics_consent,
"third_party_sharing": user.sharing_consent
}
}
TALK "✓ Consent records collected"
END IF
TALK ""
TALK "📁 Generating export files..."
' Generate export files based on format
timestamp = FORMAT(NOW(), "YYYYMMDD_HHmmss")
base_filename = "data_export_" + timestamp
SELECT CASE export_format
CASE "json"
filename = base_filename + ".json"
WRITE filename, JSON(export_data)
CASE "csv"
' Generate multiple CSV files for different data types
IF include_profile THEN
WRITE base_filename + "_profile.csv", CSV(export_data.profile)
END IF
IF include_conversations THEN
WRITE base_filename + "_messages.csv", CSV(export_data.conversations.messages)
END IF
IF include_files THEN
WRITE base_filename + "_files.csv", CSV(export_data.files.file_list)
END IF
IF include_activity THEN
WRITE base_filename + "_activity.csv", CSV(export_data.activity.events)
END IF
' Create ZIP of all CSVs
filename = base_filename + "_csv.zip"
COMPRESS filename, base_filename + "_*.csv"
CASE "xml"
filename = base_filename + ".xml"
WRITE filename, XML(export_data)
CASE "all"
' Generate all formats
WRITE base_filename + ".json", JSON(export_data)
WRITE base_filename + ".xml", XML(export_data)
IF include_profile THEN
WRITE base_filename + "_profile.csv", CSV(export_data.profile)
END IF
IF include_conversations THEN
WRITE base_filename + "_messages.csv", CSV(export_data.conversations.messages)
END IF
IF include_files THEN
WRITE base_filename + "_files.csv", CSV(export_data.files.file_list)
END IF
filename = base_filename + "_complete.zip"
COMPRESS filename, base_filename + ".*"
END SELECT
' Upload to secure storage
secure_path = "/secure/exports/" + user.id + "/"
UPLOAD filename TO secure_path
' Generate download link (expires in 7 days)
download_link = GENERATE SECURE LINK secure_path + filename EXPIRES 7 DAYS
' Log the export request for compliance
INSERT INTO "privacy_requests" VALUES {
"id": request_id,
"user_id": user.id,
"request_type": "data_portability",
"data_scope": data_scope,
"format": format_name,
"requested_at": NOW(),
"completed_at": NOW(),
"status": "completed",
"legal_basis": "LGPD Art. 18 V / GDPR Art. 20"
}
' Send email with download link
SEND MAIL email, "Your Data Export is Ready - " + request_id, "
Dear " + user.name + ",
Your data export request has been completed.
**Export Details:**
- Request ID: " + request_id + "
- Format: " + format_name + "
- Data Included: " + data_scope + "
- Generated: " + FORMAT(NOW(), "DD/MM/YYYY HH:mm") + "
**Download Your Data:**
" + download_link + "
This link expires in 7 days for security purposes.
**What's Included:**
" + IF(include_profile, " Profile information\n", "") + IF(include_conversations, " Conversation history\n", "") + IF(include_files, " Files metadata\n", "") + IF(include_activity, " Activity logs\n", "") + IF(include_consents, " Consent records\n", "") + "
**Your Rights Under LGPD/GDPR:**
- Import this data to another service provider
- Request data deletion after export
- Request additional data categories
- File a complaint with data protection authorities
If you have questions, contact privacy@company.com
Pragmatismo Privacy Team
"
TALK ""
TALK "✅ **Export Complete!**"
TALK ""
TALK "📧 A download link has been sent to: " + email
TALK ""
TALK "**Export Details:**"
TALK "• Request ID: " + request_id
TALK "• Format: " + format_name
TALK "• Link expires in: 7 days"
TALK ""
TALK "You can use this data to:"
TALK "• Import into another service"
TALK "• Keep a personal backup"
TALK "• Review what data we hold"
TALK ""
TALK "🔒 Need anything else?"
TALK "• Say **'delete my data'** to request deletion"
TALK "• Say **'privacy settings'** to manage consents"
TALK "• Say **'help'** for other options"

View file

@ -0,0 +1,333 @@
' ============================================================================
' Privacy Template: Consent Management
' LGPD Art. 8 / GDPR Art. 7 - Consent Management
' ============================================================================
' This dialog allows users to view, grant, and revoke their consents
' Essential for LGPD/GDPR compliance with granular consent tracking
TALK "🔐 **Consent Management Center**"
TALK "Here you can view and manage all your data processing consents."
TALK ""
' Verify user identity first
HEAR email AS EMAIL WITH "Please enter your registered email address:"
user = FIND "users" WHERE email = email
IF user IS NULL THEN
TALK "⚠️ We couldn't find an account with that email."
TALK "Please check the email address and try again."
EXIT
END IF
' Send quick verification
code = GENERATE CODE 6
SET SESSION "consent_verify_code", code
SET SESSION "consent_verify_email", email
SEND MAIL email, "Consent Management - Verification", "
Your verification code is: " + code + "
This code expires in 10 minutes.
Pragmatismo Privacy Team
"
HEAR entered_code AS TEXT WITH "📧 Enter the verification code sent to your email:"
IF entered_code <> code THEN
TALK "❌ Invalid code. Please try again."
EXIT
END IF
TALK "✅ Identity verified!"
TALK ""
' Load current consents
consents = FIND "user_consents" WHERE user_id = user.id
' Define consent categories
consent_categories = [
{
"id": "essential",
"name": "Essential Services",
"description": "Required for basic service functionality",
"required": TRUE,
"legal_basis": "Contract performance"
},
{
"id": "analytics",
"name": "Analytics & Improvement",
"description": "Help us improve our services through usage analysis",
"required": FALSE,
"legal_basis": "Legitimate interest / Consent"
},
{
"id": "marketing",
"name": "Marketing Communications",
"description": "Receive news, updates, and promotional content",
"required": FALSE,
"legal_basis": "Consent"
},
{
"id": "personalization",
"name": "Personalization",
"description": "Customize your experience based on preferences",
"required": FALSE,
"legal_basis": "Consent"
},
{
"id": "third_party",
"name": "Third-Party Sharing",
"description": "Share data with trusted partners for enhanced services",
"required": FALSE,
"legal_basis": "Consent"
},
{
"id": "ai_training",
"name": "AI Model Training",
"description": "Use anonymized data to improve AI capabilities",
"required": FALSE,
"legal_basis": "Consent"
}
]
TALK "📋 **Your Current Consents:**"
TALK ""
FOR EACH category IN consent_categories
current_consent = FILTER(consents, "category = '" + category.id + "'")
IF current_consent IS NOT NULL THEN
status = current_consent.granted ? "✅ Granted" : "❌ Denied"
granted_date = FORMAT(current_consent.updated_at, "DD/MM/YYYY")
ELSE
status = "⚪ Not Set"
granted_date = "N/A"
END IF
required_tag = category.required ? " (Required)" : ""
TALK category.name + required_tag + ": " + status
TALK " └─ " + category.description
TALK " └─ Legal basis: " + category.legal_basis
TALK " └─ Last updated: " + granted_date
TALK ""
NEXT
TALK "**What would you like to do?**"
TALK ""
TALK "1⃣ Grant a consent"
TALK "2⃣ Revoke a consent"
TALK "3⃣ Revoke ALL optional consents"
TALK "4⃣ Grant ALL consents"
TALK "5⃣ View consent history"
TALK "6⃣ Download consent record"
TALK "7⃣ Exit"
HEAR action AS INTEGER WITH "Enter your choice (1-7):"
SELECT CASE action
CASE 1
' Grant consent
TALK "Which consent would you like to grant?"
TALK "Available options: analytics, marketing, personalization, third_party, ai_training"
HEAR grant_category WITH "Enter consent category:"
' Validate category
valid_categories = ["analytics", "marketing", "personalization", "third_party", "ai_training"]
IF NOT CONTAINS(valid_categories, grant_category) THEN
TALK "❌ Invalid category. Please try again."
EXIT
END IF
' Record consent with full audit trail
consent_record = {
"user_id": user.id,
"category": grant_category,
"granted": TRUE,
"granted_at": NOW(),
"updated_at": NOW(),
"ip_address": GET SESSION "client_ip",
"user_agent": GET SESSION "user_agent",
"consent_version": "2.0",
"method": "explicit_dialog"
}
' Check if exists and update, otherwise insert
existing = FIND "user_consents" WHERE user_id = user.id AND category = grant_category
IF existing IS NOT NULL THEN
UPDATE "user_consents" SET granted = TRUE, updated_at = NOW(), method = "explicit_dialog" WHERE id = existing.id
ELSE
INSERT INTO "user_consents" VALUES consent_record
END IF
' Log to consent history
INSERT INTO "consent_history" VALUES {
"user_id": user.id,
"category": grant_category,
"action": "granted",
"timestamp": NOW(),
"ip_address": GET SESSION "client_ip"
}
TALK "✅ Consent for **" + grant_category + "** has been granted."
TALK "You can revoke this consent at any time."
CASE 2
' Revoke consent
TALK "Which consent would you like to revoke?"
TALK "Note: Essential services consent cannot be revoked while using the service."
HEAR revoke_category WITH "Enter consent category:"
IF revoke_category = "essential" THEN
TALK "⚠️ Essential consent is required for service operation."
TALK "To revoke it, you must delete your account."
EXIT
END IF
UPDATE "user_consents" SET granted = FALSE, updated_at = NOW(), method = "explicit_revoke" WHERE user_id = user.id AND category = revoke_category
INSERT INTO "consent_history" VALUES {
"user_id": user.id,
"category": revoke_category,
"action": "revoked",
"timestamp": NOW(),
"ip_address": GET SESSION "client_ip"
}
TALK "✅ Consent for **" + revoke_category + "** has been revoked."
TALK "This change takes effect immediately."
' Notify relevant systems
WEBHOOK POST "/internal/consent-changed" WITH {
"user_id": user.id,
"category": revoke_category,
"action": "revoked"
}
CASE 3
' Revoke all optional
TALK "⚠️ This will revoke ALL optional consents:"
TALK "• Analytics & Improvement"
TALK "• Marketing Communications"
TALK "• Personalization"
TALK "• Third-Party Sharing"
TALK "• AI Model Training"
HEAR confirm WITH "Type 'REVOKE ALL' to confirm:"
IF confirm <> "REVOKE ALL" THEN
TALK "Operation cancelled."
EXIT
END IF
UPDATE "user_consents" SET granted = FALSE, updated_at = NOW() WHERE user_id = user.id AND category <> "essential"
INSERT INTO "consent_history" VALUES {
"user_id": user.id,
"category": "ALL_OPTIONAL",
"action": "bulk_revoked",
"timestamp": NOW(),
"ip_address": GET SESSION "client_ip"
}
TALK "✅ All optional consents have been revoked."
CASE 4
' Grant all
TALK "This will grant consent for all categories."
TALK "You can revoke individual consents at any time."
HEAR confirm WITH "Type 'GRANT ALL' to confirm:"
IF confirm <> "GRANT ALL" THEN
TALK "Operation cancelled."
EXIT
END IF
FOR EACH category IN consent_categories
existing = FIND "user_consents" WHERE user_id = user.id AND category = category.id
IF existing IS NOT NULL THEN
UPDATE "user_consents" SET granted = TRUE, updated_at = NOW() WHERE id = existing.id
ELSE
INSERT INTO "user_consents" VALUES {
"user_id": user.id,
"category": category.id,
"granted": TRUE,
"granted_at": NOW(),
"updated_at": NOW(),
"method": "bulk_grant"
}
END IF
NEXT
INSERT INTO "consent_history" VALUES {
"user_id": user.id,
"category": "ALL",
"action": "bulk_granted",
"timestamp": NOW()
}
TALK "✅ All consents have been granted."
CASE 5
' View history
TALK "📜 **Your Consent History:**"
TALK ""
history = FIND "consent_history" WHERE user_id = user.id ORDER BY timestamp DESC LIMIT 20
IF COUNT(history) = 0 THEN
TALK "No consent history found."
ELSE
FOR EACH record IN history
action_icon = record.action CONTAINS "grant" ? "✅" : "❌"
TALK action_icon + " " + FORMAT(record.timestamp, "DD/MM/YYYY HH:mm") + " - " + record.category + " " + record.action
NEXT
END IF
CASE 6
' Download consent record
TALK "📥 Generating your consent record..."
consent_report = {
"generated_at": NOW(),
"user_email": email,
"current_consents": consents,
"consent_history": FIND "consent_history" WHERE user_id = user.id,
"legal_notice": "This document serves as proof of consent status under LGPD/GDPR"
}
filename = "consent_record_" + FORMAT(NOW(), "YYYYMMDD") + ".pdf"
GENERATE PDF filename WITH TEMPLATE "consent_report" DATA consent_report
SEND MAIL email, "Your Consent Record", "
Dear User,
Please find attached your complete consent record as requested.
This document includes:
- Current consent status for all categories
- Complete consent history with timestamps
- Legal basis for each processing activity
Keep this document for your records.
Pragmatismo Privacy Team
", ATTACHMENT filename
TALK "✅ Consent record has been sent to " + email
CASE 7
TALK "Thank you for managing your privacy preferences."
TALK "You can return here anytime to update your consents."
EXIT
CASE ELSE
TALK "Invalid choice. Please try again."
END SELECT
TALK ""
TALK "🔒 **Privacy Reminder:**"
TALK "• Your consents are stored securely"
TALK "• Changes take effect immediately"
TALK "• You can modify consents anytime"
TALK "• Contact privacy@company.com for questions"

View file

@ -0,0 +1,152 @@
' ============================================================================
' Privacy Template: Data Access Request (Subject Access Request - SAR)
' LGPD Art. 18 / GDPR Art. 15 - Right of Access
' ============================================================================
' This dialog handles user requests to access their personal data
' Companies can install this template for LGPD/GDPR compliance
TALK "📋 **Data Access Request**"
TALK "You have the right to access all personal data we hold about you."
TALK ""
' Verify user identity
TALK "First, I need to verify your identity for security purposes."
HEAR email AS EMAIL WITH "Please provide your registered email address:"
' Check if email exists in system
user = FIND "users" WHERE email = email
IF user IS NULL THEN
TALK "❌ We couldn't find an account with that email address."
TALK "Please check the email and try again, or contact support."
EXIT
END IF
' Send verification code
code = GENERATE CODE 6
SET SESSION "verification_code", code
SET SESSION "verified_email", email
SEND MAIL email, "Data Access Request - Verification Code", "
Your verification code is: " + code + "
This code expires in 15 minutes.
If you did not request this, please ignore this email.
"
HEAR entered_code AS TEXT WITH "📧 We sent a verification code to your email. Please enter it:"
IF entered_code <> code THEN
TALK "❌ Invalid verification code. Please start over."
EXIT
END IF
TALK "✅ Identity verified successfully!"
TALK ""
' Gather all user data
TALK "🔍 Gathering your personal data... This may take a moment."
TALK ""
' Get user profile data
profile = FIND "users" WHERE email = email
sessions = FIND "sessions" WHERE user_id = profile.id
messages = FIND "messages" WHERE user_id = profile.id
files = FIND "user_files" WHERE user_id = profile.id
consents = FIND "user_consents" WHERE user_id = profile.id
audit_logs = FIND "audit_logs" WHERE user_id = profile.id
' Build comprehensive report
report_data = {
"request_date": NOW(),
"request_type": "Subject Access Request (SAR)",
"legal_basis": "LGPD Art. 18 / GDPR Art. 15",
"profile": {
"name": profile.name,
"email": profile.email,
"phone": profile.phone,
"created_at": profile.created_at,
"last_login": profile.last_login,
"preferences": profile.preferences
},
"sessions": {
"total_count": COUNT(sessions),
"active_sessions": FILTER(sessions, "status = 'active'"),
"session_history": sessions
},
"communications": {
"total_messages": COUNT(messages),
"messages": messages
},
"files": {
"total_files": COUNT(files),
"file_list": MAP(files, "name, size, created_at")
},
"consents": consents,
"activity_log": audit_logs
}
' Generate PDF report
report_filename = "data_access_report_" + FORMAT(NOW(), "YYYYMMDD_HHmmss") + ".pdf"
GENERATE PDF report_filename WITH TEMPLATE "data_access_report" DATA report_data
' Upload to user's secure area
UPLOAD report_filename TO "/secure/reports/" + profile.id + "/"
' Send report via email
SEND MAIL email, "Your Data Access Request - Complete Report", "
Dear " + profile.name + ",
As requested, please find attached a complete report of all personal data we hold about you.
This report includes:
- Your profile information
- Session history
- Communication records
- Files you have uploaded
- Consent records
- Activity logs
Report generated: " + FORMAT(NOW(), "DD/MM/YYYY HH:mm") + "
Your rights under LGPD/GDPR:
- Right to rectification (Art. 18 III LGPD / Art. 16 GDPR)
- Right to erasure (Art. 18 VI LGPD / Art. 17 GDPR)
- Right to data portability (Art. 18 V LGPD / Art. 20 GDPR)
- Right to object to processing (Art. 18 IV LGPD / Art. 21 GDPR)
To exercise any of these rights, please contact us or use our privacy portal.
Best regards,
Privacy & Compliance Team
", ATTACHMENT report_filename
' Log the request for compliance audit
INSERT INTO "privacy_requests" VALUES {
"user_id": profile.id,
"request_type": "data_access",
"requested_at": NOW(),
"completed_at": NOW(),
"status": "completed",
"legal_basis": "LGPD Art. 18 / GDPR Art. 15"
}
TALK "✅ **Request Complete!**"
TALK ""
TALK "📧 We have sent a comprehensive report to: " + email
TALK ""
TALK "The report includes:"
TALK "• Your profile information"
TALK "• " + COUNT(sessions) + " session records"
TALK "• " + COUNT(messages) + " message records"
TALK "• " + COUNT(files) + " files"
TALK "• Consent history"
TALK "• Activity logs"
TALK ""
TALK "You can also download the report from your account settings."
TALK ""
TALK "🔒 **Your other privacy rights:**"
TALK "• Say **'correct my data'** to update your information"
TALK "• Say **'delete my data'** to request data erasure"
TALK "• Say **'export my data'** for portable format"
TALK "• Say **'privacy settings'** to manage consents"

View file

@ -0,0 +1,39 @@
' =============================================================================
' Privacy Rights Center - LGPD/GDPR Compliance Dialog
' General Bots Template for Data Subject Rights Management
' =============================================================================
' This template helps organizations comply with:
' - LGPD (Lei Geral de Proteção de Dados - Brazil)
' - GDPR (General Data Protection Regulation - EU)
' - CCPA (California Consumer Privacy Act)
' =============================================================================
TALK "Welcome to the Privacy Rights Center. I can help you exercise your data protection rights."
TALK "As a data subject, you have the following rights under LGPD/GDPR:"
TALK "1. Right of Access - View all data we hold about you"
TALK "2. Right to Rectification - Correct inaccurate data"
TALK "3. Right to Erasure - Request deletion of your data"
TALK "4. Right to Portability - Export your data"
TALK "5. Right to Object - Opt-out of certain processing"
TALK "6. Consent Management - Review and update your consents"
HEAR choice AS TEXT WITH "What would you like to do? (1-6 or type your request)"
SELECT CASE choice
CASE "1", "access", "view", "see my data"
CALL "access-data.bas"
CASE "2", "rectification", "correct", "update", "fix"
CALL "rectify-data.bas"
CASE "3", "erasure", "delete", "remove", "forget me"
CALL "erase-data.bas"
CASE "4", "portability", "export", "download"
CALL "export-data.bas"
CASE "5", "object", "opt-out", "stop processing"
CALL "object-processing.bas"
CASE "6", "consent", "cons

View file

@ -0,0 +1,44 @@
name,value
Bot Name,Privacy Rights Center
Bot Description,LGPD/GDPR Data Subject Rights Management Bot
Bot Version,1.0.0
Bot Author,Pragmatismo
Bot License,AGPL-3.0
Bot Category,Compliance
Bot Tags,privacy;lgpd;gdpr;hipaa;ccpa;compliance;data-protection
Default Language,en
Supported Languages,en;pt;es;de;fr;it
Welcome Message,Welcome to the Privacy Rights Center. I can help you exercise your data protection rights under LGPD, GDPR, and other privacy regulations.
Error Message,I apologize, but I encountered an issue processing your request. Please try again or contact our privacy team at privacy@company.com
Timeout Message,Your session has timed out for security. Please start a new conversation.
Session Timeout,900
Max Retries,3
Log Level,info
Enable Audit Log,true
Audit Log Retention Days,2555
Require Authentication,true
Require Email Verification,true
Require 2FA,false
Data Retention Days,30
Auto Delete Completed Requests,false
Send Confirmation Emails,true
Privacy Officer Email,privacy@company.com
DPO Contact,dpo@company.com
Supervisory Authority,ANPD
Company Name,Your Company Name
Company Address,Your Company Address
Company Country,BR
Compliance Frameworks,LGPD;GDPR;CCPA
Response SLA Hours,72
Escalation Email,legal@company.com
Enable HIPAA Mode,false
PHI Processing Enabled,false
Encryption At Rest,true
Encryption In Transit,true
PII Detection Enabled,true
Auto Anonymization,true
Consent Required,true
Consent Version,1.0
Terms URL,https://company.com/terms
Privacy Policy URL,https://company.com/privacy
Cookie Policy URL,https://company.com/cookies
Can't render this file because it has a wrong number of fields in line 11.

View file

@ -0,0 +1,913 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Privacy Rights Center</title>
<style>
:root {
--primary-color: #2563eb;
--primary-hover: #1d4ed8;
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--text-primary: #1e293b;
--text-secondary: #64748b;
--bg-primary: #f8fafc;
--bg-secondary: #ffffff;
--border-color: #e2e8f0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
header {
text-align: center;
margin-bottom: 3rem;
padding: 2rem;
background: var(--bg-secondary);
border-radius: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.logo {
font-size: 3rem;
margin-bottom: 1rem;
}
h1 {
font-size: 2rem;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.subtitle {
color: var(--text-secondary);
font-size: 1.1rem;
}
.compliance-badges {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 1.5rem;
flex-wrap: wrap;
}
.badge {
padding: 0.5rem 1rem;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 2rem;
font-size: 0.875rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.5rem;
}
.badge.lgpd { border-color: #22c55e; color: #16a34a; }
.badge.gdpr { border-color: #3b82f6; color: #2563eb; }
.badge.hipaa { border-color: #a855f7; color: #9333ea; }
.badge.ccpa { border-color: #f97316; color: #ea580c; }
.rights-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 3rem;
}
.right-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 1rem;
padding: 1.5rem;
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
}
.right-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.15);
}
.right-card .icon {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.right-card h3 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.right-card p {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 1rem;
}
.right-card .legal-ref {
font-size: 0.75rem;
color: var(--primary-color);
background: rgba(37, 99, 235, 0.1);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
display: inline-block;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 500;
border: none;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-primary:hover {
background: var(--primary-hover);
}
.btn-secondary {
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background: var(--border-color);
}
.btn-danger {
background: var(--danger-color);
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
/* Modal Styles */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-overlay.active {
display: flex;
}
.modal {
background: var(--bg-secondary);
border-radius: 1rem;
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
padding: 2rem;
position: relative;
}
.modal-close {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-secondary);
}
.modal h2 {
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
font-weight: 500;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: 0.5rem;
font-size: 1rem;
color: var(--text-primary);
background: var(--bg-secondary);
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.form-group textarea {
min-height: 100px;
resize: vertical;
}
.checkbox-group {
display: flex;
align-items: flex-start;
gap: 0.75rem;
}
.checkbox-group input[type="checkbox"] {
width: auto;
margin-top: 0.25rem;
}
.alert {
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
display: flex;
align-items: flex-start;
gap: 0.75rem;
}
.alert-info {
background: rgba(37, 99, 235, 0.1);
color: var(--primary-color);
border: 1px solid rgba(37, 99, 235, 0.2);
}
.alert-warning {
background: rgba(245, 158, 11, 0.1);
color: #b45309;
border: 1px solid rgba(245, 158, 11, 0.2);
}
.alert-success {
background: rgba(16, 185, 129, 0.1);
color: #047857;
border: 1px solid rgba(16, 185, 129, 0.2);
}
.consent-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: var(--bg-primary);
border-radius: 0.5rem;
margin-bottom: 0.75rem;
}
.consent-info h4 {
font-size: 1rem;
margin-bottom: 0.25rem;
}
.consent-info p {
font-size: 0.85rem;
color: var(--text-secondary);
}
.toggle {
position: relative;
width: 48px;
height: 26px;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #cbd5e1;
transition: 0.3s;
border-radius: 26px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 3px;
bottom: 3px;
background: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle input:checked + .toggle-slider {
background: var(--success-color);
}
.toggle input:checked + .toggle-slider:before {
transform: translateX(22px);
}
.request-status {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 1rem;
padding: 2rem;
margin-top: 2rem;
}
.status-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.status-list {
border-top: 1px solid var(--border-color);
}
.status-item {
display: grid;
grid-template-columns: 1fr 2fr 1fr 1fr;
padding: 1rem 0;
border-bottom: 1px solid var(--border-color);
align-items: center;
}
.status-item:last-child {
border-bottom: none;
}
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-size: 0.75rem;
font-weight: 500;
}
.status-pending {
background: rgba(245, 158, 11, 0.1);
color: #b45309;
}
.status-completed {
background: rgba(16, 185, 129, 0.1);
color: #047857;
}
.status-processing {
background: rgba(37, 99, 235, 0.1);
color: var(--primary-color);
}
footer {
text-align: center;
padding: 2rem;
color: var(--text-secondary);
font-size: 0.875rem;
}
footer a {
color: var(--primary-color);
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.rights-grid {
grid-template-columns: 1fr;
}
.status-item {
grid-template-columns: 1fr;
gap: 0.5rem;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">🔒</div>
<h1>Privacy Rights Center</h1>
<p class="subtitle">Exercise your data protection rights under LGPD, GDPR, and other privacy regulations</p>
<div class="compliance-badges">
<span class="badge lgpd">🇧🇷 LGPD Compliant</span>
<span class="badge gdpr">🇪🇺 GDPR Compliant</span>
<span class="badge hipaa">🏥 HIPAA Ready</span>
<span class="badge ccpa">🇺🇸 CCPA Compliant</span>
</div>
</header>
<section class="rights-grid">
<div class="right-card" onclick="openModal('accessModal')">
<div class="icon">📋</div>
<h3>Access My Data</h3>
<p>Request a complete copy of all personal data we hold about you in a portable format.</p>
<span class="legal-ref">LGPD Art. 18 / GDPR Art. 15</span>
</div>
<div class="right-card" onclick="openModal('rectifyModal')">
<div class="icon">✏️</div>
<h3>Correct My Data</h3>
<p>Request correction of inaccurate or incomplete personal data we hold about you.</p>
<span class="legal-ref">LGPD Art. 18 III / GDPR Art. 16</span>
</div>
<div class="right-card" onclick="openModal('deleteModal')">
<div class="icon">🗑️</div>
<h3>Delete My Data</h3>
<p>Request deletion of your personal data (Right to be Forgotten).</p>
<span class="legal-ref">LGPD Art. 18 VI / GDPR Art. 17</span>
</div>
<div class="right-card" onclick="openModal('portabilityModal')">
<div class="icon">📦</div>
<h3>Export My Data</h3>
<p>Download your data in a machine-readable format to transfer to another service.</p>
<span class="legal-ref">LGPD Art. 18 V / GDPR Art. 20</span>
</div>
<div class="right-card" onclick="openModal('consentModal')">
<div class="icon">⚙️</div>
<h3>Manage Consents</h3>
<p>Review and update your data processing consents and preferences.</p>
<span class="legal-ref">LGPD Art. 8 / GDPR Art. 7</span>
</div>
<div class="right-card" onclick="openModal('objectModal')">
<div class="icon">🚫</div>
<h3>Object to Processing</h3>
<p>Object to certain types of data processing or opt-out of specific activities.</p>
<span class="legal-ref">LGPD Art. 18 IV / GDPR Art. 21</span>
</div>
</section>
<section class="request-status">
<div class="status-header">
<h2>📊 Your Request History</h2>
<button class="btn btn-secondary" onclick="refreshStatus()">🔄 Refresh</button>
</div>
<div class="status-list" id="statusList">
<div class="status-item">
<span class="status-badge status-completed">Completed</span>
<span>Data Access Request</span>
<span>2025-01-15</span>
<a href="#" class="btn btn-secondary" style="padding: 0.5rem 1rem; font-size: 0.875rem;">Download</a>
</div>
<div class="status-item">
<span class="status-badge status-processing">Processing</span>
<span>Consent Update</span>
<span>2025-01-20</span>
<span>In Progress</span>
</div>
</div>
</section>
</div>
<!-- Access Data Modal -->
<div class="modal-overlay" id="accessModal">
<div class="modal">
<button class="modal-close" onclick="closeModal('accessModal')">&times;</button>
<h2>📋 Access My Data</h2>
<div class="alert alert-info">
<span></span>
<span>You will receive a complete report of all personal data we hold about you within 15 days.</span>
</div>
<form onsubmit="submitRequest(event, 'access')">
<div class="form-group">
<label for="access</span>-email">Email Address *</label>
<input type="email" id="access-email" required placeholder="your@email.com">
</div>
<div class="form-group">
<label for="access-format">Preferred Format</label>
<select id="access-format">
<option value="pdf">PDF Report</option>
<option value="json">JSON (Machine Readable)</option>
<option value="csv">CSV (Spreadsheet)</option>
<option value="all">All Formats</option>
</select>
</div>
<div class="form-group">
<label for="access-notes">Additional Notes</label>
<textarea id="access-notes" placeholder="Any specific data you're looking for..."></textarea>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">Submit Request</button>
</form>
</div>
</div>
<!-- Delete Data Modal -->
<div class="modal-overlay" id="deleteModal">
<div class="modal">
<button class="modal-close" onclick="closeModal('deleteModal')">&times;</button>
<h2>🗑️ Delete My Data</h2>
<div class="alert alert-warning">
<span>⚠️</span>
<span>This action is permanent and cannot be undone. Some data may be retained for legal compliance.</span>
</div>
<form onsubmit="submitRequest(event, 'delete')">
<div class="form-group">
<label for="delete-email">Email Address *</label>
<input type="email" id="delete-email" required placeholder="your@email.com">
</div>
<div class="form-group">
<label for="delete-scope">What to Delete</label>
<select id="delete-scope">
<option value="all">Everything (Complete Account Deletion)</option>
<option value="conversations">Conversation History Only</option>
<option value="files">Files and Documents Only</option>
<option value="activity">Activity Logs Only</option>
</select>
</div>
<div class="form-group">
<label for="delete-reason">Reason (Optional)</label>
<textarea id="delete-reason" placeholder="Help us improve by sharing why you're leaving..."></textarea>
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="delete-confirm" required>
<label for="delete-confirm">I understand this action is permanent and I want to proceed with data deletion</label>
</div>
</div>
<button type="submit" class="btn btn-danger" style="width: 100%;">Request Deletion</button>
</form>
</div>
</div>
<!-- Consent Management Modal -->
<div class="modal-overlay" id="consentModal">
<div class="modal">
<button class="modal-close" onclick="closeModal('consentModal')">&times;</button>
<h2>⚙️ Manage Consents</h2>
<div class="alert alert-info">
<span></span>
<span>Changes to your consents take effect immediately.</span>
</div>
<form onsubmit="submitRequest(event, 'consent')">
<div class="form-group">
<label for="consent-email">Email Address *</label>
<input type="email" id="consent-email" required placeholder="your@email.com">
</div>
<div class="consent-item">
<div class="consent-info">
<h4>Essential Services</h4>
<p>Required for basic functionality (cannot be disabled)</p>
</div>
<label class="toggle">
<input type="checkbox" checked disabled>
<span class="toggle-slider"></span>
</label>
</div>
<div class="consent-item">
<div class="consent-info">
<h4>Analytics & Improvement</h4>
<p>Help us improve through usage analysis</p>
</div>
<label class="toggle">
<input type="checkbox" id="consent-analytics" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div class="consent-item">
<div class="consent-info">
<h4>Marketing Communications</h4>
<p>Receive news, updates, and promotions</p>
</div>
<label class="toggle">
<input type="checkbox" id="consent-marketing">
<span class="toggle-slider"></span>
</label>
</div>
<div class="consent-item">
<div class="consent-info">
<h4>Personalization</h4>
<p>Customize experience based on your usage</p>
</div>
<label class="toggle">
<input type="checkbox" id="consent-personalization" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div class="consent-item">
<div class="consent-info">
<h4>Third-Party Sharing</h4>
<p>Share data with trusted partners</p>
</div>
<label class="toggle">
<input type="checkbox" id="consent-thirdparty">
<span class="toggle-slider"></span>
</label>
</div>
<div class="consent-item">
<div class="consent-info">
<h4>AI Model Training</h4>
<p>Use anonymized data to improve AI</p>
</div>
<label class="toggle">
<input type="checkbox" id="consent-ai">
<span class="toggle-slider"></span>
</label>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%; margin-top: 1rem;">Save Preferences</button>
</form>
</div>
</div>
<!-- Rectify Modal -->
<div class="modal-overlay" id="rectifyModal">
<div class="modal">
<button class="modal-close" onclick="closeModal('rectifyModal')">&times;</button>
<h2>✏️ Correct My Data</h2>
<form onsubmit="submitRequest(event, 'rectify')">
<div class="form-group">
<label for="rectify-email">Email Address *</label>
<input type="email" id="rectify-email" required placeholder="your@email.com">
</div>
<div class="form-group">
<label for="rectify-field">Data to Correct *</label>
<select id="rectify-field" required>
<option value="">Select field...</option>
<option value="name">Name</option>
<option value="email">Email Address</option>
<option value="phone">Phone Number</option>
<option value="address">Address</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group">
<label for="rectify-current">Current Value</label>
<input type="text" id="rectify-current" placeholder="What it currently shows">
</div>
<div class="form-group">
<label for="rectify-new">Correct Value *</label>
<input type="text" id="rectify-new" required placeholder="What it should be">
</div>
<div class="form-group">
<label for="rectify-reason">Additional Information</label>
<textarea id="rectify-reason" placeholder="Any additional details..."></textarea>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">Submit Correction Request</button>
</form>
</div>
</div>
<!-- Portability Modal -->
<div class="modal-overlay" id="portabilityModal">
<div class="modal">
<button class="modal-close" onclick="closeModal('portabilityModal')">&times;</button>
<h2>📦 Export My Data</h2>
<div class="alert alert-info">
<span></span>
<span>Your data will be prepared and a download link sent to your email within 72 hours.</span>
</div>
<form onsubmit="submitRequest(event, 'portability')">
<div class="form-group">
<label for="port-email">Email Address *</label>
<input type="email" id="port-email" required placeholder="your@email.com">
</div>
<div class="form-group">
<label for="port-format">Export Format *</label>
<select id="port-format" required>
<option value="json">JSON (Recommended for data transfer)</option>
<option value="csv">CSV (For spreadsheets)</option>
<option value="xml">XML (Universal format)</option>
<option value="all">All Formats (ZIP archive)</option>
</select>
</div>
<div class="form-group">
<label>Data Categories to Include</label>
<div class="checkbox-group">
<input type="checkbox" id="port-profile" checked>
<label for="port-profile">Profile Information</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="port-conversations" checked>
<label for="port-conversations">Conversations & Messages</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="port-files" checked>
<label for="port-files">Files & Documents</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="port-activity">
<label for="port-activity">Activity Logs</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="port-consents" checked>
<label for="port-consents">Consent Records</label>
</div>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">Generate Export</button>
</form>
</div>
</div>
<!-- Object Modal -->
<div class="modal-overlay" id="objectModal">
<div class="modal">
<button class="modal-close" onclick="closeModal('objectModal')">&times;</button>
<h2>🚫 Object to Processing</h2>
<form onsubmit="submitRequest(event, 'object')">
<div class="form-group">
<label for="object-email">Email Address *</label>
<input type="email" id="object-email" required placeholder="your@email.com">
</div>
<div class="form-group">
<label for="object-type">Processing Activity *</label>
<select id="object-type" required>
<option value="">Select activity...</option>
<option value="profiling">Automated Profiling</option>
<option value="direct-marketing">Direct Marketing</option>
<option value="analytics">Analytics & Statistics</option>
<option value="third-party">Third-Party Data Sharing</option>
<option value="ai-processing">AI/ML Processing</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group">
<label for="object-reason">Reason for Objection *</label>
<textarea id="object-reason" required placeholder="Please explain why you object to this processing..."></textarea>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">Submit Objection</button>
</form>
</div>
</div>
<footer>
<p>🔒 Your privacy matters. All requests are processed securely and confidentially.</p>
<p>
<a href="/privacy-policy">Privacy Policy</a>
<a href="/terms">Terms of Service</a>
<a href="mailto:privacy@company.com">Contact DPO</a>
</p>
<p style="margin-top: 1rem;">© 2025 Pragmatismo. Built with General Bots.</p>
</footer>
<script>
function openModal(modalId) {
document.getElementById(modalId).classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
document.body.style.overflow = 'auto';
}
// Close modal on overlay click
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.classList.remove('active');
document.body.style.overflow = 'auto';
}
});
});
// Close modal on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay.active').forEach(modal => {
modal.classList.remove('active');
});
document.body.style.overflow = 'auto';
}
});
async function submitRequest(event, type) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
data.request_type = type;
try {
const response = await fetch('/api/privacy/request', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (response.ok) {
alert('✅ Your request has been submitted successfully! Check your email for confirmation.');
form.reset();
closeModal(form.closest('.modal-overlay').id);
refreshStatus();
} else {
throw new Error('Request failed');
}
} catch (error) {
alert('❌ There was an error submitting your request. Please try again or contact support.');
}
}
async function refreshStatus() {
try {
const response = await fetch('/api/privacy/requests');
if (response.ok) {
const requests = await response.json();
updateStatusList(requests);
}
} catch (error) {
console.error('Failed to refresh status:', error);
}
}
function updateStatusList(requests) {
const statusList = document.getElementById('statusList');
if (requests.length === 0) {
statusList.innerHTML = '<p style="padding: 1rem; color: var(--text-secondary);">No requests found.</p>';
return;
}
statusList.innerHTML = requests.map(req => `
<div class="status-item">
<span class="status-badge status-${req.status.toLowerCase()}">${req.status}</span>
<span>${req.type}</span>
<span>${new Date(req.created_at).toLocaleDateString()}</span>
${req.download_url ?
`<a href="${req.download_url}" class="btn btn-secondary" style="padding: 0.5rem 1rem; font-size: 0.875rem;">Download</a>` :
`<span>${req.status}</span>`
}
</div>
`).join('');
}
// Load status on page load
document.addEventListener('DOMContentLoaded', refreshStatus);
</script>
</body>
</html>

View file

@ -1,423 +0,0 @@
{% extends "base.html" %}
{% block title %}Drive - BotServer{% endblock %}
{% block content %}
<div class="drive-container">
<!-- Header -->
<div class="drive-header">
<h1>Drive</h1>
<div class="drive-actions">
<button class="btn btn-primary"
hx-get="/api/drive/upload"
hx-target="#modal-container"
hx-swap="innerHTML">
<span>📤</span> Upload
</button>
<button class="btn btn-secondary"
hx-post="/api/drive/folder/new</span>"
hx-target="#file-tree"
hx-swap="outerHTML">
<span>📁</span> New Folder
</button>
</div>
</div>
<!-- Storage Info -->
<div class="storage-info"
hx-get="/api/drive/storage"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<div class="storage-bar">
<div class="storage-used" style="width: 25%"></div>
</div>
<div class="storage-text">12.3 GB of 50 GB used</div>
</div>
<!-- Main Content -->
<div class="drive-content">
<!-- Sidebar -->
<div class="drive-sidebar">
<div class="quick-access">
<div class="sidebar-item active"
hx-get="/api/drive/files?filter=all"
hx-target="#file-list"
hx-swap="innerHTML">
<span>📁</span> All Files
</div>
<div class="sidebar-item"
hx-get="/api/drive/files?filter=recent"
hx-target="#file-list"
hx-swap="innerHTML">
<span>🕐</span> Recent
</div>
<div class="sidebar-item"
hx-get="/api/drive/files?filter=starred"
hx-target="#file-list"
hx-swap="innerHTML">
<span></span> Starred
</div>
<div class="sidebar-item"
hx-get="/api/drive/files?filter=shared"
hx-target="#file-list"
hx-swap="innerHTML">
<span>👥</span> Shared
</div>
<div class="sidebar-item"
hx-get="/api/drive/files?filter=trash"
hx-target="#file-list"
hx-swap="innerHTML">
<span>🗑️</span> Trash
</div>
</div>
<!-- Folders Tree -->
<div class="folders-tree" id="folder-tree"
hx-get="/api/drive/folders"
hx-trigger="load"
hx-swap="innerHTML">
<div class="loading">Loading folders...</div>
</div>
</div>
<!-- File List -->
<div class="drive-main">
<!-- Breadcrumb -->
<div class="breadcrumb"
id="breadcrumb"
hx-get="/api/drive/breadcrumb"
hx-trigger="load, path-changed from:body"
hx-swap="innerHTML">
<span class="breadcrumb-item">Home</span>
</div>
<!-- Search Bar -->
<div class="search-container">
<input type="text"
class="search-input"
placeholder="Search files..."
name="query"
hx-get="/api/drive/search"
hx-trigger="keyup changed delay:500ms"
hx-target="#file-list"
hx-swap="innerHTML">
</div>
<!-- File Grid/List -->
<div class="file-list" id="file-list"
hx-get="/api/drive/files"
hx-trigger="load"
hx-swap="innerHTML">
<div class="loading">Loading files...</div>
</div>
</div>
<!-- File Preview Panel -->
<div class="file-preview" id="file-preview" style="display: none;">
<div class="preview-header">
<h3>File Preview</h3>
<button class="close-btn" onclick="closePreview()"></button>
</div>
<div class="preview-content" id="preview-content">
<!-- Preview content loaded here -->
</div>
<div class="preview-actions">
<button class="btn btn-secondary"
hx-get="/api/drive/file/download"
hx-include="#preview-file-id">
<span>⬇️</span> Download
</button>
<button class="btn btn-secondary"
hx-post="/api/drive/file/share"
hx-include="#preview-file-id">
<span>🔗</span> Share
</button>
<button class="btn btn-danger"
hx-delete="/api/drive/file"
hx-include="#preview-file-id"
hx-confirm="Are you sure you want to delete this file?">
<span>🗑️</span> Delete
</button>
</div>
</div>
</div>
</div>
<!-- Modal Container -->
<div id="modal-container"></div>
<!-- Hidden file ID input for preview actions -->
<input type="hidden" id="preview-file-id" name="file_id" value="">
<style>
.drive-container {
height: calc(100vh - var(--header-height));
display: flex;
flex-direction: column;
}
.drive-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background: var(--surface);
border-bottom: 1px solid var(--border);
}
.drive-actions {
display: flex;
gap: 0.5rem;
}
.storage-info {
padding: 1rem 1.5rem;
background: var(--surface);
border-bottom: 1px solid var(--border);
}
.storage-bar {
width: 100%;
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.storage-used {
height: 100%;
background: var(--primary);
transition: width 0.3s ease;
}
.storage-text {
font-size: 0.875rem;
color: var(--text-secondary);
}
.drive-content {
flex: 1;
display: flex;
overflow: hidden;
}
.drive-sidebar {
width: 240px;
background: var(--surface);
border-right: 1px solid var(--border);
overflow-y: auto;
}
.quick-access {
padding: 1rem;
border-bottom: 1px solid var(--border);
}
.sidebar-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.625rem 0.75rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 0.25rem;
}
.sidebar-item:hover {
background: var(--hover);
}
.sidebar-item.active {
background: var(--primary-light);
color: var(--primary);
font-weight: 500;
}
.folders-tree {
padding: 1rem;
}
.drive-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.breadcrumb {
padding: 0.75rem 1.5rem;
background: var(--background);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 0.5rem;
}
.breadcrumb-item {
color: var(--text-secondary);
cursor: pointer;
transition: color 0.2s;
}
.breadcrumb-item:hover {
color: var(--primary);
}
.breadcrumb-item:not(:last-child)::after {
content: '/';
margin-left: 0.5rem;
color: var(--border);
}
.search-container {
padding: 1rem 1.5rem;
}
.search-input {
width: 100%;
padding: 0.625rem 1rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
font-size: 0.875rem;
}
.file-list {
flex: 1;
overflow-y: auto;
padding: 1rem 1.5rem;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1rem;
}
.file-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.file-item:hover {
background: var(--hover);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.file-icon {
font-size: 3rem;
margin-bottom: 0.5rem;
}
.file-name {
font-size: 0.875rem;
text-align: center;
word-break: break-word;
max-width: 100%;
}
.file-preview {
width: 320px;
background: var(--surface);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid var(--border);
}
.preview-content {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.preview-actions {
padding: 1rem;
border-top: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.close-btn {
background: none;
border: none;
font-size: 1.25rem;
cursor: pointer;
color: var(--text-secondary);
}
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
color: var(--text-secondary);
}
/* Responsive */
@media (max-width: 768px) {
.drive-sidebar {
display: none;
}
.file-preview {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 100%;
z-index: 1000;
}
}
</style>
<script>
function closePreview() {
document.getElementById('file-preview').style.display = 'none';
}
function openPreview(fileId) {
document.getElementById('preview-file-id').value = fileId;
document.getElementById('file-preview').style.display = 'flex';
// Load preview content
htmx.ajax('GET', `/api/drive/file/${fileId}/preview`, {
target: '#preview-content',
swap: 'innerHTML'
});
}
// Handle file selection
document.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'file-list') {
// Attach click handlers to file items
document.querySelectorAll('.file-item').forEach(item => {
item.addEventListener('click', function() {
const fileId = this.dataset.fileId;
if (fileId) {
openPreview(fileId);
}
});
});
}
});
</script>
{% endblock %}

View file

@ -45,7 +45,7 @@ IF action = "create_po" THEN
WHILE adding_items = true DO
TALK "Enter item code (or 'done' to finish):"
item_code = HEAR
HEAR item_code AS "done", *
IF item_code = "done" THEN
adding_items = false
@ -56,16 +56,10 @@ IF action = "create_po" THEN
TALK "Item not found. Try again."
ELSE
TALK "Quantity to order:"
quantity = HEAR
HEAR quantity AS INTEGER
TALK "Unit price (or press Enter for last cost: " + item.last_cost + "):"
price_input = HEAR
IF price_input = "" THEN
unit_price = item.last_cost
ELSE
unit_price = price_input
END IF
HEAR unit_price AS MONEY DEFAULT item.last_cost
line = CREATE OBJECT
SET line.id = FORMAT GUID()
@ -101,7 +95,7 @@ IF action = "approve_po" THEN
IF po_number = "" THEN
TALK "Enter PO number to approve:"
po_number = HEAR
HEAR po_number
END IF
po = FIND "purchase_orders", "po_number = '" + po_number + "'"
@ -128,9 +122,9 @@ IF action = "approve_po" THEN
END FOR
TALK "Approve this PO? (yes/no)"
approval = HEAR
HEAR approval AS "yes", "no"
IF approval = "yes" OR approval = "YES" OR approval = "Yes" THEN
IF approval = "yes" THEN
po.status = "approved"
po.approved_by = user_id
po.approved_date = current_time
@ -158,7 +152,7 @@ IF action = "vendor_performance" THEN
IF vendor_code = "" THEN
TALK "Enter vendor code:"
vendor_code = HEAR
HEAR vendor_code
END IF
vendor = FIND "vendors", "vendor_code = '" + vendor_code + "'"
@ -266,16 +260,16 @@ IF action = "requisition" THEN
WHILE adding = true DO
TALK "Enter item description (or 'done'):"
item_desc = HEAR
HEAR item_desc AS "done", *
IF item_desc = "done" THEN
adding = false
ELSE
TALK "Quantity needed:"
quantity = HEAR
HEAR quantity AS INTEGER
TALK "Reason/Project:"
reason = HEAR
HEAR reason
req_item = CREATE OBJECT
SET req_item.description = item_desc
@ -304,7 +298,7 @@ IF action = "price_comparison" THEN
IF item_code = "" THEN
TALK "Enter item code:"
item_code = HEAR
HEAR item_code
END IF
item = FIND "items", "item_code = '" + item_code + "'"

View file

@ -1,89 +0,0 @@
{% extends "base.html" %}
{% block title %}Home - General Bots{% endblock %}
{% block content %}
<div class="home-container">
<h1 class="home-title">Welcome to General Bots</h1>
<p class="home-subtitle">Your AI-powered workspace</p>
<div class="app-grid">
{% for app in apps %}
<a href="{{ app.url }}"
class="app-card"
hx-get="{{ app.url }}"
hx-target="#main-content"
hx-push-url="true">
<div class="app-icon">{{ app.icon }}</div>
<div class="app-name">{{ app.name }}</div>
<div class="app-description">{{ app.description }}</div>
</a>
{% endfor %}
</div>
</div>
<style>
.home-container {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.home-title {
font-size: 2.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--text);
}
.home-subtitle {
font-size: 1.25rem;
color: var(--text-secondary);
margin-bottom: 2rem;
}
.app-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1.5rem;
padding: 1rem 0;
}
.app-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.5rem;
text-decoration: none;
color: inherit;
transition: all 0.2s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 0.75rem;
}
.app-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: var(--primary);
}
.app-icon {
font-size: 3rem;
line-height: 1;
}
.app-name {
font-weight: 600;
font-size: 1.125rem;
color: var(--text);
}
.app-description {
font-size: 0.875rem;
color: var(--text-secondary);
}
</style>
{% endblock %}

View file

@ -1,591 +0,0 @@
{% extends "base.html" %}
{% block title %}Mail - BotServer{% endblock %}
{% block content %}
<div class="mail-container">
<!-- Mail Header -->
<div class="mail-header">
<h1>Mail</h1>
<div class="mail-actions">
<button class="btn btn-primary"
hx-get="/api/mail/compose"
hx-target="#mail-content"
hx-swap="innerHTML">
<span>✉️</span> Compose
</button>
<button class="btn btn-secondary"
hx-post="/api/mail/refresh</span>"
hx-target="#mail-list"
hx-swap="innerHTML">
<span>🔄</span> Refresh
</button>
</div>
</div>
<!-- Main Content -->
<div class="mail-content-wrapper">
<!-- Sidebar -->
<div class="mail-sidebar">
<!-- Account Selector -->
<div class="account-selector"
hx-get="/api/mail/accounts"
hx-trigger="load"
hx-swap="innerHTML">
<select class="account-select">
<option>Loading accounts...</option>
</select>
</div>
<!-- Folders -->
<div class="mail</option>-folders">
<div class="folder-item active"
hx-get="/api/mail/folder/inbox"
hx-target="#mail-list"
hx-swap="innerHTML">
<span class="folder-icon">📥</span>
<span class="folder-name">Inbox</span>
<span class="folder-count" id="inbox-count"></span>
</div>
<div class="folder-item"
hx-get="/api/mail/folder/sent"
hx-target="#mail-list"
hx-swap="innerHTML">
<span class="folder-icon">📤</span>
<span class="folder-name">Sent</span>
</div>
<div class="folder-item"
hx-get="/api/mail/folder/drafts"
hx-target="#mail-list"
hx-swap="innerHTML">
<span class="folder-icon">📝</span>
<span class="folder-name">Drafts</span>
<span class="folder-count" id="drafts-count"></span>
</div>
<div class="folder-item"
hx-get="/api/mail/folder/starred"
hx-target="#mail-list"
hx-swap="innerHTML">
<span class="folder-icon"></span>
<span class="folder-name">Starred</span>
<span class="folder-count" id="starred-count"></span>
</div>
<div class="folder-item"
hx-get="/api/mail/folder/trash"
hx-target="#mail-list"
hx-swap="innerHTML">
<span class="folder-icon">🗑️</span>
<span class="folder-name">Trash</span>
</div>
</div>
<!-- Labels -->
<div class="mail-labels">
<div class="labels-header">Labels</div>
<div id="labels-list"
hx-get="/api/mail/labels"
hx-trigger="load"
hx-swap="innerHTML">
<div class="loading-small">Loading...</div>
</div>
</div>
</div>
<!-- Mail List -->
<div class="mail-list-container">
<!-- Search Bar -->
<div class="mail-search">
<input type="text"
class="mail-search-input"
placeholder="Search mail..."
name="query"
hx-get="/api/mail/search"
hx-trigger="keyup changed delay:500ms"
hx-target="#mail-list"
hx-swap="innerHTML">
</div>
<!-- Mail List -->
<div class="mail-list" id="mail-list"
hx-get="/api/mail/folder/inbox"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<div class="loading">Loading emails...</div>
</div>
</div>
<!-- Mail Content -->
<div class="mail-content" id="mail-content">
<div class="no-mail-selected">
<div class="empty-icon">📧</div>
<p>Select an email to read</p>
</div>
</div>
</div>
</div>
<!-- Compose Modal Template -->
<template id="compose-template">
<div class="compose-modal">
<div class="compose-header">
<h3>New Message</h3>
<button class="close-btn" onclick="closeCompose()"></button>
</div>
<form hx-post="/api/mail/send"
hx-target="#mail-content"
hx-swap="innerHTML">
<div class="compose-field">
<label>To:</label>
<input type="email" name="to" required>
</div>
<div class="compose-field">
<label>Cc:</label>
<input type="email" name="cc">
</div>
<div class="compose-field">
<label>Subject:</label>
<input type="text" name="subject" required>
</div>
<div class="compose-body">
<textarea name="body" rows="15" required></textarea>
</div>
<div class="compose-actions">
<button type="submit" class="btn btn-primary">
<span>📤</span> Send
</button>
<button type="button" class="btn btn-secondary"
hx-post="/api/mail/draft"
hx-include="closest form">
<span>💾</span> Save Draft
</button>
<button type="button" class="btn btn-ghost" onclick="closeCompose()">
Cancel
</button>
</div>
</form>
</div>
</template>
<style>
.mail-container {
height: calc(100vh - var(--header-height));
display: flex;
flex-direction: column;
}
.mail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background: var(--surface);
border-bottom: 1px solid var(--border);
}
.mail-actions {
display: flex;
gap: 0.5rem;
}
.mail-content-wrapper {
flex: 1;
display: flex;
overflow: hidden;
}
.mail-sidebar {
width: 240px;
background: var(--surface);
border-right: 1px solid var(--border);
overflow-y: auto;
}
.account-selector {
padding: 1rem;
border-bottom: 1px solid var(--border);
}
.account-select {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--background);
font-size: 0.875rem;
}
.mail-folders {
padding: 1rem;
border-bottom: 1px solid var(--border);
}
.folder-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.625rem 0.75rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 0.25rem;
}
.folder-item:hover {
background: var(--hover);
}
.folder-item.active {
background: var(--primary-light);
color: var(--primary);
font-weight: 500;
}
.folder-icon {
font-size: 1.125rem;
}
.folder-name {
flex: 1;
}
.folder-count {
background: var(--primary);
color: white;
padding: 0.125rem 0.375rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
}
.mail-labels {
padding: 1rem;
}
.labels-header {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 0.75rem;
}
.mail-list-container {
width: 380px;
background: var(--background);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
}
.mail-search {
padding: 1rem;
border-bottom: 1px solid var(--border);
}
.mail-search-input {
width: 100%;
padding: 0.625rem 1rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
font-size: 0.875rem;
}
.mail-list {
flex: 1;
overflow-y: auto;
}
.mail-item {
padding: 0.875rem 1rem;
border-bottom: 1px solid var(--border);
cursor: pointer;
transition: background 0.2s;
}
.mail-item:hover {
background: var(--hover);
}
.mail-item.unread {
background: var(--surface);
font-weight: 500;
}
.mail-item.selected {
background: var(--primary-light);
border-left: 3px solid var(--primary);
}
.mail-from {
font-size: 0.875rem;
margin-bottom: 0.25rem;
display: flex;
justify-content: space-between;
}
.mail-time {
font-size: 0.75rem;
color: var(--text-secondary);
}
.mail-subject {
font-size: 0.875rem;
margin-bottom: 0.25rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mail-preview {
font-size: 0.8125rem;
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mail-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.no-mail-selected {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary);
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.mail-view {
height: 100%;
display: flex;
flex-direction: column;
}
.mail-view-header {
padding: 1.5rem;
border-bottom: 1px solid var(--border);
}
.mail-view-subject {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1rem;
}
.mail-view-meta {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 1rem;
}
.mail-view-from {
font-weight: 500;
}
.mail-view-to {
color: var(--text-secondary);
font-size: 0.875rem;
}
.mail-view-date {
color: var(--text-secondary);
font-size: 0.875rem;
margin-left: auto;
}
.mail-view-actions {
display: flex;
gap: 0.5rem;
}
.mail-view-body {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
white-space: pre-wrap;
line-height: 1.6;
}
.compose-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 600px;
background: var(--surface);
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
z-index: 1000;
}
.compose-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
}
.compose-field {
display: flex;
align-items: center;
padding: 0.75rem 1.5rem;
border-bottom: 1px solid var(--border);
}
.compose-field label {
width: 60px;
color: var(--text-secondary);
font-size: 0.875rem;
}
.compose-field input {
flex: 1;
border: none;
background: none;
font-size: 0.875rem;
padding: 0.25rem;
}
.compose-body {
padding: 1rem 1.5rem;
}
.compose-body textarea {
width: 100%;
border: none;
background: none;
resize: vertical;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.5;
}
.compose-actions {
display: flex;
gap: 0.5rem;
padding: 1rem 1.5rem;
border-top: 1px solid var(--border);
}
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
color: var(--text-secondary);
}
.loading-small {
padding: 0.5rem;
color: var(--text-secondary);
font-size: 0.875rem;
}
/* Responsive */
@media (max-width: 1024px) {
.mail-list-container {
width: 320px;
}
}
@media (max-width: 768px) {
.mail-sidebar {
display: none;
}
.mail-list-container {
width: 100%;
}
.mail-content {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--background);
z-index: 100;
display: none;
}
.mail-content.active {
display: flex;
}
}
</style>
<script>
function closeCompose() {
const modal = document.querySelector('.compose-modal');
if (modal) {
modal.remove();
}
}
// Handle mail item clicks
document.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'mail-list') {
document.querySelectorAll('.mail-item').forEach(item => {
item.addEventListener('click', function() {
// Remove selected class from all items
document.querySelectorAll('.mail-item').forEach(i => {
i.classList.remove('selected');
});
// Add selected class to clicked item
this.classList.add('selected');
// Mark as read
this.classList.remove('unread');
// Load email content
const emailId = this.dataset.emailId;
if (emailId) {
htmx.ajax('GET', `/api/mail/email/${emailId}`, {
target: '#mail-content',
swap: 'innerHTML'
});
}
});
});
}
});
// Update folder counts
htmx.on('htmx:afterRequest', function(evt) {
if (evt.detail.pathInfo.requestPath.includes('/api/mail/counts')) {
const counts = JSON.parse(evt.detail.xhr.response);
if (counts.inbox !== undefined) {
document.getElementById('inbox-count').textContent = counts.inbox || '';
}
if (counts.drafts !== undefined) {
document.getElementById('drafts-count').textContent = counts.drafts || '';
}
if (counts.starred !== undefined) {
document.getElementById('starred-count').textContent = counts.starred || '';
}
}
});
// Load folder counts on page load
document.addEventListener('DOMContentLoaded', function() {
htmx.ajax('GET', '/api/mail/counts', {
swap: 'none'
});
});
</script>
{% endblock %}

View file

@ -1,949 +0,0 @@
{% extends "base.html" %}
{% block title %}Meet - BotServer{% endblock %}
{% block content %}
<div class="meet-container">
<!-- Meet Header -->
<div class="meet-header">
<h1>Video Meetings</h1>
<div class="meet-actions">
<button class="btn btn-primary"
hx-get="/api/meet/new"
hx-target="#modal-container"
hx-swap="innerHTML">
<span>🎥</span> New Meeting
</button>
<button class="btn btn-secondary"
hx-get="/api/meet/join</span>"
hx-target="#modal-container"
hx-swap="innerHTML">
<span>🔗</span> Join Meeting
</button>
</div>
</div>
<!-- Main Content -->
<div class="meet-content">
<!-- Left Panel - Meetings List -->
<div class="meet-sidebar">
<!-- Meeting Tabs -->
<div class="meet-tabs">
<button class="tab-btn active"
hx-get="/api/meet/upcoming"
hx-target="#meetings-list"
hx-swap="innerHTML">
Upcoming
</button>
<button class="tab-btn"
hx-get="/api/meet/past"
hx-target="#meetings-list"
hx-swap="innerHTML">
Past
</button>
<button class="tab-btn"
hx-get="/api/meet/recorded"
hx-target="#meetings-list"
hx-swap="innerHTML">
Recorded
</button>
</div>
<!-- Meetings List -->
<div class="meetings-list" id="meetings-list"
hx-get="/api/meet/upcoming"
hx-trigger="load, every 60s"
hx-swap="innerHTML">
<div class="loading">Loading meetings...</div>
</div>
</div>
<!-- Center - Meeting Area -->
<div class="meet-main" id="meet-main">
<!-- Pre-meeting Screen -->
<div class="pre-meeting" id="pre-meeting">
<div class="preview-container">
<div class="video-preview">
<video id="local-preview" autoplay muted></video>
<div class="preview-controls">
<button class="control-btn" id="toggle-camera" onclick="toggleCamera()">
<span>📹</span>
</button>
<button class="control-btn" id="toggle-mic" onclick="toggleMic()">
<span>🎤</span>
</button>
<button class="control-btn" onclick="testAudio()">
<span>🔊</span> Test Audio
</button>
</div>
</div>
<div class="meeting-info">
<h2>Ready to join?</h2>
<p>Check your audio and video before joining</p>
<div class="device-selectors">
<div class="device-selector">
<label>Camera</label>
<select id="camera-select" onchange="changeCamera()">
<option>Loading cameras...</option>
</select>
</div>
<div class="device-selector">
<label>Microphone</label>
<select id="mic-select" onchange="changeMic()">
<option>Loading microphones...</option>
</select>
</div>
<div class="device-selector">
<label>Speaker</label>
<select id="speaker-select" onchange="changeSpeaker()">
<option>Loading speakers...</option>
</select>
</div>
</div>
</div>
</div>
</div>
<!-- In-Meeting Screen (hidden by default) -->
<div class="in-meeting" id="in-meeting" style="display: none;">
<!-- Video Grid -->
<div class="video-grid" id="video-grid">
<div class="video-container main-video">
<video id="main-video" autoplay></video>
<div class="participant-info">
<span class="participant-name">You</span>
<span class="participant-status">🎤</span>
</div>
</div>
</div>
<!-- Meeting Controls -->
<div class="meeting-controls">
<div class="controls-left">
<span class="meeting-timer" id="meeting-timer">00:00</span>
<span class="meeting-id" id="meeting-id"></span>
</div>
<div class="controls-center">
<button class="control-btn" onclick="toggleMicrophone()">
<span>🎤</span>
</button>
<button class="control-btn" onclick="toggleVideo</span>()">
<span>📹</span>
</button>
<button class="control-btn" onclick="toggleScreenShare()">
<span>🖥️</span>
</button>
<button class="control-btn" onclick="toggleRecording()">
<span>⏺️</span>
</button>
<button class="control-btn danger" onclick="leaveMeeting()">
<span>📞</span></span> Leave
</button>
</div>
<div class="controls-right">
<button class="control-btn" onclick="toggleParticipants()">
<span>👥</span>
<span class="participant-count">1</span>
</button>
<button class="control-btn" onclick="toggleChat()">
<span>💬</span>
<span class="chat-badge" style="display: none;">0</span>
</button>
<button class="control-btn" onclick="toggleTranscription()">
<span>📝</span>
</button>
<button class="control-btn" onclick="toggleSettings()">
<span>⚙️</span>
</button>
</div>
</div>
</div>
</div>
<!-- Right Panel - Chat/Participants (hidden by default) -->
<div class="meet-panel" id="meet-panel" style="display: none;">
<!-- Panel Tabs -->
<div class="panel-tabs">
<button class="panel-tab active" onclick="showPanelTab('participants')">
Participants
</button>
<button class="panel-tab" onclick="showPanelTab('chat')">
Chat
</button>
<button class="panel-tab" onclick="showPanelTab('transcription')">
Transcription
</button>
</div>
<!-- Participants Panel -->
<div class="panel-content" id="participants-panel">
<div class="participants-list" id="participants-list"
hx-get="/api/meet/participants"
hx-trigger="load, every 5s"
hx-swap="innerHTML">
<div class="participant-item">
<span class="participant-avatar">👤</span>
<span class="participant-name">{{ user_name }} (You)</span>
<span class="participant-controls">
<span>🎤</span>
<span>📹</span>
</span>
</div>
</div></span>
</div>
<!-- Chat Panel -->
<div class="panel-content" id="chat-panel" style="display: none;">
<div class="chat-messages" id="meet-chat-messages"
hx-get="/api/meet/chat/messages"
hx-trigger="load, sse:message"
hx-swap="innerHTML">
</div>
<form class="chat-input-form"
hx-post="/api/meet/chat/send"
hx-target="#meet-chat-messages"
hx-swap="beforeend">
<input type="text"
name="message"
placeholder="Type a message..."
autocomplete="off">
<button type="submit">Send</button>
</form>
</div>
<!-- Transcription Panel -->
<div class="panel-content" id="transcription-panel" style="display: none;">
<div class="transcription-content" id="transcription-content"
hx-get="/api/meet/transcription"
hx-trigger="load, sse:transcription"
hx-swap="innerHTML">
<p class="transcription-empty">Transcription will appear here when enabled</p>
</div>
<div class="transcription-actions">
<button class="btn btn-sm" onclick="downloadTranscript()">
Download Transcript
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Modal Container -->
<div id="modal-container"></div>
<!-- WebSocket Connection for Real-time -->
<div hx-ext="ws" ws-connect="/ws/meet" id="meet-ws"></div>
<style>
.meet-container {
height: calc(100vh - var(--header-height));
display: flex;
flex-direction: column;
}
.meet-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background: var(--surface);
border-bottom: 1px solid var(--border);
}
.meet-actions {
display: flex;
gap: 0.5rem;
}
.meet-content {
flex: 1;
display: flex;
overflow: hidden;
}
.meet-sidebar {
width: 320px;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
}
.meet-tabs {
display: flex;
padding: 1rem;
gap: 0.5rem;
border-bottom: 1px solid var(--border);
}
.tab-btn {
flex: 1;
padding: 0.5rem;
background: var(--background);
border: 1px solid var(--border);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
}
.tab-btn.active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.meetings-list {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.meeting-item {
background: var(--background);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
margin-bottom: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.meeting-item:hover {
background: var(--hover);
transform: translateX(4px);
}
.meeting-title {
font-weight: 500;
margin-bottom: 0.5rem;
}
.meeting-time {
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 0.25rem;
}
.meeting-participants {
font-size: 0.8125rem;
color: var(--text-secondary);
}
.meet-main {
flex: 1;
display: flex;
flex-direction: column;
background: var(--background);
}
.pre-meeting {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.preview-container {
display: flex;
gap: 2rem;
align-items: center;
}
.video-preview {
position: relative;
width: 480px;
height: 360px;
background: #000;
border-radius: 12px;
overflow: hidden;
}
.video-preview video {
width: 100%;
height: 100%;
object-fit: cover;
}
.preview-controls {
position: absolute;
bottom: 1rem;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 0.5rem;
}
.meeting-info {
max-width: 300px;
}
.meeting-info h2 {
margin-bottom: 0.5rem;
}
.meeting-info p {
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
.device-selectors {
display: flex;
flex-direction: column;
gap: 1rem;
}
.device-selector {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.device-selector label {
font-size: 0.875rem;
color: var(--text-secondary);
}
.device-selector select {
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
font-size: 0.875rem;
}
.in-meeting {
flex: 1;
display: flex;
flex-direction: column;
}
.video-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1rem;
padding: 1rem;
background: #000;
}
.video-container {
position: relative;
background: #1a1a1a;
border-radius: 8px;
overflow: hidden;
aspect-ratio: 16/9;
}
.video-container video {
width: 100%;
height: 100%;
object-fit: cover;
}
.participant-info {
position: absolute;
bottom: 0.5rem;
left: 0.5rem;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.meeting-controls {
background: var(--surface);
border-top: 1px solid var(--border);
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.controls-left,
.controls-center,
.controls-right {
display: flex;
align-items: center;
gap: 0.5rem;
}
.control-btn {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--background);
border: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
font-size: 1.25rem;
}
.control-btn:hover {
background: var(--hover);
transform: scale(1.1);
}
.control-btn.active {
background: var(--primary);
color: white;
}
.control-btn.danger {
background: #ef4444;
color: white;
width: auto;
padding: 0 1rem;
border-radius: 24px;
}
.meeting-timer {
font-family: monospace;
font-size: 1rem;
color: var(--text-secondary);
}
.meeting-id {
font-size: 0.875rem;
color: var(--text-secondary);
}
.participant-count,
.chat-badge {
background: var(--primary);
color: white;
font-size: 0.75rem;
padding: 0.125rem 0.25rem;
border-radius: 10px;
margin-left: 0.25rem;
}
.meet-panel {
width: 360px;
background: var(--surface);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
}
.panel-tabs {
display: flex;
border-bottom: 1px solid var(--border);
}
.panel-tab {
flex: 1;
padding: 1rem;
background: none;
border: none;
cursor: pointer;
font-size: 0.875rem;
color: var(--text-secondary);
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.panel-tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.panel-content {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.participants-list {
padding: 1rem;
}
.participant-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 0.5rem;
}
.participant-item:hover {
background: var(--hover);
}
.participant-avatar {
font-size: 1.5rem;
}
.participant-name {
flex: 1;
font-size: 0.875rem;
}
.participant-controls {
display: flex;
gap: 0.5rem;
}
.chat-messages {
flex: 1;
padding: 1rem;
overflow-y: auto;
}
.chat-message {
margin-bottom: 1rem;
}
.chat-sender {
font-weight: 500;
font-size: 0.875rem;
margin-bottom: 0.25rem;
}
.chat-text {
font-size: 0.875rem;
color: var(--text-secondary);
}
.chat-time {
font-size: 0.75rem;
color: var(--text-secondary);
margin-top: 0.25rem;
}
.chat-input-form {
display: flex;
padding: 1rem;
border-top: 1px solid var(--border);
}
.chat-input-form input {
flex: 1;
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 6px 0 0 6px;
font-size: 0.875rem;
}
.chat-input-form button {
padding: 0.5rem 1rem;
background: var(--primary);
color: white;
border: none;
border-radius: 0 6px 6px 0;
cursor: pointer;
}
.transcription-content {
flex: 1;
padding: 1rem;
overflow-y: auto;
}
.transcription-empty {
text-align: center;
color: var(--text-secondary);
padding: 2rem;
}
.transcription-actions {
padding: 1rem;
border-top: 1px solid var(--border);
}
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
color: var(--text-secondary);
}
/* Responsive */
@media (max-width: 1024px) {
.meet-sidebar {
width: 280px;
}
.meet-panel {
width: 320px;
}
}
@media (max-width: 768px) {
.meet-sidebar {
display: none;
}
.meet-panel {
position: fixed;
top: 0;
right: -100%;
bottom: 0;
width: 100%;
z-index: 1000;
transition: right 0.3s;
}
.meet-panel.active {
right: 0;
}
.preview-container {
flex-direction: column;
}
.video-preview {
width: 100%;
max-width: 480px;
}
}
</style>
<script>
let localStream = null;
let meetingTimer = null;
let meetingStartTime = null;
// Initialize local preview
async function initPreview() {
try {
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
const video = document.getElementById('local-preview');
if (video) {
video.srcObject = localStream;
}
// Populate device selectors
await updateDeviceList();
} catch (err) {
console.error('Error accessing media devices:', err);
}
}
// Update device list
async function updateDeviceList() {
const devices = await navigator.mediaDevices.enumerateDevices();
const cameras = devices.filter(d => d.kind === 'videoinput');
const mics = devices.filter(d => d.kind === 'audioinput');
const speakers = devices.filter(d => d.kind === 'audiooutput');
updateSelect('camera-select', cameras);
updateSelect('mic-select', mics);
updateSelect('speaker-select', speakers);
}
function updateSelect(selectId, devices) {
const select = document.getElementById(selectId);
if (!select) return;
select.innerHTML = '';
devices.forEach(device => {
const option = document.createElement('option');
option.value = device.deviceId;
option.textContent = device.label || `Device ${device.deviceId.substr(0, 5)}`;
select.appendChild(option);
});
}
// Toggle functions
function toggleCamera() {
if (localStream) {
const videoTrack = localStream.getVideoTracks()[0];
if (videoTrack) {
videoTrack.enabled = !videoTrack.enabled;
document.getElementById('toggle-camera').classList.toggle('active');
}
}
}
function toggleMic() {
if (localStream) {
const audioTrack = localStream.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = !audioTrack.enabled;
document.getElementById('toggle-mic').classList.toggle('active');
}
}
}
function toggleMicrophone() {
toggleMic();
}
function toggleVideo() {
toggleCamera();
}
function toggleScreenShare() {
// Screen share implementation
console.log('Toggle screen share');
}
function toggleRecording() {
// Recording implementation
console.log('Toggle recording');
}
function toggleTranscription() {
// Transcription implementation
console.log('Toggle transcription');
}
function toggleParticipants() {
togglePanel('participants');
}
function toggleChat() {
togglePanel('chat');
}
function toggleSettings() {
// Settings implementation
console.log('Toggle settings');
}
function togglePanel(panelName) {
const panel = document.getElementById('meet-panel');
if (panel) {
if (panel.style.display === 'none') {
panel.style.display = 'flex';
showPanelTab(panelName);
} else {
panel.style.display = 'none';
}
}
}
function showPanelTab(tabName) {
// Hide all panels
document.querySelectorAll('.panel-content').forEach(p => {
p.style.display = 'none';
});
// Remove active class from all tabs
document.querySelectorAll('.panel-tab').forEach(t => {
t.classList.remove('active');
});
// Show selected panel
const panel = document.getElementById(`${tabName}-panel`);
if (panel) {
panel.style.display = 'flex';
}
// Set active tab
event.target.classList.add('active');
}
function joinMeeting(meetingId) {
// Hide pre-meeting screen
document.getElementById('pre-meeting').style.display = 'none';
// Show in-meeting screen
document.getElementById('in-meeting').style.display = 'flex';
// Start timer
startMeetingTimer();
// Set meeting ID
document.getElementById('meeting-id').textContent = `Meeting: ${meetingId}`;
}
function leaveMeeting() {
if (confirm('Are you sure you want to leave the meeting?')) {
// Stop all tracks
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
}
// Stop timer
if (meetingTimer) {
clearInterval(meetingTimer);
}
// Show pre-meeting screen
document.getElementById('in-meeting').style.display = 'none';
document.getElementById('pre-meeting').style.display = 'flex';
// Reinitialize preview
initPreview();
}
}
function startMeetingTimer() {
meetingStartTime = Date.now();
meetingTimer = setInterval(() => {
const elapsed = Math.floor((Date.now() - meetingStartTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
document.getElementById('meeting-timer').textContent =
`${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}, 1000);
}
function testAudio() {
// Play test sound
const audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSp9y+7hljcFHWzA7+OZURE');
audio.play();
}
function downloadTranscript() {
// Download transcript implementation
console.log('Download transcript');
}
// Initialize on load
document.addEventListener('DOMContentLoaded', function() {
initPreview();
});
// Handle WebSocket messages
document.addEventListener('htmx:wsMessage', function(event) {
const message = JSON.parse(event.detail.message);
switch(message.type) {
case 'participant_joined':
console.log('Participant joined:', message.participant);
break;
case 'participant_left':
console.log('Participant left:', message.participant);
break;
case 'chat_message':
// Update chat badge
const badge = document.querySelector('.chat-badge');
if (badge && document.getElementById('chat-panel').style.display === 'none') {
const count = parseInt(badge.textContent) + 1;
badge.textContent = count;
badge.style.display = count > 0 ? 'inline' : 'none';
}
break;
case 'transcription':
// Update transcription
if (document.getElementById('transcription-panel').style.display !== 'none') {
htmx.ajax('GET', '/api/meet/transcription', {
target: '#transcription-content',
swap: 'innerHTML'
});
}
break;
}
});
</script>
{% endblock %}

View file

@ -0,0 +1,257 @@
' =============================================================================
' Custom Report Generator Dialog
' General Bots Analytics Template
' =============================================================================
' This dialog allows users to create custom reports from platform metrics
' =============================================================================
TALK "Custom Report Generator"
TALK "I will help you create a custom analytics report."
HEAR report_name AS TEXT WITH "What would you like to name this report?"
TALK "Select the time range for your report:"
TALK "1. Last Hour"
TALK "2. Last 24 Hours"
TALK "3. Last 7 Days"
TALK "4. Last 30 Days"
TALK "5. Last 90 Days"
TALK "6. Custom Range"
HEAR time_choice AS INTEGER
SELECT CASE time_choice
CASE 1
time_range = "1h"
time_label = "Last Hour"
CASE 2
time_range = "24h"
time_label = "Last 24 Hours"
CASE 3
time_range = "7d"
time_label = "Last 7 Days"
CASE 4
time_range = "30d"
time_label = "Last 30 Days"
CASE 5
time_range = "90d"
time_label = "Last 90 Days"
CASE 6
HEAR start_date AS DATE WITH "Enter start date (YYYY-MM-DD):"
HEAR end_date AS DATE WITH "Enter end date (YYYY-MM-DD):"
time_range = "custom"
time_label = FORMAT(start_date, "YYYY-MM-DD") + " to " + FORMAT(end_date, "YYYY-MM-DD")
CASE ELSE
time_range = "7d"
time_label = "Last 7 Days"
END SELECT
TALK "Select metrics to include (enter numbers separated by commas):"
TALK "1. Message Volume"
TALK "2. Active Sessions"
TALK "3. Response Time"
TALK "4. LLM Token Usage"
TALK "5. Error Rate"
TALK "6. Storage Usage"
TALK "7. API Calls"
TALK "8. User Activity"
TALK "9. Bot Performance"
TALK "10. All Metrics"
HEAR metrics_choice AS TEXT
metrics_list = SPLIT(metrics_choice, ",")
include_messages = CONTAINS(metrics_list, "1") OR CONTAINS(metrics_list, "10")
include_sessions = CONTAINS(metrics_list, "2") OR CONTAINS(metrics_list, "10")
include_response = CONTAINS(metrics_list, "3") OR CONTAINS(metrics_list, "10")
include_tokens = CONTAINS(metrics_list, "4") OR CONTAINS(metrics_list, "10")
include_errors = CONTAINS(metrics_list, "5") OR CONTAINS(metrics_list, "10")
include_storage = CONTAINS(metrics_list, "6") OR CONTAINS(metrics_list, "10")
include_api = CONTAINS(metrics_list, "7") OR CONTAINS(metrics_list, "10")
include_users = CONTAINS(metrics_list, "8") OR CONTAINS(metrics_list, "10")
include_bots = CONTAINS(metrics_list, "9") OR CONTAINS(metrics_list, "10")
TALK "Select grouping interval:"
TALK "1. Hourly"
TALK "2. Daily"
TALK "3. Weekly"
TALK "4. Monthly"
HEAR group_choice AS INTEGER
SELECT CASE group_choice
CASE 1
group_interval = "1h"
CASE 2
group_interval = "1d"
CASE 3
group_interval = "1w"
CASE 4
group_interval = "1mo"
CASE ELSE
group_interval = "1d"
END SELECT
TALK "Generating your custom report..."
report_data = {}
report_data.name = report_name
report_data.time_range = time_label
report_data.generated_at = NOW()
report_data.generated_by = GET SESSION "user_email"
IF include_messages THEN
messages = QUERY METRICS "messages" FOR time_range BY group_interval
report_data.messages = messages
report_data.total_messages = SUM(messages, "count")
END IF
IF include_sessions THEN
sessions = QUERY METRICS "active_sessions" FOR time_range BY group_interval
report_data.sessions = sessions
report_data.peak_sessions = MAX(sessions, "count")
END IF
IF include_response THEN
response_times = QUERY METRICS "response_time" FOR time_range BY group_interval
report_data.response_times = response_times
report_data.avg_response_ms = AVG(response_times, "duration_ms")
END IF
IF include_tokens THEN
tokens = QUERY METRICS "llm_tokens" FOR time_range BY group_interval
report_data.tokens = tokens
report_data.total_tokens = SUM(tokens, "total_tokens")
END IF
IF include_errors THEN
errors = QUERY METRICS "errors" FOR time_range BY group_interval
report_data.errors = errors
report_data.total_errors = SUM(errors, "count")
END IF
IF include_storage THEN
storage = QUERY METRICS "storage_usage" FOR time_range BY group_interval
report_data.storage = storage
report_data.current_storage_gb = LAST(storage, "bytes_used") / 1073741824
END IF
IF include_api THEN
api_calls = QUERY METRICS "api_requests" FOR time_range BY group_interval
report_data.api_calls = api_calls
report_data.total_api_calls = SUM(api_calls, "count")
END IF
IF include_users THEN
users = FIND "users" WHERE last_login >= DATEADD(NOW(), -30, "day")
report_data.active_users_30d = COUNT(users)
END IF
IF include_bots THEN
bots = FIND "bots" WHERE status = "active"
report_data.active_bots = COUNT(bots)
END IF
SET CONTEXT "You are an analytics expert. Generate executive insights from this report data."
insights = LLM "Analyze this platform data and provide 3-5 key insights for executives: " + JSON(report_data)
report_data.ai_insights = insights
TALK "Select export format:"
TALK "1. PDF Report"
TALK "2. Excel Spreadsheet"
TALK "3. CSV Data"
TALK "4. JSON Data"
TALK "5. All Formats"
HEAR format_choice AS INTEGER
timestamp = FORMAT(NOW(), "YYYYMMDD_HHmmss")
base_filename = "report_" + REPLACE(report_name, " ", "_") + "_" + timestamp
SELECT CASE format_choice
CASE 1
filename = base_filename + ".pdf"
GENERATE PDF filename WITH TEMPLATE "analytics_report" DATA report_data
CASE 2
filename = base_filename + ".xlsx"
WRITE filename, report_data
CASE 3
filename = base_filename + ".csv"
WRITE filename, CSV(report_data)
CASE 4
filename = base_filename + ".json"
WRITE filename, JSON(report_data)
CASE 5
GENERATE PDF base_filename + ".pdf" WITH TEMPLATE "analytics_report" DATA report_data
WRITE base_filename + ".xlsx", report_data
WRITE base_filename + ".csv", CSV(report_data)
WRITE base_filename + ".json", JSON(report_data)
filename = base_filename + ".zip"
COMPRESS filename, base_filename + ".*"
CASE ELSE
filename = base_filename + ".pdf"
GENERATE PDF filename WITH TEMPLATE "analytics_report" DATA report_data
END SELECT
UPLOAD filename TO "/reports/custom/"
download_link = GENERATE SECURE LINK "/reports/custom/" + filename EXPIRES 7 DAYS
TALK "Report generated successfully."
TALK "Report Name: " + report_name
TALK "Time Range: " + time_label
TALK "Download: " + download_link
HEAR send_email AS BOOLEAN WITH "Would you like to receive this report via email?"
IF send_email THEN
user_email = GET SESSION "user_email"
SEND MAIL user_email, "Custom Analytics Report: " + report_name, "Your custom analytics report is ready. Download link: " + download_link + " (expires in 7 days)", ATTACHMENT filename
TALK "Report sent to " + user_email
END IF
INSERT INTO "report_history" VALUES {
"id": "RPT-" + timestamp,
"name": report_name,
"generated_by": GET SESSION "user_email",
"generated_at": NOW(),
"time_range": time_label,
"metrics_included": metrics_choice,
"filename": filename
}
TALK "Would you like to schedule this report to run automatically?"
HEAR schedule_report AS BOOLEAN
IF schedule_report THEN
TALK "Select schedule frequency:"
TALK "1. Daily"
TALK "2. Weekly"
TALK "3. Monthly"
HEAR freq_choice AS INTEGER
SELECT CASE freq_choice
CASE 1
schedule = "0 8 * * *"
freq_label = "Daily at 8:00 AM"
CASE 2
schedule = "0 8 * * 1"
freq_label = "Weekly on Monday at 8:00 AM"
CASE 3
schedule = "0 8 1 * *"
freq_label = "Monthly on 1st at 8:00 AM"
CASE ELSE
schedule = "0 8 * * 1"
freq_label = "Weekly on Monday at 8:00 AM"
END SELECT
SET BOT MEMORY "scheduled_report_" + report_name, JSON(report_data)
SET SCHEDULE schedule, "generate-scheduled-report.bas"
TALK "Report scheduled: " + freq_label
END IF
TALK "Thank you for using the Custom Report Generator."

View file

@ -0,0 +1,115 @@
' =============================================================================
' Platform Overview - Key Metrics Summary
' Analytics Bot Dialog for General Bots
' =============================================================================
TALK "Generating platform overview..."
HEAR timeRange AS TEXT WITH "Select time range (1h, 6h, 24h, 7d, 30d):" DEFAULT "24h"
' Query platform metrics from time-series database
messages = QUERY METRICS "messages" FOR timeRange
sessions = QUERY METRICS "active_sessions" FOR timeRange
responseTime = QUERY METRICS "response_time" FOR timeRange
errors = QUERY METRICS "errors" FOR timeRange
tokens = QUERY METRICS "llm_tokens" FOR timeRange
' Calculate totals
totalMessages = SUM(messages, "count")
avgSessions = AVG(sessions, "count")
avgResponseTime = AVG(responseTime, "duration_ms")
totalErrors = SUM(errors, "count")
totalTokens = SUM(tokens, "total_tokens")
' Calculate trends compared to previous period
prevMessages = QUERY METRICS "messages" FOR timeRange OFFSET 1
prevSessions = QUERY METRICS "active_sessions" FOR timeRange OFFSET 1
messagesTrend = ((totalMessages - SUM(prevMessages, "count")) / SUM(prevMessages, "count")) * 100
sessionsTrend = ((avgSessions - AVG(prevSessions, "count")) / AVG(prevSessions, "count")) * 100
TALK "Platform Overview for " + timeRange
TALK ""
TALK "Messages"
TALK " Total: " + FORMAT(totalMessages, "#,###")
TALK " Trend: " + FORMAT(messagesTrend, "+#.#") + "%"
TALK ""
TALK "Sessions"
TALK " Average Active: " + FORMAT(avgSessions, "#,###")
TALK " Trend: " + FORMAT(sessionsTrend, "+#.#") + "%"
TALK ""
TALK "Performance"
TALK " Avg Response Time: " + FORMAT(avgResponseTime, "#.##") + " ms"
TALK ""
TALK "Errors"
TALK " Total: " + FORMAT(totalErrors, "#,###")
TALK " Error Rate: " + FORMAT((totalErrors / totalMessages) * 100, "#.##") + "%"
TALK ""
TALK "LLM Usage"
TALK " Total Tokens: " + FORMAT(totalTokens, "#,###")
TALK ""
HEAR action AS TEXT WITH "Options: (D)etail, (E)xport report, (A)lerts, (B)ack"
SELECT CASE UCASE(action)
CASE "D", "DETAIL"
TALK "Select metric for detailed view:"
TALK "1. Messages breakdown by channel"
TALK "2. Sessions by bot"
TALK "3. Response time distribution"
TALK "4. Error breakdown by type"
HEAR detailChoice AS INTEGER
SELECT CASE detailChoice
CASE 1
CALL "message-analytics.bas"
CASE 2
CALL "user-analytics.bas"
CASE 3
CALL "performance-metrics.bas"
CASE 4
CALL "error-analysis.bas"
END SELECT
CASE "E", "EXPORT"
HEAR exportFormat AS TEXT WITH "Export format (PDF, CSV, XLSX):" DEFAULT "PDF"
report = {
"title": "Platform Overview Report",
"generated_at": NOW(),
"time_range": timeRange,
"metrics": {
"total_messages": totalMessages,
"messages_trend": messagesTrend,
"avg_sessions": avgSessions,
"sessions_trend": sessionsTrend,
"avg_response_time": avgResponseTime,
"total_errors": totalErrors,
"error_rate": (totalErrors / totalMessages) * 100,
"total_tokens": totalTokens
}
}
filename = "platform_overview_" + FORMAT(NOW(), "YYYYMMDD_HHmmss")
SELECT CASE UCASE(exportFormat)
CASE "PDF"
GENERATE PDF filename + ".pdf" WITH TEMPLATE "analytics_report" DATA report
CASE "CSV"
WRITE filename + ".csv", CSV(report.metrics)
CASE "XLSX"
WRITE filename + ".xlsx", EXCEL(report)
END SELECT
TALK "Report exported: " + filename + "." + LCASE(exportFormat)
TALK "The file is available in your Drive."
CASE "A", "ALERTS"
CALL "configure-alerts.bas"
CASE "B", "BACK"
CALL "start.bas"
CASE ELSE
CALL "start.bas"
END SELECT

View file

@ -0,0 +1,58 @@
' =============================================================================
' Analytics Bot - Platform Metrics and Reporting Dialog
' General Bots Template for Platform Analytics
' =============================================================================
' This template provides analytics capabilities for:
' - Platform usage metrics
' - Performance monitoring
' - Custom report generation
' - Multi-agent analytics queries
' =============================================================================
TALK "Welcome to the Analytics Center. I can help you understand your platform metrics and generate reports."
TALK "What would you like to analyze?"
TALK "1. Platform Overview - Key metrics summary"
TALK "2. Message Analytics - Conversation statistics"
TALK "3. User Analytics - Active users and sessions"
TALK "4. Performance Metrics - Response times and throughput"
TALK "5. LLM Usage - Token consumption and costs"
TALK "6. Storage Analytics - Disk usage and file statistics"
TALK "7. Error Analysis - Error patterns and trends"
TALK "8. Generate Custom Report"
HEAR choice AS INTEGER
SELECT CASE choice
CASE 1
CALL "platform-overview.bas"
CASE 2
CALL "message-analytics.bas"
CASE 3
CALL "user-analytics.bas"
CASE 4
CALL "performance-metrics.bas"
CASE 5
CALL "llm-usage.bas"
CASE 6
CALL "storage-analytics.bas"
CASE 7
CALL "error-analysis.bas"
CASE 8
CALL "custom-report.bas"
CASE ELSE
SET CONTEXT "You are an analytics assistant. Help the user understand platform metrics. Available data: messages, sessions, response_time, llm_tokens, storage, errors. Answer questions about trends, patterns, and performance."
HEAR query AS TEXT
response = LLM "Analyze this analytics query and provide insights: " + query
TALK response
END SELECT

View file

@ -0,0 +1,39 @@
name,value
Bot Name,Analytics Manager
Bot Description,Platform analytics and reporting bot for managers and administrators
Bot Version,1.0.0
Bot Author,Pragmatismo
Bot License,AGPL-3.0
Bot Category,Platform
Bot Tags,analytics;reporting;metrics;dashboard;monitoring;insights
Default Language,en
Supported Languages,en;pt;es
Welcome Message,Welcome to the Analytics Manager. I can help you understand your platform metrics and generate reports.
Error Message,I encountered an issue processing your analytics request. Please try again or contact support.
Timeout Message,Your session has timed out. Please start a new conversation.
Session Timeout,1800
Max Retries,3
Log Level,info
Enable Audit Log,true
Require Authentication,true
Required Role,manager
Data Retention Days,365
Default Time Range,7d
Supported Time Ranges,1h;6h;24h;7d;30d;90d;1y
Default Chart Type,line
Enable Real Time,true
Refresh Interval Seconds,30
Export Formats,pdf;csv;xlsx;json
Max Export Rows,100000
Enable AI Insights,true
Enable Anomaly Detection,true
Enable Forecasting,true
Enable Alerts,true
Alert Threshold Critical,90
Alert Threshold Warning,70
Metrics Bucket,metrics
Metrics Org,pragmatismo
Dashboard Cache Seconds,60
Report Cache Seconds,300
Enable Multi Agent,true
Delegate Bots,default;crm;support
1 name value
2 Bot Name Analytics Manager
3 Bot Description Platform analytics and reporting bot for managers and administrators
4 Bot Version 1.0.0
5 Bot Author Pragmatismo
6 Bot License AGPL-3.0
7 Bot Category Platform
8 Bot Tags analytics;reporting;metrics;dashboard;monitoring;insights
9 Default Language en
10 Supported Languages en;pt;es
11 Welcome Message Welcome to the Analytics Manager. I can help you understand your platform metrics and generate reports.
12 Error Message I encountered an issue processing your analytics request. Please try again or contact support.
13 Timeout Message Your session has timed out. Please start a new conversation.
14 Session Timeout 1800
15 Max Retries 3
16 Log Level info
17 Enable Audit Log true
18 Require Authentication true
19 Required Role manager
20 Data Retention Days 365
21 Default Time Range 7d
22 Supported Time Ranges 1h;6h;24h;7d;30d;90d;1y
23 Default Chart Type line
24 Enable Real Time true
25 Refresh Interval Seconds 30
26 Export Formats pdf;csv;xlsx;json
27 Max Export Rows 100000
28 Enable AI Insights true
29 Enable Anomaly Detection true
30 Enable Forecasting true
31 Enable Alerts true
32 Alert Threshold Critical 90
33 Alert Threshold Warning 70
34 Metrics Bucket metrics
35 Metrics Org pragmatismo
36 Dashboard Cache Seconds 60
37 Report Cache Seconds 300
38 Enable Multi Agent true
39 Delegate Bots default;crm;support

View file

@ -0,0 +1,480 @@
' =============================================================================
' Account Management Dialog
' Dynamics CRM-style Account Entity Management
' General Bots CRM Template
' =============================================================================
' This dialog provides comprehensive account (company) management similar to
' Microsoft Dynamics CRM including:
' - Account creation and updates
' - Account hierarchy (parent/child relationships)
' - Contact associations
' - Activity timeline
' - Account scoring and health
' =============================================================================
PARAM action AS TEXT
SELECT CASE UCASE(action)
CASE "CREATE"
CALL create_account
CASE "UPDATE"
CALL update_account
CASE "VIEW"
CALL view_account
CASE "LIST"
CALL list_accounts
CASE "SEARCH"
CALL search_accounts
CASE "HIERARCHY"
CALL account_hierarchy
CASE "CONTACTS"
CALL account_contacts
CASE "ACTIVITIES"
CALL account_activities
CASE "HEALTH"
CALL account_health
CASE ELSE
TALK "Account Management"
TALK "Available actions: Create, Update, View, List, Search, Hierarchy, Contacts, Activities, Health"
HEAR selected_action AS TEXT WITH "What would you like to do?"
CALL "account-management.bas", selected_action
END SELECT
' -----------------------------------------------------------------------------
' CREATE ACCOUNT
' -----------------------------------------------------------------------------
SUB create_account
TALK "Create New Account"
HEAR account_name AS TEXT WITH "Company name:"
HEAR account_type AS TEXT WITH "Account type (Customer, Partner, Vendor, Competitor, Other):" DEFAULT "Customer"
HEAR industry AS TEXT WITH "Industry:"
HEAR phone AS TEXT WITH "Main phone:" DEFAULT ""
HEAR website AS TEXT WITH "Website:" DEFAULT ""
HEAR email AS TEXT WITH "Primary email:" DEFAULT ""
TALK "Address Information"
HEAR street AS TEXT WITH "Street address:" DEFAULT ""
HEAR city AS TEXT WITH "City:" DEFAULT ""
HEAR state AS TEXT WITH "State/Province:" DEFAULT ""
HEAR postal_code AS TEXT WITH "Postal code:" DEFAULT ""
HEAR country AS TEXT WITH "Country:" DEFAULT ""
TALK "Business Information"
HEAR employees AS INTEGER WITH "Number of employees:" DEFAULT 0
HEAR revenue AS MONEY WITH "Annual revenue:" DEFAULT 0
HEAR description AS TEXT WITH "Description:" DEFAULT ""
HEAR parent_account AS TEXT WITH "Parent account (leave empty if none):" DEFAULT ""
account_id = "ACC-" + FORMAT(NOW(), "YYYYMMDDHHmmss") + "-" + RANDOM(1000, 9999)
created_by = GET SESSION "user_email"
account = {
"id": account_id,
"name": account_name,
"type": account_type,
"industry": industry,
"phone": phone,
"website": website,
"email": email,
"street": street,
"city": city,
"state": state,
"postal_code": postal_code,
"country": country,
"employees": employees,
"annual_revenue": revenue,
"description": description,
"parent_account_id": parent_account,
"owner_id": created_by,
"created_by": created_by,
"created_at": NOW(),
"modified_at": NOW(),
"status": "Active",
"health_score": 100
}
SAVE "accounts.csv", account
LOG ACTIVITY account_id, "Account", "Created", "Account created: " + account_name, created_by
RECORD METRIC "crm_accounts" WITH action="created", type=account_type
TALK "Account created successfully"
TALK "Account ID: " + account_id
TALK "Name: " + account_name
TALK "Type: " + account_type
HEAR add_contact AS BOOLEAN WITH "Would you like to add a primary contact?"
IF add_contact THEN
SET BOT MEMORY "current_account_id", account_id
CALL "contact-management.bas", "CREATE"
END IF
END SUB
' -----------------------------------------------------------------------------
' UPDATE ACCOUNT
' -----------------------------------------------------------------------------
SUB update_account
HEAR account_id AS TEXT WITH "Enter Account ID or search by name:"
IF LEFT(account_id, 4) <> "ACC-" THEN
accounts = FIND "accounts" WHERE name LIKE "%" + account_id + "%"
IF COUNT(accounts) = 0 THEN
TALK "No accounts found matching: " + account_id
EXIT SUB
ELSEIF COUNT(accounts) = 1 THEN
account = FIRST(accounts)
account_id = account.id
ELSE
TALK "Multiple accounts found:"
FOR EACH acc IN accounts
TALK acc.id + " - " + acc.name
NEXT
HEAR account_id AS TEXT WITH "Enter the Account ID:"
END IF
END IF
account = FIND "accounts" WHERE id = account_id
IF account IS NULL THEN
TALK "Account not found: " + account_id
EXIT SUB
END IF
TALK "Updating: " + account.name
TALK "Press Enter to keep current value"
HEAR new_name AS TEXT WITH "Company name [" + account.name + "]:" DEFAULT account.name
HEAR new_type AS TEXT WITH "Account type [" + account.type + "]:" DEFAULT account.type
HEAR new_industry AS TEXT WITH "Industry [" + account.industry + "]:" DEFAULT account.industry
HEAR new_phone AS TEXT WITH "Phone [" + account.phone + "]:" DEFAULT account.phone
HEAR new_website AS TEXT WITH "Website [" + account.website + "]:" DEFAULT account.website
HEAR new_email AS TEXT WITH "Email [" + account.email + "]:" DEFAULT account.email
HEAR new_employees AS INTEGER WITH "Employees [" + account.employees + "]:" DEFAULT account.employees
HEAR new_revenue AS MONEY WITH "Annual revenue [" + account.annual_revenue + "]:" DEFAULT account.annual_revenue
HEAR new_status AS TEXT WITH "Status [" + account.status + "] (Active, Inactive, On Hold):" DEFAULT account.status
UPDATE "accounts" SET
name = new_name,
type = new_type,
industry = new_industry,
phone = new_phone,
website = new_website,
email = new_email,
employees = new_employees,
annual_revenue = new_revenue,
status = new_status,
modified_at = NOW(),
modified_by = GET SESSION "user_email"
WHERE id = account_id
LOG ACTIVITY account_id, "Account", "Updated", "Account updated", GET SESSION "user_email"
TALK "Account updated successfully"
END SUB
' -----------------------------------------------------------------------------
' VIEW ACCOUNT (360-degree view)
' -----------------------------------------------------------------------------
SUB view_account
HEAR account_id AS TEXT WITH "Enter Account ID:"
account = FIND "accounts" WHERE id = account_id
IF account IS NULL THEN
TALK "Account not found"
EXIT SUB
END IF
TALK "Account Details"
TALK "Name: " + account.name
TALK "ID: " + account.id
TALK "Type: " + account.type
TALK "Industry: " + account.industry
TALK "Status: " + account.status
TALK ""
TALK "Contact Information"
TALK "Phone: " + account.phone
TALK "Email: " + account.email
TALK "Website: " + account.website
TALK ""
TALK "Address"
TALK account.street
TALK account.city + ", " + account.state + " " + account.postal_code
TALK account.country
TALK ""
TALK "Business Information"
TALK "Employees: " + FORMAT(account.employees, "#,###")
TALK "Annual Revenue: " + FORMAT(account.annual_revenue, "$#,###")
TALK "Health Score: " + account.health_score + "/100"
TALK ""
TALK "System Information"
TALK "Owner: " + account.owner_id
TALK "Created: " + FORMAT(account.created_at, "YYYY-MM-DD")
TALK "Modified: " + FORMAT(account.modified_at, "YYYY-MM-DD")
contacts = FIND "contacts" WHERE account_id = account_id
opportunities = FIND "opportunities" WHERE account_id = account_id
cases = FIND "cases" WHERE account_id = account_id
TALK ""
TALK "Related Records"
TALK "Contacts: " + COUNT(contacts)
TALK "Opportunities: " + COUNT(opportunities)
TALK "Cases: " + COUNT(cases)
open_opportunities = FILTER(opportunities, "status <> 'Closed Won' AND status <> 'Closed Lost'")
total_pipeline = SUM(open_opportunities, "estimated_value")
TALK "Pipeline Value: " + FORMAT(total_pipeline, "$#,###")
won_opportunities = FILTER(opportunities, "status = 'Closed Won'")
total_revenue = SUM(won_opportunities, "actual_value")
TALK "Lifetime Revenue: " + FORMAT(total_revenue, "$#,###")
END SUB
' -----------------------------------------------------------------------------
' LIST ACCOUNTS
' -----------------------------------------------------------------------------
SUB list_accounts
TALK "List Accounts"
TALK "Filter by:"
TALK "1. All Active"
TALK "2. By Type"
TALK "3. By Industry"
TALK "4. By Owner"
TALK "5. Recently Modified"
HEAR filter_choice AS INTEGER
SELECT CASE filter_choice
CASE 1
accounts = FIND "accounts" WHERE status = "Active" ORDER BY name
CASE 2
HEAR filter_type AS TEXT WITH "Account type (Customer, Partner, Vendor, Competitor):"
accounts = FIND "accounts" WHERE type = filter_type AND status = "Active" ORDER BY name
CASE 3
HEAR filter_industry AS TEXT WITH "Industry:"
accounts = FIND "accounts" WHERE industry = filter_industry AND status = "Active" ORDER BY name
CASE 4
HEAR filter_owner AS TEXT WITH "Owner email:"
accounts = FIND "accounts" WHERE owner_id = filter_owner AND status = "Active" ORDER BY name
CASE 5
accounts = FIND "accounts" WHERE modified_at >= DATEADD(NOW(), -7, "day") ORDER BY modified_at DESC
CASE ELSE
accounts = FIND "accounts" WHERE status = "Active" ORDER BY name LIMIT 20
END SELECT
IF COUNT(accounts) = 0 THEN
TALK "No accounts found"
EXIT SUB
END IF
TALK "Found " + COUNT(accounts) + " accounts:"
TALK ""
FOR EACH acc IN accounts
TALK acc.id + " | " + acc.name + " | " + acc.type + " | " + acc.industry
NEXT
END SUB
' -----------------------------------------------------------------------------
' SEARCH ACCOUNTS
' -----------------------------------------------------------------------------
SUB search_accounts
HEAR search_term AS TEXT WITH "Search accounts (name, email, phone, or website):"
accounts = FIND "accounts" WHERE
name LIKE "%" + search_term + "%" OR
email LIKE "%" + search_term + "%" OR
phone LIKE "%" + search_term + "%" OR
website LIKE "%" + search_term + "%"
IF COUNT(accounts) = 0 THEN
TALK "No accounts found for: " + search_term
EXIT SUB
END IF
TALK "Found " + COUNT(accounts) + " matching accounts:"
FOR EACH acc IN accounts
TALK acc.id + " - " + acc.name + " (" + acc.type + ")"
TALK " Phone: " + acc.phone + " | Email: " + acc.email
NEXT
END SUB
' -----------------------------------------------------------------------------
' ACCOUNT HIERARCHY
' -----------------------------------------------------------------------------
SUB account_hierarchy
HEAR account_id AS TEXT WITH "Enter Account ID to view hierarchy:"
account = FIND "accounts" WHERE id = account_id
IF account IS NULL THEN
TALK "Account not found"
EXIT SUB
END IF
TALK "Account Hierarchy for: " + account.name
TALK ""
IF account.parent_account_id <> "" THEN
parent = FIND "accounts" WHERE id = account.parent_account_id
IF parent IS NOT NULL THEN
TALK "Parent Account:"
TALK " " + parent.name + " (" + parent.id + ")"
END IF
END IF
TALK ""
TALK "Current Account:"
TALK " " + account.name + " (" + account.id + ")"
children = FIND "accounts" WHERE parent_account_id = account_id
IF COUNT(children) > 0 THEN
TALK ""
TALK "Child Accounts:"
FOR EACH child IN children
TALK " - " + child.name + " (" + child.id + ")"
NEXT
END IF
END SUB
' -----------------------------------------------------------------------------
' ACCOUNT CONTACTS
' -----------------------------------------------------------------------------
SUB account_contacts
HEAR account_id AS TEXT WITH "Enter Account ID:"
account = FIND "accounts" WHERE id = account_id
IF account IS NULL THEN
TALK "Account not found"
EXIT SUB
END IF
contacts = FIND "contacts" WHERE account_id = account_id ORDER BY is_primary DESC, last_name
TALK "Contacts for: " + account.name
TALK "Total: " + COUNT(contacts)
TALK ""
IF COUNT(contacts) = 0 THEN
TALK "No contacts associated with this account"
HEAR add_new AS BOOLEAN WITH "Would you like to add a contact?"
IF add_new THEN
SET BOT MEMORY "current_account_id", account_id
CALL "contact-management.bas", "CREATE"
END IF
EXIT SUB
END IF
FOR EACH contact IN contacts
primary_marker = ""
IF contact.is_primary THEN
primary_marker = " [PRIMARY]"
END IF
TALK contact.first_name + " " + contact.last_name + primary_marker
TALK " Title: " + contact.job_title
TALK " Email: " + contact.email
TALK " Phone: " + contact.phone
TALK ""
NEXT
END SUB
' -----------------------------------------------------------------------------
' ACCOUNT ACTIVITIES
' -----------------------------------------------------------------------------
SUB account_activities
HEAR account_id AS TEXT WITH "Enter Account ID:"
account = FIND "accounts" WHERE id = account_id
IF account IS NULL THEN
TALK "Account not found"
EXIT SUB
END IF
activities = FIND "activities" WHERE related_to = account_id ORDER BY activity_date DESC LIMIT 20
TALK "Recent Activities for: " + account.name
TALK ""
IF COUNT(activities) = 0 THEN
TALK "No activities recorded"
EXIT SUB
END IF
FOR EACH activity IN activities
TALK FORMAT(activity.activity_date, "YYYY-MM-DD HH:mm") + " | " + activity.activity_type
TALK " " + activity.subject
TALK " By: " + activity.created_by
TALK ""
NEXT
END SUB
' -----------------------------------------------------------------------------
' ACCOUNT HEALTH SCORE
' -----------------------------------------------------------------------------
SUB account_health
HEAR account_id AS TEXT WITH "Enter Account ID:"
account = FIND "accounts" WHERE id = account_id
IF account IS NULL THEN
TALK "Account not found"
EXIT SUB
END IF
contacts = FIND "contacts" WHERE account_id = account_id
opportunities = FIND "opportunities" WHERE account_id = account_id
activities = FIND "activities" WHERE related_to = account_id AND activity_date >= DATEADD(NOW(), -90, "day")
cases = FIND "cases" WHERE account_id = account_id AND status = "Open"
health_score = 100
health_factors = []
IF COUNT(contacts) = 0 THEN
health_score = health_score - 20
PUSH health_factors, "No contacts (-20)"
END IF
recent_activities = FILTER(activities, "activity_date >= " + DATEADD(NOW(), -30, "day"))
IF COUNT(recent_activities) = 0 THEN
health_score = health_score - 15
PUSH health_factors, "No recent activity (-15)"
END IF
IF COUNT(cases) > 3 THEN
health_score = health_score - 10
PUSH health_factors, "Multiple open cases (-10)"
END IF
open_opps = FILTER(opportunities, "status <> 'Closed Won' AND status <> 'Closed Lost'")
IF COUNT(open_opps) > 0 THEN
health_score = health_score + 10
PUSH health_factors, "Active opportunities (+10)"
END IF
won_opps = FILTER(opportunities, "status = 'Closed Won' AND close_date >= " + DATEADD(NOW(), -365, "day"))
IF COUNT(won_opps) > 0 THEN
health_score = health_score + 15
PUSH health_factors, "Recent closed deals (+15)"
END IF
IF health_score > 100 THEN health_score = 100
IF health_score < 0 THEN health_score = 0
UPDATE "accounts" SET health_score = health_score, modified_at = NOW() WHERE id = account_id
TALK "Account Health Assessment"
TALK "Account: " + account.name
TALK ""
TALK "Health Score: " + health_score + "/100"
TALK ""
TALK "Factors:"
FOR EACH factor IN health_factors
TALK " - " + factor
NEXT
TALK ""
TALK "Statistics:"
TALK " Contacts: " + COUNT(contacts)
TALK " Activities (90 days): " + COUNT(activities)
TALK " Open Cases: " + COUNT(cases)
TALK " Open Opportunities: " + COUNT(open_opps)
END SUB

View file

@ -0,0 +1,148 @@
' =============================================================================
' Activity Tracking Dialog - CRM Template
' Microsoft Dynamics CRM-style Activity Management
' =============================================================================
' This dialog handles logging and tracking of customer activities:
' - Phone Calls
' - Emails
' - Meetings/Appointments
' - Tasks
' - Notes
' =============================================================================
TALK "Activity Tracking - Log and manage customer interactions"
HEAR activity_type AS TEXT WITH "Select activity type: (call, email, meeting, task, note)"
SELECT CASE LCASE(activity_type)
CASE "call", "phone", "1"
CALL "log-call.bas"
CASE "email", "2"
CALL "log-email-activity.bas"
CASE "meeting", "appointment", "3"
CALL "log-meeting.bas"
CASE "task", "4"
CALL "log-task.bas"
CASE "note", "5"
CALL "log-note.bas"
CASE ELSE
TALK "I will help you log a general activity."
HEAR regarding_type AS TEXT WITH "What does this activity relate to? (lead, contact, account, opportunity)"
HEAR regarding_id AS TEXT WITH "Enter the record ID or name:"
SELECT CASE LCASE(regarding_type)
CASE "lead"
record = FIND "leads" WHERE id = regarding_id OR name LIKE regarding_id FIRST
CASE "contact"
record = FIND "contacts" WHERE id = regarding_id OR full_name LIKE regarding_id FIRST
CASE "account"
record = FIND "accounts" WHERE id = regarding_id OR name LIKE regarding_id FIRST
CASE "opportunity"
record = FIND "opportunities" WHERE id = regarding_id OR name LIKE regarding_id FIRST
END SELECT
IF record IS NULL THEN
TALK "Record not found. Please verify the ID or name."
EXIT
END IF
TALK "Record found: " + record.name
HEAR subject AS TEXT WITH "Activity subject:"
HEAR description AS TEXT WITH "Activity description (details of the interaction):"
HEAR duration AS INTEGER WITH "Duration in minutes:" DEFAULT 30
HEAR outcome AS TEXT WITH "Outcome (completed, pending, cancelled):" DEFAULT "completed"
activity_id = "ACT-" + FORMAT(NOW(), "YYYYMMDDHHmmss") + "-" + RANDOM(1000, 9999)
INSERT INTO "activities" VALUES {
"id": activity_id,
"activity_type": "general",
"subject": subject,
"description": description,
"regarding_type": regarding_type,
"regarding_id": record.id,
"regarding_name": record.name,
"duration_minutes": duration,
"outcome": outcome,
"status": "completed",
"owner_id": GET SESSION "user_id",
"owner_name": GET SESSION "user_name",
"created_at": NOW(),
"activity_date": NOW()
}
RECORD METRIC "crm_activities" WITH activity_type = "general", outcome = outcome
TALK "Activity logged successfully."
TALK "Activity ID: " + activity_id
END SELECT
' Show recent activities for context
HEAR show_recent AS BOOLEAN WITH "Would you like to see recent activities?"
IF show_recent THEN
HEAR filter_type AS TEXT WITH "Filter by: (all, lead, contact, account, opportunity)" DEFAULT "all"
IF filter_type = "all" THEN
recent = FIND "activities" ORDER BY activity_date DESC LIMIT 10
ELSE
recent = FIND "activities" WHERE regarding_type = filter_type ORDER BY activity_date DESC LIMIT 10
END IF
IF COUNT(recent) = 0 THEN
TALK "No recent activities found."
ELSE
TALK "Recent Activities:"
TALK ""
FOR EACH act IN recent
date_str = FORMAT(act.activity_date, "MMM DD, YYYY HH:mm")
TALK act.activity_type + " - " + act.subject
TALK " Regarding: " + act.regarding_name + " (" + act.regarding_type + ")"
TALK " Date: " + date_str + " | Duration: " + act.duration_minutes + " min"
TALK " Status: " + act.status
TALK ""
NEXT
END IF
END IF
' Activity analytics summary
HEAR show_summary AS BOOLEAN WITH "Would you like to see activity summary?"
IF show_summary THEN
HEAR summary_period AS TEXT WITH "Period: (today, week, month)" DEFAULT "week"
SELECT CASE summary_period
CASE "today"
start_date = TODAY()
CASE "week"
start_date = DATEADD(TODAY(), -7, "day")
CASE "month"
start_date = DATEADD(TODAY(), -30, "day")
END SELECT
activities = FIND "activities" WHERE activity_date >= start_date
call_count = COUNT(FILTER(activities, "activity_type = 'call'"))
email_count = COUNT(FILTER(activities, "activity_type = 'email'"))
meeting_count = COUNT(FILTER(activities, "activity_type = 'meeting'"))
task_count = COUNT(FILTER(activities, "activity_type = 'task'"))
total_duration = SUM(activities, "duration_minutes")
TALK "Activity Summary for " + summary_period
TALK ""
TALK "Calls: " + call_count
TALK "Emails: " + email_count
TALK "Meetings: " + meeting_count
TALK "Tasks: " + task_count
TALK ""
TALK "Total Activities: " + COUNT(activities)
TALK "Total Time: " + FORMAT(total_duration / 60, "#.#") + " hours"
END IF

Some files were not shown because too many files have changed in this diff Show more