, 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:
parent
635f3a7923
commit
48c1ae0b51
132 changed files with 27274 additions and 4858 deletions
|
|
@ -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"]
|
||||
|
||||
|
|
|
|||
788
docs/HEAR_VALIDATION_REFERENCE.md
Normal file
788
docs/HEAR_VALIDATION_REFERENCE.md
Normal 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.*
|
||||
3550
docs/MULTI_AGENT_OFFICE_SUITE.md
Normal file
3550
docs/MULTI_AGENT_OFFICE_SUITE.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
# DELETE_FILE
|
||||
# DELETE FILE
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
# DELETE_HTTP
|
||||
# DELETE HTTP
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
# GENERATE_PDF
|
||||
# GENERATE PDF
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
# GROUP_BY
|
||||
# GROUP BY
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
# MERGE_PDF
|
||||
# MERGE PDF
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
# SET_HEADER
|
||||
# SET HEADER
|
||||
|
|
|
|||
123
docs/src/chapter-06-gbdialog/keyword-synchronize.md
Normal file
123
docs/src/chapter-06-gbdialog/keyword-synchronize.md
Normal 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
|
||||
223
docs/src/chapter-06-gbdialog/keyword-table.md
Normal file
223
docs/src/chapter-06-gbdialog/keyword-table.md
Normal 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
|
||||
|
|
@ -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
|
||||
476
docs/src/chapter-06-gbdialog/prompt-blocks.md
Normal file
476
docs/src/chapter-06-gbdialog/prompt-blocks.md
Normal 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
|
||||
508
docs/src/chapter-06-gbdialog/script-execution-flow.md
Normal file
508
docs/src/chapter-06-gbdialog/script-execution-flow.md
Normal 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
|
||||
243
docs/src/chapter-09-api/llm-rest-server.md
Normal file
243
docs/src/chapter-09-api/llm-rest-server.md
Normal 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
|
||||
|
|
@ -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) | – |
|
||||
| Knowledge‑base 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 |
|
||||
| Built‑in 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
|
||||
375
docs/src/chapter-11-features/editions.md
Normal file
375
docs/src/chapter-11-features/editions.md
Normal 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)
|
||||
|
|
@ -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.
|
||||
15
migrations/2025-01-20-000001_multi_agent_bots/down.sql
Normal file
15
migrations/2025-01-20-000001_multi_agent_bots/down.sql
Normal 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;
|
||||
226
migrations/2025-01-20-000001_multi_agent_bots/up.sql
Normal file
226
migrations/2025-01-20-000001_multi_agent_bots/up.sql
Normal 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();
|
||||
22
migrations/6.1.0_table_keyword/down.sql
Normal file
22
migrations/6.1.0_table_keyword/down.sql
Normal 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;
|
||||
120
migrations/6.1.0_table_keyword/up.sql
Normal file
120
migrations/6.1.0_table_keyword/up.sql
Normal 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();
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
889
src/basic/keywords/add_bot.rs
Normal file
889
src/basic/keywords/add_bot.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
757
src/basic/keywords/play.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
763
src/basic/keywords/table_definition.rs
Normal file
763
src/basic/keywords/table_definition.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
394
src/basic/mod.rs
394
src/basic/mod.rs
|
|
@ -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(¤t_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(¤t_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(¤t_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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
670
src/timeseries/mod.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
685
templates/bank.gbai/start.bas
Normal file
685
templates/bank.gbai/start.bas
Normal 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
|
||||
|
|
@ -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>
|
||||
118
templates/bling.gbai/bling.gbdialog/README.md
Normal file
118
templates/bling.gbai/bling.gbdialog/README.md
Normal 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)
|
||||
32
templates/bling.gbai/bling.gbdialog/add-stock.bas
Normal file
32
templates/bling.gbai/bling.gbdialog/add-stock.bas
Normal 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}."
|
||||
38
templates/bling.gbai/bling.gbdialog/data-analysis.bas
Normal file
38
templates/bling.gbai/bling.gbdialog/data-analysis.bas
Normal 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
|
||||
2
templates/bling.gbai/bling.gbdialog/refresh-llm.bas
Normal file
2
templates/bling.gbai/bling.gbdialog/refresh-llm.bas
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
SET SCHEDULE "0 0 21 * * *"
|
||||
REFRESH "data-analysis"
|
||||
16
templates/bling.gbai/bling.gbdialog/start.bas
Normal file
16
templates/bling.gbai/bling.gbdialog/start.bas
Normal 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
|
||||
92
templates/bling.gbai/bling.gbdialog/sync-accounts.bas
Normal file
92
templates/bling.gbai/bling.gbdialog/sync-accounts.bas
Normal 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."
|
||||
333
templates/bling.gbai/bling.gbdialog/sync-erp.bas
Normal file
333
templates/bling.gbai/bling.gbdialog/sync-erp.bas
Normal 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."
|
||||
66
templates/bling.gbai/bling.gbdialog/sync-inventory.bas
Normal file
66
templates/bling.gbai/bling.gbdialog/sync-inventory.bas
Normal 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."
|
||||
73
templates/bling.gbai/bling.gbdialog/sync-suppliers.bas
Normal file
73
templates/bling.gbai/bling.gbdialog/sync-suppliers.bas
Normal 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."
|
||||
284
templates/bling.gbai/bling.gbdialog/tables.bas
Normal file
284
templates/bling.gbai/bling.gbdialog/tables.bas
Normal 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
|
||||
25
templates/bling.gbai/bling.gbot/config.csv
Normal file
25
templates/bling.gbai/bling.gbot/config.csv
Normal 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,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 %}
|
||||
174
templates/compliance/hipaa-medical.gbai/README.md
Normal file
174
templates/compliance/hipaa-medical.gbai/README.md
Normal 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
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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.
|
200
templates/compliance/privacy.gbai/README.md
Normal file
200
templates/compliance/privacy.gbai/README.md
Normal 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.
|
||||
|
|
@ -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."
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
39
templates/compliance/privacy.gbai/privacy.gbdialog/start.bas
Normal file
39
templates/compliance/privacy.gbai/privacy.gbdialog/start.bas
Normal 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
|
||||
44
templates/compliance/privacy.gbai/privacy.gbot/config.csv
Normal file
44
templates/compliance/privacy.gbai/privacy.gbot/config.csv
Normal 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.
|
913
templates/compliance/privacy.gbai/privacy.gbui/index.html
Normal file
913
templates/compliance/privacy.gbai/privacy.gbui/index.html
Normal 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')">×</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')">×</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')">×</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')">×</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')">×</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')">×</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>
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 + "'"
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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."
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
39
templates/platform/analytics.gbai/analytics.gbot/config.csv
Normal file
39
templates/platform/analytics.gbai/analytics.gbot/config.csv
Normal 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
|
||||
|
480
templates/sales/crm.gbai/crm.gbdialog/account-management.bas
Normal file
480
templates/sales/crm.gbai/crm.gbdialog/account-management.bas
Normal 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
|
||||
148
templates/sales/crm.gbai/crm.gbdialog/activity-tracking.bas
Normal file
148
templates/sales/crm.gbai/crm.gbdialog/activity-tracking.bas
Normal 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
Loading…
Add table
Reference in a new issue