Add tar/flate2 deps and document new BASIC keywords

Add flate2 and tar dependencies for archive extraction support in file
operations. Update documentation with:

- New BASIC keywords: SWITCH/CASE, WEBHOOK, INSTR, IS_NUMERIC
- HTTP operations: POST, PUT, PATCH, DELETE_HTTP, GRAPHQL, SOAP
- Data operations: SAVE, INSERT, UPDATE, DELETE, MERGE, FILTER, etc.
- File operations: READ, WRITE, COMPRESS, EXTRACT, GENERATE_PDF, etc.

Simplify README and add appendices for external services and environment
variables. Add monitoring dashboard and player UI docs.
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-11-30 07:53:58 -03:00
parent 46d6ff6268
commit e5ff51de76
62 changed files with 10734 additions and 317 deletions

36
Cargo.lock generated
View file

@ -1326,6 +1326,7 @@ dependencies = [
"dotenvy",
"downloader",
"env_logger",
"flate2",
"futures",
"futures-util",
"hex",
@ -1365,6 +1366,7 @@ dependencies = [
"sha2",
"smartstring",
"sysinfo",
"tar",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
@ -3086,6 +3088,18 @@ dependencies = [
"rustc_version",
]
[[package]]
name = "filetime"
version = "0.2.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.60.2",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.5"
@ -4635,6 +4649,7 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags 2.10.0",
"libc",
"redox_syscall",
]
[[package]]
@ -8375,6 +8390,17 @@ dependencies = [
"syn 2.0.110",
]
[[package]]
name = "tar"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]]
name = "target-lexicon"
version = "0.12.16"
@ -10676,6 +10702,16 @@ dependencies = [
"time",
]
[[package]]
name = "xattr"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
"rustix 1.1.2",
]
[[package]]
name = "xml-rs"
version = "0.8.28"

View file

@ -60,7 +60,7 @@ msteams = []
# ===== PRODUCTIVITY FEATURES =====
chat = []
drive = ["dep:aws-config", "dep:aws-sdk-s3", "dep:pdf-extract", "dep:zip", "dep:downloader", "dep:mime_guess"]
drive = ["dep:aws-config", "dep:aws-sdk-s3", "dep:pdf-extract", "dep:zip", "dep:downloader", "dep:mime_guess", "dep:flate2", "dep:tar"]
tasks = ["dep:cron"]
calendar = []
meet = ["dep:livekit"]
@ -185,6 +185,8 @@ pdf-extract = { version = "0.10.0", optional = true }
zip = { version = "2.2", optional = true }
downloader = { version = "0.2", optional = true }
mime_guess = { version = "2.0", optional = true }
flate2 = { version = "1.0", optional = true }
tar = { version = "0.4", optional = true }
# Task Management (tasks feature)
cron = { version = "0.15.0", optional = true }

144
README.md
View file

@ -4,51 +4,47 @@
**A strongly-typed LLM conversational platform focused on convention over configuration and code-less approaches.**
## 🚀 Quick Links
## Quick Links
- **[Complete Documentation](docs/INDEX.md)** - Full documentation index
- **[Complete Documentation](docs/src/SUMMARY.md)** - Full documentation index
- **[Quick Start Guide](docs/QUICK_START.md)** - Get started in minutes
- **[Current Status](docs/07-STATUS.md)** - Production readiness (v6.0.8)
- **[Changelog](CHANGELOG.md)** - Version history
## 📚 Documentation Structure
## Documentation Structure
All documentation has been organized into the **[docs/](docs/)** directory:
### Core Documentation (Numbered Chapters)
- **[Chapter 0: Introduction & Getting Started](docs/00-README.md)**
- **[Chapter 1: Build & Development Status](docs/01-BUILD_STATUS.md)**
- **[Chapter 2: Code of Conduct](docs/02-CODE_OF_CONDUCT.md)**
- **[Chapter 3: Código de Conduta (PT-BR)](docs/03-CODE_OF_CONDUCT-pt-br.md)**
- **[Chapter 4: Contributing Guidelines](docs/04-CONTRIBUTING.md)**
- **[Chapter 5: Integration Status](docs/05-INTEGRATION_STATUS.md)**
- **[Chapter 6: Security Policy](docs/06-SECURITY.md)**
- **[Chapter 7: Production Status](docs/07-STATUS.md)**
### Core Documentation
- **[Introduction & Getting Started](docs/src/chapter-01/README.md)**
- **[Package System](docs/src/chapter-02/README.md)**
- **[Knowledge Base Reference](docs/src/chapter-03/README.md)**
- **[User Interface](docs/src/chapter-04-gbui/README.md)**
- **[BASIC Dialogs](docs/src/chapter-06-gbdialog/README.md)**
- **[Architecture Reference](docs/src/chapter-07-gbapp/README.md)**
- **[Configuration](docs/src/chapter-08-config/README.md)**
- **[REST API Reference](docs/src/chapter-10-api/README.md)**
- **[Security & Authentication](docs/src/chapter-12-auth/README.md)**
### Technical Documentation
- **[KB & Tools System](docs/KB_AND_TOOLS.md)** - Core system architecture
- **[Security Features](docs/SECURITY_FEATURES.md)** - Security implementation
- **[Semantic Cache](docs/SEMANTIC_CACHE.md)** - LLM caching with 70% cost reduction
- **[SMB Deployment](docs/SMB_DEPLOYMENT_GUIDE.md)** - Small business deployment guide
- **[Universal Messaging](docs/BASIC_UNIVERSAL_MESSAGING.md)** - Multi-channel communication
### Technical References
- **[KB & Tools System](docs/src/chapter-03/kb-and-tools.md)** - Core system architecture
- **[Semantic Cache](docs/src/chapter-03/caching.md)** - LLM caching with 70% cost reduction
- **[Universal Messaging](docs/src/chapter-06-gbdialog/universal-messaging.md)** - Multi-channel communication
- **[External Services](docs/src/appendix-external-services/README.md)** - Service integrations
### Book-Style Documentation
- **[Detailed Docs](docs/src/)** - Comprehensive book-format documentation
## 🎯 What is General Bots?
## What is General Bots?
General Bots is a **self-hosted AI automation platform** that provides:
- **Multi-Vendor LLM API** - Unified interface for OpenAI, Groq, Claude, Anthropic
- **MCP + LLM Tools Generation** - Instant tool creation from code/functions
- **Semantic Caching** - Intelligent response caching (70% cost reduction)
- **Web Automation Engine** - Browser automation + AI intelligence
- **External Data APIs** - Integrated services via connectors
- **Enterprise Data Connectors** - CRM, ERP, database native integrations
- **Git-like Version Control** - Full history with rollback capabilities
- **Contract Analysis** - Legal document review and summary
- **Multi-Vendor LLM API** - Unified interface for OpenAI, Groq, Claude, Anthropic
- **MCP + LLM Tools Generation** - Instant tool creation from code/functions
- **Semantic Caching** - Intelligent response caching (70% cost reduction)
- **Web Automation Engine** - Browser automation + AI intelligence
- **External Data APIs** - Integrated services via connectors
- **Enterprise Data Connectors** - CRM, ERP, database native integrations
- **Git-like Version Control** - Full history with rollback capabilities
- **Contract Analysis** - Legal document review and summary
## 🎮 Command-Line Options
## Command-Line Options
```bash
# Run with default settings (console UI enabled)
@ -66,7 +62,7 @@ cargo run -- --noui
# Specify tenant
cargo run -- --tenant <tenant_name>
# Container mode
# LXC container mode
cargo run -- --container
```
@ -78,16 +74,16 @@ cargo run -- --container
- The HTTP server always runs on port 8080 unless in desktop mode
## 🏆 Key Features
## Key Features
### 4 Essential Keywords
General Bots provides a minimal, focused system for managing Knowledge Bases and Tools:
```basic
USE KB "kb-name" # Load knowledge base into vector database
CLEAR KB "kb-name" # Remove KB from session
USE TOOL "tool-name" # Make tool available to LLM
CLEAR TOOLS # Remove all tools from session
USE KB "kb-name" ' Load knowledge base into vector database
CLEAR KB "kb-name" ' Remove KB from session
USE TOOL "tool-name" ' Make tool available to LLM
CLEAR TOOLS ' Remove all tools from session
```
### Strategic Advantages
@ -96,7 +92,7 @@ CLEAR TOOLS # Remove all tools from session
- **vs Microsoft 365**: User control, not locked systems
- **vs Salesforce**: Open-source AI orchestration connecting all systems
## 🚀 Quick Start
## Quick Start
### Prerequisites
- **Rust** (latest stable) - [Install from rustup.rs](https://rustup.rs/)
@ -114,7 +110,7 @@ cargo run
```
On first run, BotServer automatically:
- Installs required components (PostgreSQL, MinIO, Redis, LLM)
- Installs required components (PostgreSQL, S3-compatible storage, Cache, LLM)
- Sets up database with migrations
- Downloads AI models
- Uploads template bots
@ -129,30 +125,52 @@ botserver list # List available components
botserver status <component> # Check component status
```
## 📊 Current Status
## Current Status
**Version:** 6.0.8
**Build Status:** SUCCESS
**Build Status:** SUCCESS
**Production Ready:** YES
**Compilation:** 0 errors
**Warnings:** 82 (all Tauri desktop UI - intentional)
See **[docs/07-STATUS.md](docs/07-STATUS.md)** for detailed status.
## Deployment
## 🤝 Contributing
General Bots supports deployment via **LXC containers** for isolated, lightweight virtualization:
```bash
# Deploy with LXC container isolation
cargo run -- --container
```
See [Container Deployment](docs/src/chapter-07-gbapp/containers.md) for detailed LXC setup instructions.
## Environment Variables
General Bots uses minimal environment configuration. Only Directory service variables are required:
| Variable | Purpose |
|----------|---------|
| `DIRECTORY_URL` | Zitadel instance URL |
| `DIRECTORY_CLIENT_ID` | OAuth client ID |
| `DIRECTORY_CLIENT_SECRET` | OAuth client secret |
All service credentials (database, storage, cache) are managed automatically by the Directory service. Application configuration is done through `config.csv` files in each bot's `.gbot` folder.
See [Environment Variables](docs/src/appendix-env-vars/README.md) for details.
## Contributing
We welcome contributions! Please read:
- **[Contributing Guidelines](docs/04-CONTRIBUTING.md)**
- **[Code of Conduct](docs/02-CODE_OF_CONDUCT.md)**
- **[Build Status](docs/01-BUILD_STATUS.md)** for current development status
- **[Contributing Guidelines](docs/src/chapter-13-community/README.md)**
- **[Development Setup](docs/src/chapter-13-community/setup.md)**
- **[Testing Guide](docs/src/chapter-13-community/testing.md)**
## 🔒 Security
## Security
Security issues should be reported to: **security@pragmatismo.com.br**
See **[docs/06-SECURITY.md](docs/06-SECURITY.md)** for our security policy.
See **[Security Policy](docs/src/chapter-12-auth/security-policy.md)** for our security guidelines.
## 📄 License
## License
General Bot Copyright (c) pragmatismo.com.br. All rights reserved.
Licensed under the **AGPL-3.0**.
@ -161,24 +179,24 @@ According to our dual licensing model, this program can be used either under the
See [LICENSE](LICENSE) for details.
## 🌟 Key Facts
## Key Facts
- LLM Orchestrator AGPL licensed (contribute back for custom-label SaaS)
- True community governance
- No single corporate control
- 5+ years of stability
- Never changed license
- Enterprise-grade
- Hosted locally or multicloud
- LLM Orchestrator AGPL licensed (contribute back for custom-label SaaS)
- True community governance
- No single corporate control
- 5+ years of stability
- Never changed license
- Enterprise-grade
- Hosted locally or multicloud
## 📞 Support & Resources
## Support & Resources
- **Documentation:** [docs.pragmatismo.com.br](https://docs.pragmatismo.com.br)
- **GitHub:** [github.com/GeneralBots/BotServer](https://github.com/GeneralBots/BotServer)
- **Stack Overflow:** Tag questions with `generalbots`
- **Video Tutorial:** [7 AI General Bots LLM Templates](https://www.youtube.com/watch?v=KJgvUPXi3Fw)
## 🎬 Demo
## Demo
See conversational data analytics in action:
@ -190,7 +208,7 @@ img = CHART "bar", data
SEND FILE img
```
## 👥 Contributors
## Contributors
<a href="https://github.com/generalbots/botserver/graphs/contributors">
<img src="https://contrib.rocks/image?repo=generalbots/botserver" />
@ -202,4 +220,4 @@ SEND FILE img
> "No one should have to do work that can be done by a machine." - Roberto Mangabeira Unger
<a href="https://stackoverflow.com/questions/ask?tags=generalbots">:speech_balloon: Ask a question</a> &nbsp;&nbsp;&nbsp;&nbsp; <a href="https://github.com/GeneralBots/BotBook">:book: Read the Docs</a>
<a href="https://stackoverflow.com/questions/ask?tags=generalbots">Ask a question</a> | <a href="https://github.com/GeneralBots/BotBook">Read the Docs</a>

View file

@ -1,6 +1,6 @@
# Summary
[🚀 Executive Vision](./executive-vision.md)
[Executive Vision](./executive-vision.md)
[Introduction](./introduction.md)
# Part I - Getting Started
@ -39,6 +39,8 @@
- [default.gbui - Full Desktop](./chapter-04-gbui/default-gbui.md)
- [single.gbui - Simple Chat](./chapter-04-gbui/single-gbui.md)
- [Console Mode](./chapter-04-gbui/console-mode.md)
- [Player - Media Viewer](./chapter-04-gbui/player.md)
- [Monitoring Dashboard](./chapter-04-gbui/monitoring.md)
# Part V - Themes and Styling
@ -82,6 +84,44 @@
- [EXIT FOR](./chapter-06-gbdialog/keyword-exit-for.md)
- [SEND MAIL](./chapter-06-gbdialog/keyword-send-mail.md)
- [FIND](./chapter-06-gbdialog/keyword-find.md)
- [INSTR](./chapter-06-gbdialog/keyword-instr.md)
- [IS NUMERIC](./chapter-06-gbdialog/keyword-is-numeric.md)
- [SWITCH](./chapter-06-gbdialog/keyword-switch.md)
- [WEBHOOK](./chapter-06-gbdialog/keyword-webhook.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)
- [GRAPHQL](./chapter-06-gbdialog/keyword-graphql.md)
- [SOAP](./chapter-06-gbdialog/keyword-soap.md)
- [Data Operations](./chapter-06-gbdialog/keywords-data.md)
- [SAVE](./chapter-06-gbdialog/keyword-save.md)
- [INSERT](./chapter-06-gbdialog/keyword-insert.md)
- [UPDATE](./chapter-06-gbdialog/keyword-update.md)
- [DELETE](./chapter-06-gbdialog/keyword-delete.md)
- [MERGE](./chapter-06-gbdialog/keyword-merge.md)
- [FILL](./chapter-06-gbdialog/keyword-fill.md)
- [MAP](./chapter-06-gbdialog/keyword-map.md)
- [FILTER](./chapter-06-gbdialog/keyword-filter.md)
- [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)
- [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)
- [COPY](./chapter-06-gbdialog/keyword-copy.md)
- [MOVE](./chapter-06-gbdialog/keyword-move.md)
- [LIST](./chapter-06-gbdialog/keyword-list.md)
- [COMPRESS](./chapter-06-gbdialog/keyword-compress.md)
- [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)
# Part VII - Extending General Bots
@ -103,7 +143,7 @@
- [Bot Parameters](./chapter-08-config/parameters.md)
- [LLM Configuration](./chapter-08-config/llm-config.md)
- [Context Configuration](./chapter-08-config/context-config.md)
- [Drive Integration](./chapter-08-config/minio.md)
- [Drive Integration](./chapter-08-config/drive.md)
# Part IX - Tools and Integration
@ -159,7 +199,7 @@
# Part XI - Security
- [Chapter 12: Authentication](./chapter-12-auth/README.md)
- [Chapter 12: Authentication & Permissions](./chapter-12-auth/README.md)
- [User Authentication](./chapter-12-auth/user-auth.md)
- [Password Security](./chapter-12-auth/password-security.md)
- [API Endpoints](./chapter-12-auth/api-endpoints.md)
@ -167,6 +207,8 @@
- [Security Features](./chapter-12-auth/security-features.md)
- [Security Policy](./chapter-12-auth/security-policy.md)
- [Compliance Requirements](./chapter-12-auth/compliance-requirements.md)
- [Permissions Matrix](./chapter-12-auth/permissions-matrix.md)
- [User Context vs System Context](./chapter-12-auth/user-system-context.md)
# Part XII - Community
@ -191,10 +233,20 @@
# Appendices
- [Appendix XV: Database Model](./appendix-15/README.md)
- [Appendix A: Database Model](./appendix-15/README.md)
- [Schema Overview](./appendix-15/schema.md)
- [Tables](./appendix-15/tables.md)
- [Relationships](./appendix-15/relationships.md)
- [Appendix B: External Services](./appendix-external-services/README.md)
- [Service Catalog](./appendix-external-services/catalog.md)
- [LLM Providers](./appendix-external-services/llm-providers.md)
- [Weather API](./appendix-external-services/weather.md)
- [Channel Integrations](./appendix-external-services/channels.md)
- [Storage Services](./appendix-external-services/storage.md)
- [Directory Services](./appendix-external-services/directory.md)
- [Appendix C: Environment Variables](./appendix-env-vars/README.md)
[Glossary](./glossary.md)
[Contact](./contact/README.md)

View file

@ -0,0 +1,139 @@
# Appendix C: Environment Variables
General Bots uses a minimal set of environment variables. All service configuration is managed through the Directory service (Zitadel), and application settings are stored in `config.csv` files within each bot's `.gbot` folder.
## Required Environment Variables
Only one set of environment variables is used by General Bots:
### DIRECTORY_* Variables
**Purpose**: Directory service (Zitadel) configuration for identity and service management.
| Variable | Description | Example |
|----------|-------------|---------|
| `DIRECTORY_URL` | Zitadel instance URL | `http://localhost:8080` |
| `DIRECTORY_CLIENT_ID` | OAuth client ID | Auto-generated during bootstrap |
| `DIRECTORY_CLIENT_SECRET` | OAuth client secret | Auto-generated during bootstrap |
**Example**:
```bash
DIRECTORY_URL=http://localhost:8080
DIRECTORY_CLIENT_ID=your-client-id
DIRECTORY_CLIENT_SECRET=your-client-secret
```
## Auto-Managed Services
The following services are automatically configured through the Directory service:
| Service | Management |
|---------|------------|
| PostgreSQL | Connection managed via Directory |
| S3-Compatible Storage | Credentials managed via Directory |
| Cache (Valkey) | Connection managed via Directory |
| Email (Stalwart) | Accounts managed via Directory |
You do **not** need to set environment variables for these services. The Directory service handles credential distribution and rotation automatically.
## What NOT to Use Environment Variables For
**Do NOT use environment variables for**:
| Configuration | Where to Configure |
|--------------|-------------------|
| Database connection | Managed by Directory service |
| Storage credentials | Managed by Directory service |
| LLM API keys | `config.csv`: `llm-api-key` |
| LLM provider | `config.csv`: `llm-provider` |
| Email settings | Managed by Directory service |
| Channel tokens | `config.csv`: `whatsapp-api-key`, etc. |
| Bot settings | `config.csv`: all bot-specific settings |
| Weather API | `config.csv`: `weather-api-key` |
| Feature flags | `config.csv`: `enable-*` keys |
## Configuration Philosophy
General Bots follows these principles:
1. **Directory-First**: Infrastructure credentials are managed by the Directory service
2. **Minimal Environment**: Only identity provider settings use environment variables
3. **Database-Stored**: All application configuration is stored in the database via `config.csv` sync
4. **Per-Bot Configuration**: Each bot has its own `config.csv` in its `.gbot` folder
5. **No Hardcoded Defaults**: Configuration must be explicitly provided
## Setting Environment Variables
### Linux/macOS
```bash
export DIRECTORY_URL=http://localhost:8080
export DIRECTORY_CLIENT_ID=your-client-id
export DIRECTORY_CLIENT_SECRET=your-client-secret
```
### Systemd Service
```ini
[Service]
Environment="DIRECTORY_URL=http://localhost:8080"
Environment="DIRECTORY_CLIENT_ID=your-client-id"
Environment="DIRECTORY_CLIENT_SECRET=your-client-secret"
```
### LXC Container
When using LXC deployment, environment variables are set in the container configuration:
```bash
lxc config set container-name environment.DIRECTORY_URL="http://localhost:8080"
```
## Security Notes
1. **Never commit credentials**: Use `.env` files (gitignored) or secrets management
2. **Rotate regularly**: The Directory service can rotate credentials automatically
3. **Limit access**: Only the botserver process needs these variables
4. **Use TLS**: Always use HTTPS for the Directory URL in production
## Troubleshooting
### Directory Connection Failed
```
Error: Failed to connect to Directory service
```
Verify:
- `DIRECTORY_URL` is set correctly
- Zitadel is running and accessible
- Network allows connection to Directory host
- Client credentials are valid
### Service Not Available
If a managed service (database, storage, cache) is unavailable:
1. Check the Directory service is running
2. Verify service registration in Zitadel
3. Check service container/process status
4. Review logs for connection errors
## Bootstrap Process
During bootstrap, General Bots:
1. Connects to the Directory service using `DIRECTORY_*` variables
2. Registers itself as an application
3. Retrieves credentials for managed services
4. Starts services with provided credentials
5. Stores service endpoints in the database
This eliminates the need for manual credential management.
## See Also
- [config.csv Format](../chapter-08-config/config-csv.md) - Bot configuration
- [External Services](../appendix-external-services/README.md) - Service configuration
- [Drive Integration](../chapter-08-config/drive.md) - Storage setup
- [Authentication](../chapter-12-auth/README.md) - Directory service integration

View file

@ -0,0 +1,94 @@
# Appendix B: External Services
This appendix catalogs all external services that General Bots integrates with, including their configuration requirements, associated BASIC keywords, and API endpoints.
## Overview
General Bots connects to external services for extended functionality. All service credentials should be stored in `config.csv` within the bot's `.gbot` folder - never hardcoded in scripts.
Infrastructure services (database, storage, cache) are automatically managed by the Directory service (Zitadel).
## Service Categories
| Category | Services | Configuration Location |
|----------|----------|----------------------|
| LLM Providers | OpenAI, Groq, Anthropic, Azure OpenAI | `config.csv` |
| Weather | OpenWeatherMap | `config.csv` |
| Messaging Channels | WhatsApp, Teams, Instagram, Telegram | `config.csv` |
| Storage | S3-Compatible (MinIO, etc.) | Directory service (automatic) |
| Directory | Zitadel | `DIRECTORY_*` environment variables |
| Email | Stalwart / IMAP/SMTP | Directory service (automatic) |
| Calendar | CalDAV servers | `config.csv` |
| Database | PostgreSQL | Directory service (automatic) |
| Cache | Valkey | Directory service (automatic) |
## Quick Reference
### BASIC Keywords That Call External Services
| Keyword | Service | Config Key |
|---------|---------|-----------|
| `LLM` | LLM Provider | `llm-provider`, `llm-api-key` |
| `WEATHER` | OpenWeatherMap | `weather-api-key` |
| `SEND MAIL` | SMTP Server | Managed by Directory service |
| `SEND WHATSAPP` | WhatsApp Business API | `whatsapp-api-key`, `whatsapp-phone-number-id` |
| `SEND TEAMS` | Microsoft Teams | `teams-app-id`, `teams-app-password` |
| `SEND INSTAGRAM` | Instagram Graph API | `instagram-access-token`, `instagram-page-id` |
| `GET` (with http/https URL) | Any HTTP endpoint | N/A |
| `IMAGE` | BotModels (local) | `botmodels-enabled`, `botmodels-url` |
| `VIDEO` | BotModels (local) | `botmodels-enabled`, `botmodels-url` |
| `AUDIO` | BotModels (local) | `botmodels-enabled`, `botmodels-url` |
| `SEE` | BotModels (local) | `botmodels-enabled`, `botmodels-url` |
| `FIND` | Qdrant (local) | Internal service |
| `USE WEBSITE` | Web crawling | N/A |
## Service Configuration Template
Add these to your `config.csv`:
```csv
key,value
llm-provider,openai
llm-api-key,YOUR_API_KEY
llm-model,gpt-4o
weather-api-key,YOUR_OPENWEATHERMAP_KEY
whatsapp-api-key,YOUR_WHATSAPP_KEY
whatsapp-phone-number-id,YOUR_PHONE_ID
teams-app-id,YOUR_TEAMS_APP_ID
teams-app-password,YOUR_TEAMS_PASSWORD
instagram-access-token,YOUR_INSTAGRAM_TOKEN
instagram-page-id,YOUR_PAGE_ID
botmodels-enabled,true
botmodels-url,http://localhost:5000
```
## Auto-Managed Services
The following services are automatically configured by the Directory service (Zitadel):
| Service | What's Managed |
|---------|----------------|
| PostgreSQL | Connection credentials, database creation |
| S3-Compatible Storage | Access keys, bucket policies |
| Valkey Cache | Connection credentials |
| Stalwart Email | User accounts, SMTP/IMAP access |
You do **not** need to configure these services manually. The Directory service handles credential provisioning and rotation.
## Security Notes
1. **Never hardcode credentials** - Always use `config.csv` or `GET BOT MEMORY`
2. **Rotate keys regularly** - Update `config.csv` and restart the bot
3. **Use least privilege** - Only grant permissions needed by the bot
4. **Audit access** - Monitor external API usage through logs
5. **Infrastructure credentials** - Managed automatically by Directory service
## See Also
- [Service Catalog](./catalog.md) - Detailed service documentation
- [LLM Providers](./llm-providers.md) - AI model configuration
- [Weather API](./weather.md) - Weather service setup
- [Channel Integrations](./channels.md) - Messaging platform setup
- [Storage Services](./storage.md) - S3-compatible storage
- [Directory Services](./directory.md) - User authentication
- [Environment Variables](../appendix-env-vars/README.md) - DIRECTORY_* configuration

View file

@ -0,0 +1,273 @@
# Service Catalog
This catalog provides detailed information about every external service that General Bots integrates with.
## LLM Providers
### OpenAI
| Property | Value |
|----------|-------|
| **Service URL** | `https://api.openai.com/v1` |
| **Config Key** | `llm-provider=openai` |
| **API Key Config** | `llm-api-key` |
| **Documentation** | [platform.openai.com/docs](https://platform.openai.com/docs) |
| **BASIC Keywords** | `LLM` |
| **Supported Models** | `gpt-4o`, `gpt-4o-mini`, `gpt-4-turbo`, `gpt-3.5-turbo` |
### Groq
| Property | Value |
|----------|-------|
| **Service URL** | `https://api.groq.com/openai/v1` |
| **Config Key** | `llm-provider=groq` |
| **API Key Config** | `llm-api-key` |
| **Documentation** | [console.groq.com/docs](https://console.groq.com/docs) |
| **BASIC Keywords** | `LLM` |
| **Supported Models** | `llama-3.1-70b-versatile`, `llama-3.1-8b-instant`, `mixtral-8x7b-32768` |
### Anthropic
| Property | Value |
|----------|-------|
| **Service URL** | `https://api.anthropic.com/v1` |
| **Config Key** | `llm-provider=anthropic` |
| **API Key Config** | `llm-api-key` |
| **Documentation** | [docs.anthropic.com](https://docs.anthropic.com) |
| **BASIC Keywords** | `LLM` |
| **Supported Models** | `claude-3-5-sonnet`, `claude-3-opus`, `claude-3-haiku` |
### Azure OpenAI
| Property | Value |
|----------|-------|
| **Service URL** | `https://{resource}.openai.azure.com/` |
| **Config Key** | `llm-provider=azure` |
| **API Key Config** | `llm-api-key`, `azure-openai-endpoint` |
| **Documentation** | [learn.microsoft.com/azure/ai-services/openai](https://learn.microsoft.com/azure/ai-services/openai) |
| **BASIC Keywords** | `LLM` |
---
## Weather Services
### OpenWeatherMap
| Property | Value |
|----------|-------|
| **Service URL** | `https://api.openweathermap.org/data/2.5` |
| **Config Key** | `weather-api-key` |
| **Documentation** | [openweathermap.org/api](https://openweathermap.org/api) |
| **BASIC Keywords** | `WEATHER` |
| **Free Tier** | 1,000 calls/day |
| **Required Plan** | Free or higher |
**Example Usage:**
```basic
weather = WEATHER "Seattle"
TALK weather
```
---
## Messaging Channels
### WhatsApp Business API
| Property | Value |
|----------|-------|
| **Service URL** | `https://graph.facebook.com/v17.0` |
| **Config Keys** | `whatsapp-api-key`, `whatsapp-phone-number-id`, `whatsapp-business-account-id` |
| **Documentation** | [developers.facebook.com/docs/whatsapp](https://developers.facebook.com/docs/whatsapp) |
| **BASIC Keywords** | `SEND WHATSAPP`, `SEND FILE` (WhatsApp) |
| **Webhook URL** | `/api/channels/whatsapp/webhook` |
### Microsoft Teams
| Property | Value |
|----------|-------|
| **Service URL** | `https://smba.trafficmanager.net/apis` |
| **Config Keys** | `teams-app-id`, `teams-app-password`, `teams-tenant-id` |
| **Documentation** | [learn.microsoft.com/microsoftteams/platform](https://learn.microsoft.com/microsoftteams/platform) |
| **BASIC Keywords** | `SEND TEAMS`, `SEND FILE` (Teams) |
| **Webhook URL** | `/api/channels/teams/messages` |
### Instagram Messaging
| Property | Value |
|----------|-------|
| **Service URL** | `https://graph.facebook.com/v17.0` |
| **Config Keys** | `instagram-access-token`, `instagram-page-id`, `instagram-account-id` |
| **Documentation** | [developers.facebook.com/docs/instagram-api](https://developers.facebook.com/docs/instagram-api) |
| **BASIC Keywords** | `SEND INSTAGRAM` |
| **Webhook URL** | `/api/channels/instagram/webhook` |
### Telegram
| Property | Value |
|----------|-------|
| **Service URL** | `https://api.telegram.org/bot{token}` |
| **Config Keys** | `telegram-bot-token` |
| **Documentation** | [core.telegram.org/bots/api](https://core.telegram.org/bots/api) |
| **BASIC Keywords** | `SEND TELEGRAM` |
| **Webhook URL** | `/api/channels/telegram/webhook` |
---
## Storage Services
### S3-Compatible Storage
General Bots uses S3-compatible object storage. Configuration is **automatically managed** by the Directory service (Zitadel).
| Property | Value |
|----------|-------|
| **Local Default** | MinIO on port 9000 |
| **Management** | Directory service (automatic) |
| **Console Port** | 9001 (when using MinIO) |
| **BASIC Keywords** | `GET` (file retrieval) |
**Compatible Services:**
- MinIO (default local installation)
- Backblaze B2
- Wasabi
- DigitalOcean Spaces
- Cloudflare R2
- Any S3-compatible provider
Storage credentials are provisioned and rotated automatically by the Directory service. No manual configuration required.
---
## Directory Services
### Zitadel (Identity Provider)
| Property | Value |
|----------|-------|
| **Local Default** | Port 8080 |
| **Environment Variables** | `DIRECTORY_URL`, `DIRECTORY_CLIENT_ID`, `DIRECTORY_CLIENT_SECRET` |
| **Documentation** | [zitadel.com/docs](https://zitadel.com/docs) |
| **Purpose** | User authentication, SSO, OAuth2/OIDC, service credential management |
The Directory service manages:
- User authentication
- Service credentials (database, storage, cache)
- OAuth applications
- Role-based access control
---
## Email Services
### Stalwart Mail Server
| Property | Value |
|----------|-------|
| **Ports** | 25 (SMTP), 993 (IMAPS), 587 (Submission) |
| **Management** | Directory service (automatic) |
| **Documentation** | [stalw.art/docs](https://stalw.art/docs) |
| **BASIC Keywords** | `SEND MAIL` |
Email accounts are created and managed through the Directory service.
### External IMAP/SMTP
| Property | Value |
|----------|-------|
| **Config Keys** | `smtp-server`, `smtp-port`, `imap-server`, `imap-port`, `email-username`, `email-password` |
| **BASIC Keywords** | `SEND MAIL` |
| **Supported Providers** | Gmail, Outlook, custom SMTP/IMAP |
**Gmail Configuration Example (in config.csv):**
```csv
smtp-server,smtp.gmail.com
smtp-port,587
imap-server,imap.gmail.com
imap-port,993
```
---
## Local Services (BotModels)
### Image Generation
| Property | Value |
|----------|-------|
| **Service URL** | `http://localhost:5000` (default) |
| **Config Keys** | `botmodels-enabled`, `botmodels-url` |
| **BASIC Keywords** | `IMAGE` |
| **Requires** | BotModels service running |
### Video Generation
| Property | Value |
|----------|-------|
| **Service URL** | `http://localhost:5000` (default) |
| **Config Keys** | `botmodels-enabled`, `botmodels-url` |
| **BASIC Keywords** | `VIDEO` |
| **Requires** | BotModels service running, GPU recommended |
### Audio Generation (TTS)
| Property | Value |
|----------|-------|
| **Service URL** | `http://localhost:5000` (default) |
| **Config Keys** | `botmodels-enabled`, `botmodels-url` |
| **BASIC Keywords** | `AUDIO` |
| **Requires** | BotModels service running |
### Vision/Captioning
| Property | Value |
|----------|-------|
| **Service URL** | `http://localhost:5000` (default) |
| **Config Keys** | `botmodels-enabled`, `botmodels-url` |
| **BASIC Keywords** | `SEE` |
| **Requires** | BotModels service running |
---
## Internal Services
These services are deployed locally as part of the General Bots stack. All are managed by the Directory service:
| Service | Default Port | Purpose | Management |
|---------|-------------|---------|------------|
| PostgreSQL | 5432 | Primary database | Directory service |
| Qdrant | 6333 | Vector storage for KB | Directory service |
| Valkey | 6379 | Caching | Directory service |
| Stalwart | 25, 993 | Email server (optional) | Directory service |
| BotModels | 5000 | AI model inference | config.csv |
---
## Service Health Checks
All services can be checked via the monitoring API:
```
GET /api/monitoring/services
```
Response includes status for all configured external services.
---
## Troubleshooting
### Common Issues
1. **API Key Invalid** - Verify key in `config.csv`, ensure no trailing whitespace
2. **Rate Limited** - Check service quotas, implement caching with `SET BOT MEMORY`
3. **Connection Timeout** - Verify network access to external URLs
4. **Service Unavailable** - Check service status pages
### Debug Logging
Enable trace logging to see external API calls:
```bash
RUST_LOG=trace ./botserver
```

View file

@ -0,0 +1 @@
# Channel Integrations

View file

@ -0,0 +1 @@
# Directory Services

View file

@ -0,0 +1 @@
# LLM Providers

View file

@ -0,0 +1 @@
# Storage Services

View file

@ -0,0 +1 @@
# Weather API

View file

@ -0,0 +1,143 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300" fill="none">
<defs>
<!-- GB-inspired gradient for folders -->
<linearGradient id="folderGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#4A90E2"/>
<stop offset="100%" style="stop-color:#3672B8"/>
</linearGradient>
<!-- GB-inspired gradient for files -->
<linearGradient id="fileGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6366f1"/>
<stop offset="100%" style="stop-color:#4f46e5"/>
</linearGradient>
<!-- Connection line gradient -->
<linearGradient id="lineGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:0.8"/>
<stop offset="100%" style="stop-color:#60a5fa;stop-opacity:0.4"/>
</linearGradient>
<!-- Folder symbol -->
<symbol id="folder" viewBox="0 0 24 20">
<path d="M2 4C2 2.9 2.9 2 4 2H9.17C9.7 2 10.21 2.21 10.59 2.59L12 4H20C21.1 4 22 4.9 22 6V16C22 17.1 21.1 18 20 18H4C2.9 18 2 17.1 2 16V4Z" fill="url(#folderGrad)"/>
<circle cx="8" cy="11" r="1" fill="#fff" opacity="0.7"/>
<circle cx="12" cy="11" r="1" fill="#fff" opacity="0.7"/>
</symbol>
<!-- File symbol -->
<symbol id="file" viewBox="0 0 20 24">
<path d="M12 1H4C2.9 1 2 1.9 2 3V21C2 22.1 2.9 23 4 23H16C17.1 23 18 22.1 18 21V7L12 1Z" fill="url(#fileGrad)" opacity="0.85"/>
<path d="M12 1V7H18" fill="#8b9dc3"/>
<line x1="5" y1="12" x2="13" y2="12" stroke="#fff" stroke-width="1" opacity="0.6"/>
<line x1="5" y1="15" x2="11" y2="15" stroke="#fff" stroke-width="1" opacity="0.4"/>
<line x1="5" y1="18" x2="9" y2="18" stroke="#fff" stroke-width="1" opacity="0.3"/>
</symbol>
<!-- Bot config symbol (.gbot) -->
<symbol id="config" viewBox="0 0 20 24">
<path d="M12 1H4C2.9 1 2 1.9 2 3V21C2 22.1 2.9 23 4 23H16C17.1 23 18 22.1 18 21V7L12 1Z" fill="#f59e0b" opacity="0.85"/>
<path d="M12 1V7H18" fill="#fbbf24"/>
<circle cx="10" cy="14" r="4" fill="none" stroke="#fff" stroke-width="1.5" opacity="0.8"/>
<circle cx="10" cy="14" r="1.5" fill="#fff" opacity="0.8"/>
</symbol>
<!-- Knowledge base symbol (.gbkb) -->
<symbol id="knowledge" viewBox="0 0 20 24">
<path d="M12 1H4C2.9 1 2 1.9 2 3V21C2 22.1 2.9 23 4 23H16C17.1 23 18 22.1 18 21V7L12 1Z" fill="#10b981" opacity="0.85"/>
<path d="M12 1V7H18" fill="#34d399"/>
<circle cx="10" cy="13" r="3" fill="none" stroke="#fff" stroke-width="1.5" opacity="0.8"/>
<path d="M10 10V13L12 15" stroke="#fff" stroke-width="1.5" stroke-linecap="round" opacity="0.8"/>
</symbol>
<!-- Dialog/script symbol (.bas) -->
<symbol id="script" viewBox="0 0 20 24">
<path d="M12 1H4C2.9 1 2 1.9 2 3V21C2 22.1 2.9 23 4 23H16C17.1 23 18 22.1 18 21V7L12 1Z" fill="#8b5cf6" opacity="0.85"/>
<path d="M12 1V7H18" fill="#a78bfa"/>
<text x="5" y="16" font-family="monospace" font-size="8" fill="#fff" opacity="0.9">BAS</text>
</symbol>
</defs>
<style>
.tree-label { font-family: system-ui, -apple-system, sans-serif; font-size: 12px; fill: #374151; }
.tree-label-folder { font-weight: 600; fill: #1f2937; }
.connection { stroke: url(#lineGrad); stroke-width: 2; stroke-linecap: round; }
.node-dot { fill: #3b82f6; }
</style>
<!-- Example tree structure - customize as needed -->
<!-- Root folder -->
<g transform="translate(20, 20)">
<use href="#folder" width="24" height="20"/>
<text x="30" y="14" class="tree-label tree-label-folder">mybot.gbai</text>
</g>
<!-- Vertical connector from root -->
<path d="M32 40 L32 240" class="connection" opacity="0.5"/>
<!-- Level 1: .gbdialog folder -->
<g transform="translate(50, 60)">
<circle cx="-18" cy="10" r="4" class="node-dot" opacity="0.6"/>
<path d="M-14 10 L0 10" class="connection"/>
<use href="#folder" width="24" height="20"/>
<text x="30" y="14" class="tree-label tree-label-folder">mybot.gbdialog</text>
</g>
<!-- Vertical connector for .gbdialog children -->
<path d="M62 80 L62 140" class="connection" opacity="0.4"/>
<!-- Level 2: start.bas -->
<g transform="translate(80, 100)">
<circle cx="-18" cy="12" r="3" class="node-dot" opacity="0.5"/>
<path d="M-15 12 L0 12" class="connection" opacity="0.6"/>
<use href="#script" width="16" height="20"/>
<text x="22" y="14" class="tree-label">start.bas</text>
</g>
<!-- Level 2: auth.bas -->
<g transform="translate(80, 130)">
<circle cx="-18" cy="12" r="3" class="node-dot" opacity="0.5"/>
<path d="M-15 12 L0 12" class="connection" opacity="0.6"/>
<use href="#script" width="16" height="20"/>
<text x="22" y="14" class="tree-label">auth.bas</text>
</g>
<!-- Level 1: .gbkb folder -->
<g transform="translate(50, 160)">
<circle cx="-18" cy="10" r="4" class="node-dot" opacity="0.6"/>
<path d="M-14 10 L0 10" class="connection"/>
<use href="#folder" width="24" height="20"/>
<text x="30" y="14" class="tree-label tree-label-folder">mybot.gbkb</text>
</g>
<!-- Vertical connector for .gbkb children -->
<path d="M62 180 L62 210" class="connection" opacity="0.4"/>
<!-- Level 2: knowledge file -->
<g transform="translate(80, 195)">
<circle cx="-18" cy="12" r="3" class="node-dot" opacity="0.5"/>
<path d="M-15 12 L0 12" class="connection" opacity="0.6"/>
<use href="#knowledge" width="16" height="20"/>
<text x="22" y="14" class="tree-label">faq.docx</text>
</g>
<!-- Level 1: .gbot folder -->
<g transform="translate(50, 230)">
<circle cx="-18" cy="10" r="4" class="node-dot" opacity="0.6"/>
<path d="M-14 10 L0 10" class="connection"/>
<use href="#folder" width="24" height="20"/>
<text x="30" y="14" class="tree-label tree-label-folder">mybot.gbot</text>
</g>
<!-- Vertical connector for .gbot children -->
<path d="M62 250 L62 275" class="connection" opacity="0.4"/>
<!-- Level 2: config.csv -->
<g transform="translate(80, 260)">
<circle cx="-18" cy="12" r="3" class="node-dot" opacity="0.5"/>
<path d="M-15 12 L0 12" class="connection" opacity="0.6"/>
<use href="#config" width="16" height="20"/>
<text x="22" y="14" class="tree-label">config.csv</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<!-- Bot head - rounded square inspired by GB logo -->
<rect x="4" y="4" width="16" height="14" rx="3" ry="3"/>
<!-- Eyes - circular like GB branding -->
<circle cx="9" cy="10" r="1.5" fill="currentColor" stroke="none"/>
<circle cx="15" cy="10" r="1.5" fill="currentColor" stroke="none"/>
<!-- Smile -->
<path d="M9 14 Q12 16 15 14" fill="none"/>
<!-- Antenna -->
<line x1="12" y1="4" x2="12" y2="1"/>
<circle cx="12" cy="1" r="1" fill="currentColor" stroke="none"/>
<!-- Base/body connection -->
<path d="M8 18 L8 21 L16 21 L16 18"/>
<line x1="10" y1="21" x2="10" y2="23"/>
<line x1="14" y1="21" x2="14" y2="23"/>
</svg>

After

Width:  |  Height:  |  Size: 811 B

View file

@ -0,0 +1,27 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- GB-inspired chart icon with characteristic curves -->
<defs>
<linearGradient id="chartGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3b82f6"/>
<stop offset="100%" style="stop-color:#1d4ed8"/>
</linearGradient>
</defs>
<!-- Base platform - GB style rounded -->
<path d="M3 20h18" stroke="url(#chartGradient)" stroke-width="2" stroke-linecap="round"/>
<!-- Bar 1 - shortest -->
<rect x="4" y="14" width="3" height="6" rx="1" fill="url(#chartGradient)" opacity="0.6"/>
<!-- Bar 2 - medium -->
<rect x="9" y="10" width="3" height="10" rx="1" fill="url(#chartGradient)" opacity="0.8"/>
<!-- Bar 3 - tallest with GB curve accent -->
<rect x="14" y="5" width="3" height="15" rx="1" fill="url(#chartGradient)"/>
<!-- Trend line - GB characteristic curve -->
<path d="M5.5 13 C8 11, 10 9, 15.5 4" stroke="#10b981" stroke-width="2" stroke-linecap="round" fill="none"/>
<!-- Accent dot at peak -->
<circle cx="15.5" cy="4" r="2" fill="#10b981"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,21 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- GB-inspired checkmark icon -->
<defs>
<linearGradient id="gbCheckGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#10b981"/>
<stop offset="100%" style="stop-color:#059669"/>
</linearGradient>
</defs>
<!-- Outer circle - GB style -->
<circle cx="12" cy="12" r="10" stroke="url(#gbCheckGradient)" stroke-width="1.5" fill="none"/>
<!-- Inner fill with GB transparency -->
<circle cx="12" cy="12" r="8" fill="url(#gbCheckGradient)" opacity="0.15"/>
<!-- Checkmark path - bold and confident -->
<path d="M7 12.5L10.5 16L17 8" stroke="url(#gbCheckGradient)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<!-- Subtle accent dot at tip - GB brand element -->
<circle cx="17" cy="8" r="1" fill="#10b981"/>
</svg>

After

Width:  |  Height:  |  Size: 919 B

View file

@ -0,0 +1,29 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- GB-inspired database icon with cylindrical storage aesthetic -->
<defs>
<linearGradient id="dbGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#6366f1"/>
<stop offset="100%" style="stop-color:#4f46e5"/>
</linearGradient>
</defs>
<!-- Top ellipse - database cap -->
<ellipse cx="12" cy="5" rx="8" ry="3" fill="url(#dbGradient)" stroke="currentColor" stroke-width="1"/>
<!-- Database body sides -->
<path d="M4 5v14c0 1.66 3.58 3 8 3s8-1.34 8-3V5" stroke="currentColor" stroke-width="1" fill="none"/>
<!-- Side fill -->
<path d="M4 5v14c0 1.66 3.58 3 8 3s8-1.34 8-3V5c0 1.66-3.58 3-8 3S4 6.66 4 5z" fill="url(#dbGradient)" opacity="0.85"/>
<!-- Middle rings - GB horizontal accent lines -->
<ellipse cx="12" cy="10" rx="8" ry="2.5" fill="none" stroke="currentColor" stroke-width="0.75" opacity="0.5"/>
<ellipse cx="12" cy="15" rx="8" ry="2.5" fill="none" stroke="currentColor" stroke-width="0.75" opacity="0.5"/>
<!-- Data indicator dots - GB bot eyes reference -->
<circle cx="9" cy="12" r="1" fill="#fff" opacity="0.8"/>
<circle cx="15" cy="12" r="1" fill="#fff" opacity="0.8"/>
<!-- Bottom highlight -->
<ellipse cx="12" cy="19" rx="6" ry="1.5" fill="#fff" fill-opacity="0.15"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,20 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- GB-inspired folder icon -->
<defs>
<linearGradient id="folderGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#4A90E2"/>
<stop offset="100%" style="stop-color:#3672B8"/>
</linearGradient>
</defs>
<!-- Folder back -->
<path d="M2 6C2 4.89543 2.89543 4 4 4H9.17157C9.70201 4 10.2107 4.21071 10.5858 4.58579L12 6H20C21.1046 6 22 6.89543 22 8V18C22 19.1046 21.1046 20 20 20H4C2.89543 20 2 19.1046 2 18V6Z" fill="url(#folderGradient)" opacity="0.85"/>
<!-- Folder front tab -->
<path d="M2 8C2 7.44772 2.44772 7 3 7H21C21.5523 7 22 7.44772 22 8V18C22 19.1046 21.1046 20 20 20H4C2.89543 20 2 19.1046 2 18V8Z" fill="url(#folderGradient)"/>
<!-- GB bot face on folder - subtle branding -->
<circle cx="9" cy="13" r="1" fill="#fff" opacity="0.8"/>
<circle cx="15" cy="13" r="1" fill="#fff" opacity="0.8"/>
<path d="M10 16 Q12 17.5 14 16" stroke="#fff" stroke-width="1" fill="none" opacity="0.6" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.4 15C19.2669 15.3016 19.2272 15.6362 19.286 15.9606C19.3448 16.285 19.4995 16.5843 19.73 16.82L19.79 16.88C19.976 17.0657 20.1235 17.2863 20.2241 17.5291C20.3248 17.7719 20.3766 18.0322 20.3766 18.295C20.3766 18.5578 20.3248 18.8181 20.2241 19.0609C20.1235 19.3037 19.976 19.5243 19.79 19.71C19.6043 19.896 19.3837 20.0435 19.1409 20.1441C18.8981 20.2448 18.6378 20.2966 18.375 20.2966C18.1122 20.2966 17.8519 20.2448 17.6091 20.1441C17.3663 20.0435 17.1457 19.896 16.96 19.71L16.9 19.65C16.6643 19.4195 16.365 19.2648 16.0406 19.206C15.7162 19.1472 15.3816 19.1869 15.08 19.32C14.7842 19.4468 14.532 19.6572 14.3543 19.9255C14.1766 20.1938 14.0813 20.5082 14.08 20.83V21C14.08 21.5304 13.8693 22.0391 13.4942 22.4142C13.1191 22.7893 12.6104 23 12.08 23C11.5496 23 11.0409 22.7893 10.6658 22.4142C10.2907 22.0391 10.08 21.5304 10.08 21V20.91C10.0723 20.579 9.96512 20.258 9.77251 19.9887C9.5799 19.7194 9.31074 19.5143 9 19.4C8.69838 19.2669 8.36381 19.2272 8.03941 19.286C7.71502 19.3448 7.41568 19.4995 7.18 19.73L7.12 19.79C6.93425 19.976 6.71368 20.1235 6.47088 20.2241C6.22808 20.3248 5.96783 20.3766 5.705 20.3766C5.44217 20.3766 5.18192 20.3248 4.93912 20.2241C4.69632 20.1235 4.47575 19.976 4.29 19.79C4.10405 19.6043 3.95653 19.3837 3.85588 19.1409C3.75523 18.8981 3.70343 18.6378 3.70343 18.375C3.70343 18.1122 3.75523 17.8519 3.85588 17.6091C3.95653 17.3663 4.10405 17.1457 4.29 16.96L4.35 16.9C4.58054 16.6643 4.73519 16.365 4.794 16.0406C4.85282 15.7162 4.81312 15.3816 4.68 15.08C4.55324 14.7842 4.34276 14.532 4.07447 14.3543C3.80618 14.1766 3.49179 14.0813 3.17 14.08H3C2.46957 14.08 1.96086 13.8693 1.58579 13.4942C1.21071 13.1191 1 12.6104 1 12.08C1 11.5496 1.21071 11.0409 1.58579 10.6658C1.96086 10.2907 2.46957 10.08 3 10.08H3.09C3.42099 10.0723 3.742 9.96512 4.0113 9.77251C4.28059 9.5799 4.48572 9.31074 4.6 9C4.73312 8.69838 4.77282 8.36381 4.714 8.03941C4.65519 7.71502 4.50054 7.41568 4.27 7.18L4.21 7.12C4.02405 6.93425 3.87653 6.71368 3.77588 6.47088C3.67523 6.22808 3.62343 5.96783 3.62343 5.705C3.62343 5.44217 3.67523 5.18192 3.77588 4.93912C3.87653 4.69632 4.02405 4.47575 4.21 4.29C4.39575 4.10405 4.61632 3.95653 4.85912 3.85588C5.10192 3.75523 5.36217 3.70343 5.625 3.70343C5.88783 3.70343 6.14808 3.75523 6.39088 3.85588C6.63368 3.95653 6.85425 4.10405 7.04 4.29L7.1 4.35C7.33568 4.58054 7.63502 4.73519 7.95941 4.794C8.28381 4.85282 8.61838 4.81312 8.92 4.68H9C9.29577 4.55324 9.54802 4.34276 9.72569 4.07447C9.90337 3.80618 9.99872 3.49179 10 3.17V3C10 2.46957 10.2107 1.96086 10.5858 1.58579C10.9609 1.21071 11.4696 1 12 1C12.5304 1 13.0391 1.21071 13.4142 1.58579C13.7893 1.96086 14 2.46957 14 3V3.09C14.0013 3.41179 14.0966 3.72618 14.2743 3.99447C14.452 4.26276 14.7042 4.47324 15 4.6C15.3016 4.73312 15.6362 4.77282 15.9606 4.714C16.285 4.65519 16.5843 4.50054 16.82 4.27L16.88 4.21C17.0657 4.02405 17.2863 3.87653 17.5291 3.77588C17.7719 3.67523 18.0322 3.62343 18.295 3.62343C18.5578 3.62343 18.8181 3.67523 19.0609 3.77588C19.3037 3.87653 19.5243 4.02405 19.71 4.21C19.896 4.39575 20.0435 4.61632 20.1441 4.85912C20.2448 5.10192 20.2966 5.36217 20.2966 5.625C20.2966 5.88783 20.2448 6.14808 20.1441 6.39088C20.0435 6.63368 19.896 6.85425 19.71 7.04L19.65 7.1C19.4195 7.33568 19.2648 7.63502 19.206 7.95941C19.1472 8.28381 19.1869 8.61838 19.32 8.92V9C19.4468 9.29577 19.6572 9.54802 19.9255 9.72569C20.1938 9.90337 20.5082 9.99872 20.83 10H21C21.5304 10 22.0391 10.2107 22.4142 10.5858C22.7893 10.9609 23 11.4696 23 12C23 12.5304 22.7893 13.0391 22.4142 13.4142C22.0391 13.7893 21.5304 14 21 14H20.91C20.5882 14.0013 20.2738 14.0966 20.0055 14.2743C19.7372 14.452 19.5268 14.7042 19.4 15Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -0,0 +1,32 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- GB-inspired globe/web icon -->
<defs>
<linearGradient id="globeGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3b82f6"/>
<stop offset="100%" style="stop-color:#1d4ed8"/>
</linearGradient>
</defs>
<!-- Main globe circle -->
<circle cx="12" cy="12" r="10" stroke="url(#globeGradient)" stroke-width="1.5" fill="none"/>
<!-- Horizontal latitude lines - GB curved style -->
<ellipse cx="12" cy="12" rx="10" ry="4" stroke="currentColor" stroke-width="1" fill="none" opacity="0.4"/>
<ellipse cx="12" cy="12" rx="10" ry="7" stroke="currentColor" stroke-width="1" fill="none" opacity="0.25"/>
<!-- Vertical meridian line -->
<ellipse cx="12" cy="12" rx="4" ry="10" stroke="currentColor" stroke-width="1" fill="none" opacity="0.5"/>
<!-- Center vertical line -->
<line x1="12" y1="2" x2="12" y2="22" stroke="currentColor" stroke-width="1" opacity="0.3"/>
<!-- Center horizontal line -->
<line x1="2" y1="12" x2="22" y2="12" stroke="currentColor" stroke-width="1" opacity="0.3"/>
<!-- GB bot eyes on globe - subtle branding -->
<circle cx="9" cy="10" r="1" fill="url(#globeGradient)" opacity="0.7"/>
<circle cx="15" cy="10" r="1" fill="url(#globeGradient)" opacity="0.7"/>
<!-- Subtle highlight arc -->
<path d="M6 6 Q8 4 12 4" stroke="white" stroke-width="1.5" fill="none" opacity="0.3" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,20 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- General Bots inspired lightbulb icon -->
<defs>
<linearGradient id="gbLightbulbGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#F5A623;stop-opacity:1" />
<stop offset="100%" style="stop-color:#F8D71C;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Bulb body with GB circular motif -->
<path d="M12 2C8.13 2 5 5.13 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.87-3.13-7-7-7z" fill="url(#gbLightbulbGradient)"/>
<!-- Inner glow representing bot intelligence -->
<circle cx="12" cy="9" r="3" fill="#fff" fill-opacity="0.6"/>
<!-- Bot eye dot -->
<circle cx="12" cy="9" r="1.2" fill="#4A90E2"/>
<!-- Base segments -->
<rect x="9" y="18" width="6" height="1.5" rx="0.5" fill="#666"/>
<rect x="9" y="20" width="6" height="1.5" rx="0.5" fill="#888"/>
<!-- Light rays -->
<path d="M12 0v1.5M18.5 3.5l-1.06 1.06M21 9h-1.5M18.5 14.5l-1.06-1.06M5.5 3.5l1.06 1.06M3 9h1.5M5.5 14.5l1.06-1.06" stroke="#F5A623" stroke-width="1" stroke-linecap="round" opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,43 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- GB-inspired lock/security icon -->
<defs>
<linearGradient id="lockGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#6366f1"/>
<stop offset="100%" style="stop-color:#4f46e5"/>
</linearGradient>
</defs>
<!-- Lock shackle - rounded GB style -->
<path d="M7 10V7C7 4.23858 9.23858 2 12 2C14.7614 2 17 4.23858 17 7V10"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
fill="none"/>
<!-- Lock body - rounded rectangle with GB aesthetic -->
<rect x="5" y="10" width="14" height="12" rx="2"
fill="url(#lockGradient)"
stroke="currentColor"
stroke-width="1.5"/>
<!-- Keyhole - bot eye inspired circular design -->
<circle cx="12" cy="14.5" r="2" fill="#1e1b4b"/>
<!-- Keyhole slot -->
<path d="M12 16V18.5" stroke="#1e1b4b" stroke-width="2" stroke-linecap="round"/>
<!-- Highlight reflection - GB polish -->
<path d="M7 12C7 11.4477 7.44772 11 8 11H10C10.5523 11 11 11.4477 11 12"
stroke="white"
stroke-opacity="0.3"
stroke-width="1"
stroke-linecap="round"
fill="none"/>
<!-- Subtle inner border glow -->
<rect x="6" y="11" width="12" height="10" rx="1.5"
stroke="white"
stroke-opacity="0.1"
stroke-width="0.5"
fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,16 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- GB-inspired document/note icon -->
<!-- Page with folded corner -->
<path d="M14 2H6C4.9 2 4 2.9 4 4v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6z" stroke="currentColor" stroke-width="1.5" fill="none"/>
<!-- Folded corner -->
<path d="M14 2v6h6" stroke="currentColor" stroke-width="1.5" fill="currentColor" fill-opacity="0.1" stroke-linejoin="round"/>
<!-- Text lines - GB clean style -->
<line x1="8" y1="12" x2="16" y2="12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.7"/>
<line x1="8" y1="15" x2="14" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.5"/>
<line x1="8" y1="18" x2="12" y2="18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.3"/>
<!-- GB accent dot -->
<circle cx="17" cy="17" r="1.5" fill="#3b82f6"/>
</svg>

After

Width:  |  Height:  |  Size: 942 B

View file

@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<!-- GB-inspired package icon - hexagonal bot head with package box -->
<path d="M12 2L3 7v10l9 5 9-5V7l-9-5z" opacity="0.2" fill="currentColor"/>
<path d="M12 2L3 7v10l9 5 9-5V7l-9-5z"/>
<path d="M12 22V12"/>
<path d="M21 7l-9 5-9-5"/>
<!-- Bot eyes -->
<circle cx="9" cy="10" r="1" fill="currentColor"/>
<circle cx="15" cy="10" r="1" fill="currentColor"/>
<!-- Bot antenna -->
<path d="M12 2v-1"/>
<circle cx="12" cy="0.5" r="0.5" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 645 B

View file

@ -0,0 +1,32 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- GB-inspired palette icon for theming -->
<defs>
<linearGradient id="paletteGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#8b5cf6"/>
<stop offset="100%" style="stop-color:#6366f1"/>
</linearGradient>
</defs>
<!-- Palette shape - GB rounded organic form -->
<path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C12.76 22 13.5 21.76 14.1 21.32C14.85 20.78 15.3 19.9 15.3 18.95C15.3 18.44 15.12 17.95 14.82 17.54C14.52 17.12 14.38 16.6 14.55 16.1C14.74 15.53 15.3 15.15 15.9 15.15H18C20.21 15.15 22 13.36 22 11.15C22 6.13 17.52 2 12 2Z"
fill="url(#paletteGradient)"
stroke="currentColor"
stroke-width="1"
opacity="0.85"/>
<!-- Color dots - GB circular branding style -->
<!-- Blue dot -->
<circle cx="6.5" cy="11" r="2" fill="#3b82f6"/>
<!-- Green dot -->
<circle cx="9" cy="6.5" r="2" fill="#10b981"/>
<!-- Yellow/Gold dot -->
<circle cx="14" cy="6" r="2" fill="#fbbf24"/>
<!-- Pink/Coral dot -->
<circle cx="17" cy="10" r="2" fill="#f472b6"/>
<!-- Highlight reflection - GB polish -->
<path d="M7 4C9 3 11 2.5 13 3" stroke="white" stroke-width="1" stroke-linecap="round" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,31 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- GB-inspired rocket icon - representing launch/deploy -->
<defs>
<linearGradient id="rocketGradient" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#3b82f6"/>
<stop offset="100%" style="stop-color:#60a5fa"/>
</linearGradient>
<linearGradient id="flameGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f97316"/>
<stop offset="50%" style="stop-color:#fbbf24"/>
<stop offset="100%" style="stop-color:#fcd34d"/>
</linearGradient>
</defs>
<!-- Rocket body - GB rounded aesthetic -->
<path d="M12 2C12 2 8 6 8 12C8 15 9.5 17 12 19C14.5 17 16 15 16 12C16 6 12 2 12 2Z" fill="url(#rocketGradient)" stroke="currentColor" stroke-width="1"/>
<!-- Window - circular GB style -->
<circle cx="12" cy="9" r="2" fill="#fff" stroke="currentColor" stroke-width="0.75"/>
<circle cx="12" cy="9" r="1" fill="#1e3a5f" opacity="0.8"/>
<!-- Left fin -->
<path d="M8 14L5 17L7 18L8 16" fill="url(#rocketGradient)" stroke="currentColor" stroke-width="0.75" stroke-linejoin="round"/>
<!-- Right fin -->
<path d="M16 14L19 17L17 18L16 16" fill="url(#rocketGradient)" stroke="currentColor" stroke-width="0.75" stroke-linejoin="round"/>
<!-- Flame exhaust -->
<path d="M10 19C10 19 11 22 12 23C13 22 14 19 14 19C13.5 20 12.5 21 12 21C11.5 21 10.5 20 10 19Z" fill="url(#flameGradient)"/>
<path d="M11 19C11 19 11.5 21 12 21.5C12.5 21 13 19 13 19" fill="#fef08a" opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,18 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- GB-inspired search icon with bot-like characteristics -->
<!-- Main magnifying glass circle - GB rounded aesthetic -->
<circle cx="10" cy="10" r="7" stroke="currentColor" stroke-width="1.5" fill="none"/>
<!-- Inner circle detail - bot eye inspired -->
<circle cx="10" cy="10" r="3.5" stroke="currentColor" stroke-width="1" fill="none" opacity="0.4"/>
<!-- Center highlight - GB bot eye -->
<circle cx="10" cy="10" r="1.5" fill="currentColor" opacity="0.6"/>
<!-- Handle with GB curve -->
<path d="M15.5 15.5L21 21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<!-- Subtle reflection arc - GB style -->
<path d="M6 7 Q7.5 5.5 9.5 5.5" stroke="currentColor" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 878 B

View file

@ -0,0 +1,33 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- GB-inspired signal/broadcast icon -->
<defs>
<linearGradient id="signalGradient" x1="0%" y1="100%" x2="0%" y2="0%">
<stop offset="0%" style="stop-color:#3b82f6"/>
<stop offset="100%" style="stop-color:#60a5fa"/>
</linearGradient>
</defs>
<!-- Outer wave arc -->
<path d="M4.5 4.5C1.5 7.5 1.5 12.5 4.5 15.5" stroke="url(#signalGradient)" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.4"/>
<path d="M19.5 4.5C22.5 7.5 22.5 12.5 19.5 15.5" stroke="url(#signalGradient)" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.4"/>
<!-- Middle wave arc -->
<path d="M7 7C5 9 5 11 7 13" stroke="url(#signalGradient)" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.6"/>
<path d="M17 7C19 9 19 11 17 13" stroke="url(#signalGradient)" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.6"/>
<!-- Inner wave arc -->
<path d="M9.5 8.5C8.5 9.5 8.5 10.5 9.5 11.5" stroke="url(#signalGradient)" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.8"/>
<path d="M14.5 8.5C15.5 9.5 15.5 10.5 14.5 11.5" stroke="url(#signalGradient)" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.8"/>
<!-- Center broadcast point - GB bot head style -->
<circle cx="12" cy="10" r="2.5" fill="url(#signalGradient)"/>
<circle cx="11" cy="9.5" r="0.5" fill="#fff" opacity="0.8"/>
<circle cx="13" cy="9.5" r="0.5" fill="#fff" opacity="0.8"/>
<!-- Antenna tower base -->
<path d="M12 12.5V18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M9 18H15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M10 21H14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>
<path d="M9 18L10 21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>
<path d="M15 18L14 21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,16 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- GB Target Icon - Inspired by General Bots aesthetic -->
<!-- Outer ring -->
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.3"/>
<!-- Middle ring -->
<circle cx="12" cy="12" r="6.5" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.6"/>
<!-- Inner core - GB style rounded hexagon hint -->
<path d="M12 5.5L15.5 8V13L12 16.5L8.5 13V8L12 5.5Z" stroke="currentColor" stroke-width="1.5" fill="currentColor" fill-opacity="0.15" stroke-linejoin="round"/>
<!-- Center dot -->
<circle cx="12" cy="11" r="1.5" fill="currentColor"/>
<!-- Targeting lines -->
<line x1="12" y1="2" x2="12" y2="4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="12" y1="20" x2="12" y2="22" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="2" y1="12" x2="4" y2="12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="20" y1="12" x2="22" y2="12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,42 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- GB-inspired tree structure icon for file/directory trees -->
<defs>
<linearGradient id="treeGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3b82f6"/>
<stop offset="100%" style="stop-color:#1d4ed8"/>
</linearGradient>
</defs>
<!-- Main vertical trunk -->
<path d="M6 3V21" stroke="url(#treeGradient)" stroke-width="2" stroke-linecap="round"/>
<!-- Branch 1 - top level -->
<path d="M6 6H10" stroke="url(#treeGradient)" stroke-width="2" stroke-linecap="round"/>
<!-- Node 1 - GB bot-inspired circle -->
<circle cx="13" cy="6" r="3" fill="url(#treeGradient)" opacity="0.9"/>
<circle cx="12" cy="5.5" r="0.6" fill="#fff"/>
<circle cx="14" cy="5.5" r="0.6" fill="#fff"/>
<!-- Branch 2 - middle level -->
<path d="M6 12H10" stroke="url(#treeGradient)" stroke-width="2" stroke-linecap="round"/>
<!-- Node 2 - folder-like rectangle with GB rounding -->
<rect x="10" y="10" width="8" height="4" rx="1.5" fill="url(#treeGradient)" opacity="0.7"/>
<line x1="12" y1="12" x2="16" y2="12" stroke="#fff" stroke-width="1" stroke-linecap="round" opacity="0.8"/>
<!-- Branch 3 - bottom level -->
<path d="M6 18H10" stroke="url(#treeGradient)" stroke-width="2" stroke-linecap="round"/>
<!-- Node 3 - document icon -->
<path d="M10 16H16C16.5523 16 17 16.4477 17 17V19C17 19.5523 16.5523 20 16 20H10C10 20 10 16 10 16Z" fill="url(#treeGradient)" opacity="0.5"/>
<line x1="12" y1="18" x2="15" y2="18" stroke="#fff" stroke-width="0.75" stroke-linecap="round"/>
<!-- Sub-branch connector dots - GB style -->
<circle cx="6" cy="6" r="1" fill="url(#treeGradient)"/>
<circle cx="6" cy="12" r="1" fill="url(#treeGradient)"/>
<circle cx="6" cy="18" r="1" fill="url(#treeGradient)"/>
<!-- Root node at top -->
<circle cx="6" cy="3" r="1.5" fill="url(#treeGradient)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,25 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- GB-inspired warning icon - triangular with bot aesthetic -->
<defs>
<linearGradient id="warningGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#fbbf24"/>
<stop offset="100%" style="stop-color:#f59e0b"/>
</linearGradient>
</defs>
<!-- Warning triangle with rounded corners - GB style -->
<path d="M12 2L2 20C1.7 20.5 2.1 21 2.6 21H21.4C21.9 21 22.3 20.5 22 20L12 2Z"
fill="url(#warningGradient)"
stroke="#d97706"
stroke-width="1"
stroke-linejoin="round"/>
<!-- Bot-style exclamation with rounded elements -->
<rect x="11" y="8" width="2" height="6" rx="1" fill="#7c2d12"/>
<!-- Dot at bottom - circular like GB branding -->
<circle cx="12" cy="17" r="1.2" fill="#7c2d12"/>
<!-- Subtle inner highlight -->
<path d="M12 4L4.5 18H19.5L12 4Z" fill="white" fill-opacity="0.15"/>
</svg>

After

Width:  |  Height:  |  Size: 992 B

View file

@ -0,0 +1,200 @@
# Monitoring Dashboard
The Monitoring Dashboard provides real-time visibility into your General Bots deployment, displaying system health, active sessions, and resource utilization in a clean tree-based interface.
## Overview
Access the Monitoring tab from the Suite interface to view:
- Active sessions and conversations
- Message throughput
- System resources (CPU, GPU, Memory)
- Service health status
- Bot activity metrics
## Dashboard Layout
The monitoring interface uses a hierarchical tree view for organized data display with the following panels:
- **Sessions** - Active connections, peak usage, average duration
- **Messages** - Daily totals, hourly rates, response times
- **Resources** - CPU, Memory, GPU, and Disk utilization with progress bars
- **Services** - Health status of PostgreSQL, Qdrant, Cache, Drive, BotModels
- **Active Bots** - List of running bots with session counts
## Metrics Explained
### Sessions
| Metric | Description |
|--------|-------------|
| **Active** | Current open conversations |
| **Peak Today** | Maximum concurrent sessions today |
| **Avg Duration** | Average session length |
| **Unique Users** | Distinct users today |
### Messages
| Metric | Description |
|--------|-------------|
| **Today** | Total messages processed today |
| **This Hour** | Messages in the current hour |
| **Avg Response** | Average bot response time |
| **Success Rate** | Percentage of successful responses |
### Resources
| Resource | Description | Warning Threshold |
|----------|-------------|-------------------|
| **CPU** | Processor utilization | > 80% |
| **Memory** | RAM usage | > 85% |
| **GPU** | Graphics processor (if available) | > 90% |
| **Disk** | Storage utilization | > 90% |
### Services
| Status | Indicator | Meaning |
|--------|-----------|---------|
| Running | Green dot | Service is healthy |
| Warning | Yellow dot | Service degraded |
| Stopped | Red dot | Service unavailable |
## Real-Time Updates
The dashboard refreshes automatically using HTMX polling:
- Session counts: Every 5 seconds
- Message metrics: Every 10 seconds
- Resource usage: Every 15 seconds
- Service health: Every 30 seconds
## Accessing via API
Monitoring data is available programmatically:
```
GET /api/monitoring/status
```
**Response:**
```json
{
"sessions": {
"active": 12,
"peak_today": 47,
"avg_duration_seconds": 512
},
"messages": {
"today": 1234,
"this_hour": 89,
"avg_response_ms": 1200
},
"resources": {
"cpu_percent": 78,
"memory_percent": 62,
"gpu_percent": 45,
"disk_percent": 28
},
"services": {
"postgresql": "running",
"qdrant": "running",
"cache": "running",
"drive": "running",
"botmodels": "stopped"
}
}
```
## Console Mode
In console mode, monitoring displays as text output:
```bash
./botserver --console --monitor
```
Output:
```
[MONITOR] 2024-01-15 14:32:00
Sessions: 12 active (peak: 47)
Messages: 1,234 today (89/hour)
CPU: 78% | MEM: 62% | GPU: 45%
Services: 4/5 running
```
## Alerts Configuration
Configure alerts in `config.csv`:
```csv
key,value
alert-cpu-threshold,80
alert-memory-threshold,85
alert-disk-threshold,90
alert-response-time-ms,5000
alert-email,admin@example.com
```
## Bot-Specific Metrics
View metrics for individual bots:
```
GET /api/monitoring/bots/{bot_id}
```
Returns:
- Message count for this bot
- Active sessions for this bot
- Average response time
- KB query statistics
- Tool execution counts
## Historical Data
Access historical metrics:
```
GET /api/monitoring/history?period=24h
```
Supported periods:
- `1h` - Last hour (minute granularity)
- `24h` - Last 24 hours (hourly granularity)
- `7d` - Last 7 days (daily granularity)
- `30d` - Last 30 days (daily granularity)
## Performance Tips
1. **High CPU Usage**
- Check for complex BASIC scripts
- Review LLM call frequency
- Consider semantic caching
2. **High Memory Usage**
- Reduce `max-context-messages`
- Clear unused KB collections
- Restart services to clear caches
3. **Slow Response Times**
- Enable semantic caching
- Optimize KB document sizes
- Use faster LLM models
## Integration with External Tools
Export metrics for external monitoring:
```
GET /api/monitoring/prometheus
```
Compatible with:
- Prometheus
- Grafana
- Datadog
- New Relic
## See Also
- [Console Mode](./console-mode.md) - Command-line interface
- [Settings](../chapter-08-config/README.md) - Configuration options
- [Monitoring API](../chapter-10-api/monitoring-api.md) - Full API reference

View file

@ -0,0 +1,248 @@
# Player - Media Viewer
The Player component provides integrated viewing capabilities for documents, audio, video, and presentations directly within the General Bots Suite interface.
## Overview
Player enables users to view and interact with various file types without leaving the conversation:
- **Documents**: PDF, DOCX, TXT, MD
- **Presentations**: PPTX, ODP
- **Audio**: MP3, WAV, OGG, M4A
- **Video**: MP4, WEBM, OGV
- **Images**: PNG, JPG, GIF, SVG, WEBP
## Accessing Player
### From Chat
When a bot shares a file, click the preview to open in Player:
```basic
' Bot script example
SEND FILE "presentation.pptx"
TALK "Here's the quarterly report. Click to view."
```
### From Drive
Navigate to Drive tab and click any supported file to open in Player.
### Direct URL
Access files directly:
```
/player/{bot_id}/{file_path}
```
## Player Interface
The player displays files in a clean, focused view with navigation controls at the bottom. Controls adapt based on the file type being viewed.
## Controls by File Type
### Document Controls
| Control | Action |
|---------|--------|
| Previous / Next | Navigate pages |
| Zoom in / out | Adjust view size |
| Download | Download original |
| Search | Search in document |
| Thumbnails | Page thumbnails |
### Audio Controls
| Control | Action |
|---------|--------|
| Play / Pause | Control playback |
| Rewind / Forward | Skip 10 seconds |
| Volume | Volume slider |
| Loop | Loop toggle |
| Download | Download file |
### Video Controls
| Control | Action |
|---------|--------|
| Play / Pause | Control playback |
| Skip | Skip backward / forward |
| Volume | Volume control |
| Fullscreen | Enter fullscreen |
| Speed | Playback speed |
| Picture-in-picture | Floating window |
| Download | Download file |
### Presentation Controls
| Control | Action |
|---------|--------|
| Previous / Next | Navigate slides |
| Fullscreen | Presentation mode |
| Overview | Slide overview |
| Notes | Speaker notes (if available) |
| Download | Download original |
## Keyboard Shortcuts
| Key | Action |
|-----|--------|
| `Space` | Play/Pause (audio/video) or Next (slides) |
| `←` / `→` | Previous / Next |
| `↑` / `↓` | Volume up / down |
| `F` | Fullscreen toggle |
| `M` | Mute toggle |
| `Esc` | Exit fullscreen / Close player |
| `+` / `-` | Zoom in / out |
| `Home` / `End` | Go to start / end |
## BASIC Integration
### Share Files with Player Preview
```basic
' Send a video with player preview
video_path = "training/welcome-video.mp4"
SEND FILE video_path
TALK "Watch this 2-minute introduction video."
' Send presentation
SEND FILE "reports/q4-results.pptx"
TALK "Here are the quarterly results. Use arrow keys to navigate."
' Send audio
SEND FILE "audio/podcast-episode-42.mp3"
TALK "Listen to the latest episode."
```
### Conditional File Viewing
```basic
file_type = GET file_extension
SWITCH file_type
CASE "mp4", "webm"
TALK "Playing video..."
SEND FILE video_path
CASE "mp3", "wav"
TALK "Playing audio..."
SEND FILE audio_path
CASE "pptx", "pdf"
TALK "Opening document..."
SEND FILE doc_path
DEFAULT
TALK "Downloading file..."
SEND FILE file_path
END SWITCH
```
## Supported Formats
### Documents
| Format | Extension | Notes |
|--------|-----------|-------|
| PDF | `.pdf` | Full support with text search |
| Word | `.docx` | Converted to viewable format |
| Text | `.txt` | Plain text with syntax highlighting |
| Markdown | `.md` | Rendered with formatting |
| HTML | `.html` | Sanitized rendering |
### Presentations
| Format | Extension | Notes |
|--------|-----------|-------|
| PowerPoint | `.pptx` | Full slide support |
| OpenDocument | `.odp` | Converted to slides |
| PDF | `.pdf` | Treated as slides |
### Audio
| Format | Extension | Notes |
|--------|-----------|-------|
| MP3 | `.mp3` | Universal support |
| WAV | `.wav` | Uncompressed audio |
| OGG | `.ogg` | Open format |
| M4A | `.m4a` | AAC audio |
| FLAC | `.flac` | Lossless audio |
### Video
| Format | Extension | Notes |
|--------|-----------|-------|
| MP4 | `.mp4` | H.264/H.265 |
| WebM | `.webm` | VP8/VP9 |
| OGV | `.ogv` | Theora |
### Images
| Format | Extension | Notes |
|--------|-----------|-------|
| PNG | `.png` | Lossless with transparency |
| JPEG | `.jpg`, `.jpeg` | Compressed photos |
| GIF | `.gif` | Animated support |
| SVG | `.svg` | Vector graphics |
| WebP | `.webp` | Modern format |
## Configuration
Configure Player behavior in `config.csv`:
```csv
key,value
player-autoplay,false
player-default-volume,80
player-video-quality,auto
player-preload,metadata
player-allow-download,true
player-max-file-size-mb,100
```
## API Access
### Get File for Player
```
GET /api/drive/{bot_id}/files/{file_path}?preview=true
```
### Stream Media
```
GET /api/drive/{bot_id}/stream/{file_path}
```
Supports HTTP Range requests for seeking.
### Get Thumbnail
```
GET /api/drive/{bot_id}/thumbnail/{file_path}
```
## Security
- Files are served through authenticated endpoints
- User permissions respected for file access
- Downloads can be disabled per bot
- Watermarking available for sensitive documents
## Performance
- Lazy loading for large documents
- Adaptive streaming for video
- Thumbnail generation for previews
- Client-side caching for repeat views
## Mobile Support
Player is fully responsive:
- Touch gestures for navigation
- Pinch-to-zoom for documents
- Swipe for slides
- Native fullscreen support
## See Also
- [Drive Integration](../chapter-08-config/drive.md) - File storage
- [Storage API](../chapter-10-api/storage-api.md) - File management API

View file

@ -0,0 +1,150 @@
# INSTR
The `INSTR` keyword returns the position of a substring within a string, following classic BASIC semantics.
## Syntax
```basic
position = INSTR(string, substring)
position = INSTR(start, string, substring)
```
## Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `start` | number | Optional. Starting position for the search (1-based) |
| `string` | string | The string to search in |
| `substring` | string | The substring to find |
## Return Value
- Returns the 1-based position of the first occurrence of `substring` in `string`
- Returns `0` if the substring is not found
- Returns `0` if either string is empty
## Description
`INSTR` searches for the first occurrence of a substring within another string. Unlike zero-based indexing in many modern languages, INSTR uses 1-based positioning consistent with traditional BASIC.
When the optional `start` parameter is provided, the search begins at that position rather than at the beginning of the string.
## Examples
### Basic Usage
```basic
text = "Hello, General Bots!"
pos = INSTR(text, "General")
TALK "Found 'General' at position: " + pos
' Output: Found 'General' at position: 8
```
### Checking if Substring Exists
```basic
email = HEAR "Enter your email:"
IF INSTR(email, "@") > 0 THEN
TALK "Valid email format"
ELSE
TALK "Email must contain @"
END IF
```
### Starting Search at Position
```basic
text = "one two one three one"
first = INSTR(text, "one") ' Returns 1
second = INSTR(5, text, "one") ' Returns 9 (starts after first "one")
third = INSTR(10, text, "one") ' Returns 19
```
### Extracting Data
```basic
data = "Name: John Smith"
colon_pos = INSTR(data, ":")
IF colon_pos > 0 THEN
' Get everything after ": "
name = MID(data, colon_pos + 2)
TALK "Extracted name: " + name
END IF
```
### Case-Sensitive Search
```basic
text = "General Bots"
pos1 = INSTR(text, "bots") ' Returns 0 (not found - case matters)
pos2 = INSTR(text, "Bots") ' Returns 9 (found)
```
### Finding Multiple Occurrences
```basic
text = "apple,banana,cherry,apple"
search = "apple"
count = 0
pos = 1
DO WHILE pos > 0
pos = INSTR(pos, text, search)
IF pos > 0 THEN
count = count + 1
pos = pos + 1 ' Move past current match
END IF
LOOP
TALK "Found '" + search + "' " + count + " times"
```
### Validating Input Format
```basic
phone = HEAR "Enter phone number (XXX-XXX-XXXX):"
dash1 = INSTR(phone, "-")
dash2 = INSTR(dash1 + 1, phone, "-")
IF dash1 = 4 AND dash2 = 8 THEN
TALK "Phone format is correct"
ELSE
TALK "Invalid format. Use XXX-XXX-XXXX"
END IF
```
## Comparison with Other Keywords
| Keyword | Purpose |
|---------|---------|
| `INSTR` | Find position of substring |
| `FORMAT` | Format strings with patterns |
| `FIRST` | Get first element of array |
| `LAST` | Get last element of array |
## Notes
- **1-based indexing**: Position 1 is the first character, not 0
- **Case-sensitive**: "ABC" and "abc" are different
- **Empty strings**: Returns 0 if either string is empty
- **Not found**: Returns 0 when substring doesn't exist
## Error Handling
```basic
text = HEAR "Enter text to search:"
search = HEAR "Enter search term:"
pos = INSTR(text, search)
IF pos = 0 THEN
TALK "'" + search + "' was not found in your text"
ELSE
TALK "Found at position " + pos
END IF
```
## See Also
- [FORMAT](./keyword-format.md) - String formatting
- [SET](./keyword-set.md) - Variable assignment
- [IS NUMERIC](./keyword-is-numeric.md) - Check if string is numeric

View file

@ -0,0 +1,214 @@
# IS NUMERIC
The `IS NUMERIC` function tests whether a string value can be converted to a number. This is essential for input validation before performing mathematical operations.
## Syntax
```basic
result = IS NUMERIC(value)
```
## Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `value` | string | The value to test for numeric content |
## Return Value
- Returns `true` if the value can be parsed as a number
- Returns `false` if the value contains non-numeric characters
## Description
`IS NUMERIC` examines a string to determine if it represents a valid numeric value. It recognizes:
- Integers: `42`, `-17`, `0`
- Decimals: `3.14`, `-0.5`, `.25`
- Scientific notation: `1e10`, `2.5E-3`
Empty strings and strings containing letters or special characters (except `-`, `.`, `e`, `E`) return `false`.
## Examples
### Basic Validation
```basic
input = HEAR "Enter a number:"
IF IS NUMERIC(input) THEN
TALK "You entered: " + input
ELSE
TALK "That's not a valid number"
END IF
```
### Bot Memory with Default Value
```basic
max_items = GET BOT MEMORY "max_items"
IF max_items = "" OR NOT IS NUMERIC(max_items) THEN
max_items = "10"
END IF
TALK "Processing up to " + max_items + " items"
```
### Input Loop Until Valid
```basic
valid = false
DO WHILE NOT valid
age = HEAR "Enter your age:"
IF IS NUMERIC(age) THEN
valid = true
ELSE
TALK "Please enter a number"
END IF
LOOP
TALK "Your age is " + age
```
### Combined Conditions with OR NOT
```basic
quantity = HEAR "How many items?"
IF quantity = "" OR NOT IS NUMERIC(quantity) THEN
TALK "Invalid quantity, using default of 1"
quantity = "1"
END IF
```
### Validating Multiple Fields
```basic
price = HEAR "Enter price:"
quantity = HEAR "Enter quantity:"
IF IS NUMERIC(price) AND IS NUMERIC(quantity) THEN
total = price * quantity
TALK "Total: $" + total
ELSE
IF NOT IS NUMERIC(price) THEN
TALK "Price must be a number"
END IF
IF NOT IS NUMERIC(quantity) THEN
TALK "Quantity must be a number"
END IF
END IF
```
### Configuration Validation
```basic
' Load timeout from config, validate it's numeric
timeout = GET BOT MEMORY "api_timeout"
IF NOT IS NUMERIC(timeout) THEN
timeout = "30"
SET BOT MEMORY "api_timeout", timeout
TALK "Set default timeout to 30 seconds"
END IF
```
### Range Checking After Validation
```basic
rating = HEAR "Rate from 1-5:"
IF NOT IS NUMERIC(rating) THEN
TALK "Please enter a number"
ELSE IF rating < 1 OR rating > 5 THEN
TALK "Rating must be between 1 and 5"
ELSE
TALK "Thank you for your rating of " + rating
SET BOT MEMORY "last_rating", rating
END IF
```
## What IS NUMERIC Accepts
| Input | Result | Notes |
|-------|--------|-------|
| `"42"` | `true` | Integer |
| `"-17"` | `true` | Negative integer |
| `"3.14"` | `true` | Decimal |
| `".5"` | `true` | Leading decimal |
| `"1e10"` | `true` | Scientific notation |
| `"2.5E-3"` | `true` | Scientific with decimal |
| `""` | `false` | Empty string |
| `"abc"` | `false` | Letters |
| `"12abc"` | `false` | Mixed content |
| `"$100"` | `false` | Currency symbol |
| `"1,000"` | `false` | Thousands separator |
| `" 42 "` | `true` | Whitespace trimmed |
## Common Patterns
### Default Value Pattern
```basic
value = GET BOT MEMORY key
IF value = "" OR NOT IS NUMERIC(value) THEN
value = default_value
END IF
```
### Safe Division
```basic
divisor = HEAR "Enter divisor:"
IF NOT IS NUMERIC(divisor) THEN
TALK "Must be a number"
ELSE IF divisor = 0 THEN
TALK "Cannot divide by zero"
ELSE
result = 100 / divisor
TALK "Result: " + result
END IF
```
### Percentage Validation
```basic
percent = HEAR "Enter percentage (0-100):"
IF IS NUMERIC(percent) THEN
IF percent >= 0 AND percent <= 100 THEN
TALK "Discount: " + percent + "%"
ELSE
TALK "Must be between 0 and 100"
END IF
ELSE
TALK "Enter a number without %"
END IF
```
## Notes
- **Whitespace**: Leading and trailing spaces are trimmed before checking
- **Empty strings**: Always return `false`
- **Locale**: Uses period (.) as decimal separator
- **Currency**: Does not recognize currency symbols ($, €, etc.)
- **Separators**: Does not recognize thousands separators (commas)
## Error Prevention
Using `IS NUMERIC` prevents runtime errors when converting strings to numbers:
```basic
' Without validation - could cause error
value = HEAR "Enter number:"
result = value * 2 ' Error if value is "abc"
' With validation - safe
value = HEAR "Enter number:"
IF IS NUMERIC(value) THEN
result = value * 2
ELSE
TALK "Invalid input"
END IF
```
## See Also
- [GET BOT MEMORY](./keyword-get-bot-memory.md) - Retrieve stored values
- [SET BOT MEMORY](./keyword-set-bot-memory.md) - Store values
- [INSTR](./keyword-instr.md) - Find substring position
- [FORMAT](./keyword-format.md) - Format numbers as strings

View file

@ -0,0 +1,249 @@
# SWITCH
The `SWITCH` statement provides multi-way branching based on a value, allowing clean handling of multiple conditions without nested IF statements.
## Syntax
```basic
SWITCH expression
CASE value1
' statements for value1
CASE value2
' statements for value2
CASE value3, value4
' statements for value3 or value4
DEFAULT
' statements if no case matches
END SWITCH
```
## Parameters
| Element | Description |
|---------|-------------|
| `expression` | The value to evaluate |
| `CASE value` | A specific value to match |
| `CASE value1, value2` | Multiple values for the same case |
| `DEFAULT` | Optional fallback when no case matches |
## Description
`SWITCH` evaluates an expression once and compares it against multiple `CASE` values. When a match is found, the corresponding statements execute. Unlike some languages, General Bots BASIC does not require explicit `BREAK` statements - execution automatically stops after the matched case.
If no case matches and a `DEFAULT` block exists, those statements execute. If no case matches and there's no `DEFAULT`, execution continues after `END SWITCH`.
## Examples
### Role-Based Knowledge Base Selection
```basic
role = GET role
SWITCH role
CASE "manager"
USE KB "management"
USE KB "reports"
CASE "developer"
USE KB "documentation"
USE KB "apis"
CASE "customer"
USE KB "products"
USE KB "support"
DEFAULT
USE KB "general"
END SWITCH
```
### Menu Navigation
```basic
TALK "Select an option:"
TALK "1. Check balance"
TALK "2. Transfer funds"
TALK "3. View history"
TALK "4. Exit"
choice = HEAR "Enter your choice:"
SWITCH choice
CASE "1"
balance = GET BOT MEMORY "balance"
TALK "Your balance is: $" + balance
CASE "2"
TALK "Transfer initiated..."
' Transfer logic here
CASE "3"
history = FIND "recent transactions"
TALK history
CASE "4"
TALK "Goodbye!"
DEFAULT
TALK "Invalid option. Please choose 1-4."
END SWITCH
```
### Multiple Values Per Case
```basic
day = GET day_of_week
SWITCH day
CASE "monday", "tuesday", "wednesday", "thursday", "friday"
TALK "It's a weekday. Office hours: 9am-5pm"
CASE "saturday", "sunday"
TALK "It's the weekend. We're closed."
DEFAULT
TALK "Unknown day"
END SWITCH
```
### Language Selection
```basic
lang = GET user_language
SWITCH lang
CASE "en"
TALK "Hello! How can I help you today?"
CASE "es"
TALK "¡Hola! ¿Cómo puedo ayudarte hoy?"
CASE "pt"
TALK "Olá! Como posso ajudá-lo hoje?"
CASE "fr"
TALK "Bonjour! Comment puis-je vous aider?"
DEFAULT
TALK "Hello! How can I help you today?"
END SWITCH
```
### Department Routing
```basic
department = HEAR "Which department? (sales, support, billing)"
SWITCH department
CASE "sales"
SET CONTEXT "You are a sales assistant. Focus on products and pricing."
USE KB "products"
USE KB "pricing"
CASE "support"
SET CONTEXT "You are a technical support agent. Help resolve issues."
USE KB "troubleshooting"
USE KB "faq"
CASE "billing"
SET CONTEXT "You are a billing specialist. Handle payment questions."
USE KB "invoices"
USE KB "payment_methods"
DEFAULT
TALK "I'll connect you with general assistance."
USE KB "general"
END SWITCH
```
### Status Code Handling
```basic
status = GET api_response_status
SWITCH status
CASE "200"
TALK "Request successful!"
CASE "400"
TALK "Bad request. Please check your input."
CASE "401", "403"
TALK "Authentication error. Please log in again."
CASE "404"
TALK "Resource not found."
CASE "500", "502", "503"
TALK "Server error. Please try again later."
DEFAULT
TALK "Unexpected status: " + status
END SWITCH
```
### Numeric Ranges (Using Categories)
```basic
score = GET test_score
grade = ""
' Convert score to grade category
IF score >= 90 THEN
grade = "A"
ELSE IF score >= 80 THEN
grade = "B"
ELSE IF score >= 70 THEN
grade = "C"
ELSE IF score >= 60 THEN
grade = "D"
ELSE
grade = "F"
END IF
SWITCH grade
CASE "A"
TALK "Excellent work!"
SET BOT MEMORY "achievement", "honor_roll"
CASE "B"
TALK "Good job!"
CASE "C"
TALK "Satisfactory performance."
CASE "D"
TALK "You passed, but could improve."
CASE "F"
TALK "Please see a tutor for help."
END SWITCH
```
## Comparison with IF-ELSE
### Using IF-ELSE (Verbose)
```basic
IF color = "red" THEN
TALK "Stop"
ELSE IF color = "yellow" THEN
TALK "Caution"
ELSE IF color = "green" THEN
TALK "Go"
ELSE
TALK "Unknown signal"
END IF
```
### Using SWITCH (Cleaner)
```basic
SWITCH color
CASE "red"
TALK "Stop"
CASE "yellow"
TALK "Caution"
CASE "green"
TALK "Go"
DEFAULT
TALK "Unknown signal"
END SWITCH
```
## Notes
- **No fall-through**: Each CASE is isolated; no BREAK needed
- **Case sensitivity**: String comparisons are case-sensitive
- **Expression evaluated once**: The switch expression is evaluated only once
- **DEFAULT is optional**: Without DEFAULT, unmatched values skip the block
- **Multiple values**: Use commas to match multiple values in one CASE
## Best Practices
1. **Always include DEFAULT** for robust error handling
2. **Use meaningful case values** that are self-documenting
3. **Order cases logically** - most common first or alphabetically
4. **Keep case blocks concise** - extract complex logic to separate scripts
## See Also
- [SET CONTEXT](./keyword-set-context.md) - Set conversation context
- [USE KB](./keyword-use-kb.md) - Load knowledge base
- [GET](./keyword-get.md) - Get variable values
- [IF/THEN/ELSE](./keyword-if.md) - Conditional branching

View file

@ -0,0 +1,219 @@
# WEBHOOK
Creates a webhook endpoint for event-driven automation. When the webhook URL is called, the script containing the WEBHOOK declaration is executed.
## Syntax
```basic
WEBHOOK "endpoint-name"
```
## Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| endpoint-name | String | The unique name for the webhook endpoint (alphanumeric, hyphens, underscores) |
## Description
The WEBHOOK keyword registers an HTTP endpoint that triggers script execution when called externally. This enables event-driven automation where external systems can notify your bot of events.
The webhook creates an endpoint at:
```
POST /api/{botname}/webhook/{endpoint-name}
```
When the webhook is triggered:
1. The script containing the WEBHOOK declaration executes
2. Request data is available through special variables:
- `params` - Query string parameters (e.g., `?id=123`)
- `body` - JSON request body
- `headers` - HTTP headers
- `method` - HTTP method (usually POST)
## Example
### Basic Order Webhook
```basic
' order-received.bas
WEBHOOK "order-received"
' Access request data
order_id = params.order_id
customer_name = body.customer.name
customer_email = body.customer.email
total = body.total
items = body.items
' Log the order
PRINT "Received order: " + order_id
' Save to database
order_data = #{
"customer_name": customer_name,
"email": customer_email,
"total": total,
"status": "pending",
"created_at": NOW()
}
SAVE "orders", order_id, order_data
' Send confirmation email
SEND MAIL customer_email, "Order Confirmation", "Thank you for your order #" + order_id
' Return response (optional - script return value becomes response)
result = #{ "status": "ok", "order_id": order_id, "message": "Order received" }
```
### Calling the Webhook
```bash
curl -X POST https://bot.example.com/api/mybot/webhook/order-received \
-H "Content-Type: application/json" \
-d '{
"customer": {
"name": "John Doe",
"email": "john@example.com"
},
"items": [
{"product": "Widget", "qty": 2, "price": 29.99}
],
"total": 59.98
}'
```
### Payment Notification Webhook
```basic
' payment-webhook.bas
WEBHOOK "payment-notification"
' Verify webhook signature (if provided)
signature = headers.x_webhook_signature
IF signature = "" THEN
PRINT "Warning: No signature provided"
END IF
' Process payment event
event_type = body.event
payment_id = body.payment.id
amount = body.payment.amount
status = body.payment.status
SWITCH event_type
CASE "payment.completed"
UPDATE "orders", "payment_id=" + payment_id, #{ "status": "paid", "paid_at": NOW() }
TALK "Payment " + payment_id + " completed"
CASE "payment.failed"
UPDATE "orders", "payment_id=" + payment_id, #{ "status": "payment_failed" }
' Notify customer
order = FIND "orders", "payment_id=" + payment_id
SEND MAIL order.email, "Payment Failed", "Your payment could not be processed."
CASE "payment.refunded"
UPDATE "orders", "payment_id=" + payment_id, #{ "status": "refunded", "refunded_at": NOW() }
DEFAULT
PRINT "Unknown event type: " + event_type
END SWITCH
result = #{ "received": true }
```
### GitHub Webhook Integration
```basic
' github-webhook.bas
WEBHOOK "github-push"
' GitHub sends event type in header
event_type = headers.x_github_event
repository = body.repository.full_name
pusher = body.pusher.name
IF event_type = "push" THEN
branch = body.ref
commits = body.commits
commit_count = UBOUND(commits)
' Log the push
message = pusher + " pushed " + commit_count + " commit(s) to " + repository + " (" + branch + ")"
PRINT message
' Notify team via Slack/Teams
POST "https://hooks.slack.com/services/xxx", #{ "text": message }
' Trigger deployment if main branch
IF branch = "refs/heads/main" THEN
PRINT "Triggering deployment..."
POST "https://deploy.example.com/trigger", #{ "repo": repository }
END IF
END IF
result = #{ "status": "processed" }
```
## Response Handling
The webhook automatically returns a JSON response. You can control the response by setting a `result` variable:
```basic
WEBHOOK "my-endpoint"
' Process request...
' Simple success response
result = #{ "status": "ok" }
' Or with custom status code
result = #{
"status": 201,
"body": #{ "id": new_id, "created": true },
"headers": #{ "X-Custom-Header": "value" }
}
```
## Security Considerations
1. **Validate signatures**: Many services (Stripe, GitHub, etc.) sign webhook payloads
2. **Verify source**: Check request headers or IP addresses when possible
3. **Use HTTPS**: Always use HTTPS endpoints in production
4. **Idempotency**: Design webhooks to handle duplicate deliveries gracefully
```basic
WEBHOOK "secure-webhook"
' Verify HMAC signature
expected_signature = HASH(body, secret_key, "sha256")
IF headers.x_signature != expected_signature THEN
PRINT "Invalid signature - rejecting request"
result = #{ "status": 401, "body": #{ "error": "Invalid signature" } }
EXIT
END IF
' Continue processing...
```
## Use Cases
- **E-commerce**: Order notifications, payment confirmations, inventory updates
- **CI/CD**: Build notifications, deployment triggers
- **CRM**: Lead notifications, deal updates
- **IoT**: Sensor data ingestion, device status updates
- **Third-party integrations**: Slack commands, form submissions, calendar events
## Notes
- Webhook endpoints are registered during script compilation
- Multiple scripts can define different webhooks
- Webhooks are stored in the `system_automations` table
- The endpoint name must be unique per bot
- Request timeout is typically 30 seconds - keep processing fast
## See Also
- [SET SCHEDULE](./keyword-set-schedule.md) - Time-based automation
- [ON](./keyword-on.md) - Database trigger events
- [POST](./keyword-post.md) - Making outbound HTTP requests

View file

@ -63,6 +63,7 @@ The source code for each keyword lives in `src/basic/keywords/`. Only the keywor
- [EXIT FOR](./keyword-exit-for.md) - Exit loop early
- [ON](./keyword-on.md) - Event handler
- [SET SCHEDULE](./keyword-set-schedule.md) - Schedule execution
- [WEBHOOK](./keyword-webhook.md) - Create webhook endpoint
## Communication
@ -75,9 +76,586 @@ The source code for each keyword lives in `src/basic/keywords/`. Only the keywor
- [REMEMBER](./keyword-remember.md) - Store in memory
- [WEATHER](./keyword-weather.md) - Get weather info
---
## HTTP & API Operations
These keywords enable integration with external REST APIs, GraphQL endpoints, and SOAP services.
### POST
Sends an HTTP POST request with JSON body.
**Syntax:**
```basic
result = POST "url", data
```
**Parameters:**
- `url` - The endpoint URL (string)
- `data` - The request body (object or map)
**Example:**
```basic
payload = #{ "name": "John", "email": "john@example.com" }
response = POST "https://api.example.com/users", payload
TALK "Created user with ID: " + response.data.id
```
### PUT
Sends an HTTP PUT request to update a resource.
**Syntax:**
```basic
result = PUT "url", data
```
**Example:**
```basic
updates = #{ "name": "John Updated" }
response = PUT "https://api.example.com/users/123", updates
```
### PATCH
Sends an HTTP PATCH request for partial updates.
**Syntax:**
```basic
result = PATCH "url", data
```
**Example:**
```basic
partial = #{ "status": "active" }
response = PATCH "https://api.example.com/users/123", partial
```
### DELETE_HTTP
Sends an HTTP DELETE request.
**Syntax:**
```basic
result = DELETE_HTTP "url"
```
**Example:**
```basic
response = DELETE_HTTP "https://api.example.com/users/123"
```
### SET_HEADER
Sets an HTTP header for subsequent requests.
**Syntax:**
```basic
SET_HEADER "header-name", "value"
```
**Example:**
```basic
SET_HEADER "Authorization", "Bearer " + token
SET_HEADER "X-Custom-Header", "custom-value"
response = GET "https://api.example.com/protected"
```
### CLEAR_HEADERS
Clears all previously set HTTP headers.
**Syntax:**
```basic
CLEAR_HEADERS
```
### GRAPHQL
Executes a GraphQL query.
**Syntax:**
```basic
result = GRAPHQL "endpoint", "query", variables
```
**Example:**
```basic
query = "query GetUser($id: ID!) { user(id: $id) { name email } }"
vars = #{ "id": "123" }
response = GRAPHQL "https://api.example.com/graphql", query, vars
TALK "User name: " + response.data.user.name
```
### SOAP
Executes a SOAP API call.
**Syntax:**
```basic
result = SOAP "wsdl_url", "operation", params
```
**Example:**
```basic
params = #{ "customerId": "12345", "amount": 100.00 }
response = SOAP "https://api.example.com/service.wsdl", "CreateOrder", params
```
---
## Database & Data Operations
These keywords provide comprehensive data manipulation capabilities.
### SAVE
Saves data to a table (upsert - inserts if new, updates if exists).
**Syntax:**
```basic
SAVE "table", id, data
```
**Parameters:**
- `table` - Table name (string)
- `id` - Record identifier
- `data` - Data object to save
**Example:**
```basic
customer = #{ "name": "John", "email": "john@example.com", "status": "active" }
SAVE "customers", "cust-001", customer
```
### INSERT
Inserts a new record into a table.
**Syntax:**
```basic
result = INSERT "table", data
```
**Example:**
```basic
order = #{ "product": "Widget", "quantity": 5, "price": 29.99 }
result = INSERT "orders", order
TALK "Created order: " + result.id
```
### UPDATE
Updates records matching a filter.
**Syntax:**
```basic
rows = UPDATE "table", "filter", data
```
**Parameters:**
- `table` - Table name
- `filter` - Filter condition (e.g., "id=123" or "status=pending")
- `data` - Fields to update
**Example:**
```basic
updates = #{ "status": "completed", "completed_at": NOW() }
rows = UPDATE "orders", "id=ord-123", updates
TALK "Updated " + rows + " record(s)"
```
### DELETE
Deletes records from a table matching the filter.
**Syntax:**
```basic
rows = DELETE "table", "filter"
```
**Example:**
```basic
rows = DELETE "orders", "status=cancelled"
TALK "Deleted " + rows + " cancelled orders"
```
### MERGE
Merges data into a table using a key field for matching.
**Syntax:**
```basic
result = MERGE "table", data, "key_field"
```
**Example:**
```basic
' Import customers from external source
customers = GET "https://api.external.com/customers"
result = MERGE "customers", customers, "email"
TALK "Inserted: " + result.inserted + ", Updated: " + result.updated
```
### FILL
Transforms data by filling a template with values.
**Syntax:**
```basic
result = FILL data, template
```
**Example:**
```basic
data = [#{ "name": "John", "amount": 100 }, #{ "name": "Jane", "amount": 200 }]
template = #{ "greeting": "Hello {{name}}", "total": "Amount: ${{amount}}" }
filled = FILL data, template
```
### MAP
Maps field names from source to destination.
**Syntax:**
```basic
result = MAP data, "old_field->new_field, ..."
```
**Example:**
```basic
data = [#{ "firstName": "John", "lastName": "Doe" }]
mapped = MAP data, "firstName->name, lastName->surname"
' Result: [#{ "name": "John", "surname": "Doe" }]
```
### FILTER
Filters records based on a condition.
**Syntax:**
```basic
result = FILTER data, "condition"
```
**Supported operators:** `=`, `!=`, `>`, `<`, `>=`, `<=`, `like`
**Example:**
```basic
orders = FIND "orders.xlsx"
active = FILTER orders, "status=active"
highValue = FILTER orders, "amount>1000"
matches = FILTER orders, "name like john"
```
### AGGREGATE
Performs aggregation operations on data.
**Syntax:**
```basic
result = AGGREGATE "operation", data, "field"
```
**Operations:** `SUM`, `AVG`, `COUNT`, `MIN`, `MAX`
**Example:**
```basic
orders = FIND "orders.xlsx"
total = AGGREGATE "SUM", orders, "amount"
average = AGGREGATE "AVG", orders, "amount"
count = AGGREGATE "COUNT", orders, "id"
TALK "Total: $" + total + ", Average: $" + average + ", Count: " + count
```
### JOIN
Joins two datasets on a key field (inner join).
**Syntax:**
```basic
result = JOIN left_data, right_data, "key_field"
```
**Example:**
```basic
orders = FIND "orders.xlsx"
customers = FIND "customers.xlsx"
joined = JOIN orders, customers, "customer_id"
```
### PIVOT
Creates a pivot table from data.
**Syntax:**
```basic
result = PIVOT data, "row_field", "value_field"
```
**Example:**
```basic
sales = FIND "sales.xlsx"
byMonth = PIVOT sales, "month", "amount"
' Result: Each unique month with sum of amounts
```
### GROUP_BY
Groups data by a field.
**Syntax:**
```basic
result = GROUP_BY data, "field"
```
**Example:**
```basic
orders = FIND "orders.xlsx"
byStatus = GROUP_BY orders, "status"
' Result: Map with keys for each status value
```
---
## File & Document Operations
These keywords handle file operations within the `.gbdrive` storage.
### READ
Reads content from a file.
**Syntax:**
```basic
content = READ "path"
```
**Example:**
```basic
config = READ "config/settings.json"
data = READ "reports/daily.csv"
```
### WRITE
Writes content to a file.
**Syntax:**
```basic
WRITE "path", data
```
**Example:**
```basic
report = #{ "date": TODAY(), "total": 1500 }
WRITE "reports/summary.json", report
WRITE "logs/activity.txt", "User logged in at " + NOW()
```
### DELETE_FILE
Deletes a file from storage.
**Syntax:**
```basic
DELETE_FILE "path"
```
**Example:**
```basic
DELETE_FILE "temp/old-report.pdf"
```
### COPY
Copies a file to a new location.
**Syntax:**
```basic
COPY "source", "destination"
```
**Example:**
```basic
COPY "templates/invoice.docx", "customers/john/invoice-001.docx"
```
### MOVE
Moves or renames a file.
**Syntax:**
```basic
MOVE "source", "destination"
```
**Example:**
```basic
MOVE "inbox/new-file.pdf", "processed/file-001.pdf"
```
### LIST
Lists contents of a directory.
**Syntax:**
```basic
files = LIST "path"
```
**Example:**
```basic
reports = LIST "reports/"
FOR EACH file IN reports
TALK "Found: " + file
NEXT file
```
### COMPRESS
Creates a ZIP archive from files.
**Syntax:**
```basic
archive = COMPRESS files, "archive_name.zip"
```
**Example:**
```basic
files = ["report1.pdf", "report2.pdf", "data.xlsx"]
archive = COMPRESS files, "monthly-reports.zip"
```
### EXTRACT
Extracts an archive to a destination folder.
**Syntax:**
```basic
files = EXTRACT "archive.zip", "destination/"
```
**Example:**
```basic
extracted = EXTRACT "uploads/documents.zip", "processed/"
TALK "Extracted " + UBOUND(extracted) + " files"
```
### UPLOAD
Uploads a file to storage.
**Syntax:**
```basic
url = UPLOAD file, "destination_path"
```
**Example:**
```basic
HEAR attachment AS FILE
url = UPLOAD attachment, "uploads/" + attachment.filename
TALK "File uploaded to: " + url
```
### DOWNLOAD
Downloads a file from a URL.
**Syntax:**
```basic
path = DOWNLOAD "url", "local_path"
```
**Example:**
```basic
path = DOWNLOAD "https://example.com/report.pdf", "downloads/report.pdf"
TALK "Downloaded to: " + path
```
### GENERATE_PDF
Generates a PDF from a template with data.
**Syntax:**
```basic
result = GENERATE_PDF "template", data, "output.pdf"
```
**Example:**
```basic
invoice = #{ "customer": "John", "items": items, "total": 299.99 }
pdf = GENERATE_PDF "templates/invoice.html", invoice, "invoices/inv-001.pdf"
TALK "PDF generated: " + pdf.url
```
### MERGE_PDF
Merges multiple PDF files into one.
**Syntax:**
```basic
result = MERGE_PDF files, "merged.pdf"
```
**Example:**
```basic
pdfs = ["cover.pdf", "chapter1.pdf", "chapter2.pdf", "appendix.pdf"]
merged = MERGE_PDF pdfs, "book.pdf"
```
---
## Webhook & Event-Driven Automation
### WEBHOOK
Creates a webhook endpoint that triggers script execution when called.
**Syntax:**
```basic
WEBHOOK "endpoint-name"
```
This registers an endpoint at: `/api/{botname}/webhook/{endpoint-name}`
When the webhook is called, the script containing the WEBHOOK declaration executes. Request parameters are available as variables.
**Example:**
```basic
' order-webhook.bas
WEBHOOK "order-received"
' Access request data
order_id = params.order_id
customer = body.customer_name
amount = body.total
' Process the order
SAVE "orders", order_id, body
' Send confirmation
SEND MAIL customer.email, "Order Confirmed", "Your order " + order_id + " is confirmed."
' Return response
result = #{ "status": "ok", "order_id": order_id }
```
**Webhook Request:**
```bash
curl -X POST https://bot.example.com/api/mybot/webhook/order-received \
-H "Content-Type: application/json" \
-d '{"customer_name": "John", "total": 99.99}'
```
---
## Notes
- Keywords are case-insensitive (TALK = talk = Talk)
- String parameters can use double quotes or single quotes
- Comments start with REM or '
- Line continuation uses underscore (_)
- Line continuation uses underscore (_)
- Objects are created with `#{ key: value }` syntax
- Arrays use `[item1, item2, ...]` syntax

View file

@ -0,0 +1 @@
# Drive Integration

View file

@ -13,13 +13,12 @@ BotServer uses the drive component as its primary storage backend for:
## Configuration
The drive is configured through environment variables that are automatically generated during bootstrap:
Storage configuration is **automatically managed** by the Directory service (Zitadel). You do not need to configure storage credentials manually.
- `DRIVE_SERVER` - Drive endpoint URL (default: `http://localhost:9000`)
- `DRIVE_ACCESSKEY` - Access key for authentication
- `DRIVE_SECRET` - Secret key for authentication
These credentials are auto-generated with secure random values during the bootstrap process.
During bootstrap, the Directory service:
1. Provisions storage credentials
2. Distributes them securely to BotServer
3. Handles credential rotation
## Storage Structure
@ -27,16 +26,7 @@ These credentials are auto-generated with secure random values during the bootst
Each bot gets its own bucket named after the bot package:
```
announcements.gbai/ # Bucket for announcements bot
├── announcements.gbdialog/
│ ├── start.bas
│ └── auth.bas
├── announcements.gbkb/
│ └── documents/
└── announcements.gbot/
└── config.csv
```
<img src="../assets/directory-tree.svg" alt="Bot package structure" width="400" />
### Bucket Naming Convention
@ -74,7 +64,7 @@ During bootstrap, BotServer:
### 1. Installation
- Installs the drive binary if not present
- Configures with generated credentials
- Receives credentials from Directory service
- Creates data directories
- Uploads template files to drive
@ -113,30 +103,22 @@ The built-in console provides a file browser for drive:
/download/{bot}/{file} # Download specific file
```
## AWS SDK Configuration
## S3-Compatible Client Configuration
BotServer uses the AWS SDK S3 client configured for drive:
BotServer uses an S3-compatible client configured for the drive:
```rust
let config = aws_config::from_env()
let config = S3Config::builder()
.endpoint_url(&drive_endpoint)
.region("us-east-1")
.load()
.await;
.region("us-east-1") // Required but arbitrary for S3-compatible
.force_path_style(true)
.build();
```
This is configured with `force_path_style(true)` for compatibility with S3-compatible storage.
The `force_path_style(true)` setting ensures compatibility with S3-compatible storage providers.
## Deployment Modes
### Cloud Storage
While the drive typically runs locally alongside BotServer, it can be configured to use:
- Remote S3-compatible instances
- AWS S3 (change endpoint URL)
- Azure Blob Storage (with S3 compatibility)
- Google Cloud Storage (with S3 compatibility)
### Local Mode
Default mode where drive runs on the same machine:
@ -144,20 +126,35 @@ Default mode where drive runs on the same machine:
- Data stored in `{{DATA_PATH}}`
- Logs written to `{{LOGS_PATH}}/drive.log`
### Container Mode
### Container Mode (LXC)
Drive can run in a container with mapped volumes for persistent storage.
Drive can run in an LXC container with mapped volumes for persistent storage:
### External Storage
```bash
lxc config device add default-drive data disk \
source=/opt/gbo/data path=/opt/gbo/data
```
Configure BotServer to use existing S3-compatible infrastructure by updating the drive configuration.
### External S3-Compatible Storage
BotServer can use existing S3-compatible infrastructure. The Directory service manages the connection:
**Supported Providers:**
- MinIO (default local installation)
- Backblaze B2
- Wasabi
- DigitalOcean Spaces
- Cloudflare R2
- Any S3-compatible service
To use external storage, configure it through the Directory service admin console.
## Security
- Access keys are generated with 32 random bytes
- Secret keys are generated with 64 random bytes
- Credentials are managed by the Directory service
- TLS can be enabled for secure communication
- Bucket policies control access per bot
- Credential rotation is handled automatically
## Monitoring
@ -171,7 +168,7 @@ Configure BotServer to use existing S3-compatible infrastructure by updating the
### Check Drive Status
The package manager monitors drive status with:
```
```bash
ps -ef | grep drive | grep -v grep
```
@ -186,13 +183,19 @@ Drive console available at `http://localhost:9001` for:
## Common Issues
1. **Connection Failed**: Check drive is running and ports are accessible
2. **Access Denied**: Verify credentials in environment variables
2. **Access Denied**: Verify Directory service has provisioned credentials
3. **Bucket Not Found**: Ensure bot deployment completed successfully
4. **Upload Failed**: Check disk space and permissions
### Debug Logging
Enable trace logging to see drive operations:
```bash
RUST_LOG=trace ./botserver
```
This shows:
- File retrieval details
- Bucket operations
- Authentication attempts
@ -203,4 +206,10 @@ Enable trace logging to see drive operations:
2. **Monitor Disk Usage**: Ensure adequate storage space
3. **Access Control**: Use bucket policies to restrict access
4. **Versioning**: Enable object versioning for critical data
5. **Lifecycle Policies**: Configure automatic cleanup for old files
5. **Lifecycle Policies**: Configure automatic cleanup for old files
## See Also
- [Storage API](../chapter-10-api/storage-api.md) - API reference
- [Environment Variables](../appendix-env-vars/README.md) - Directory service configuration
- [LXC Containers](../chapter-07-gbapp/containers.md) - Container deployment

View file

@ -4,17 +4,17 @@ The Conversations API provides endpoints for managing chat conversations, messag
## Overview
**Note**: These endpoints are planned but not yet implemented. They represent the intended API design for conversation management.
Conversations in General Bots are handled primarily through WebSocket connections for real-time messaging, with REST endpoints for history retrieval and session management.
## Planned Endpoints
## Endpoints
### Start Conversation
**POST** `/conversations/start`
**POST** `/api/conversations/start`
Initiates a new conversation with a bot.
**Planned Request:**
**Request:**
```json
{
"bot_id": "bot-123",
@ -22,7 +22,7 @@ Initiates a new conversation with a bot.
}
```
**Planned Response:**
**Response:**
```json
{
"conversation_id": "conv-456",
@ -33,11 +33,11 @@ Initiates a new conversation with a bot.
### Send Message
**POST** `/conversations/:id/messages`
**POST** `/api/conversations/:id/messages`
Sends a message in an existing conversation.
**Planned Request:**
**Request:**
```json
{
"content": "User message",
@ -45,66 +45,175 @@ Sends a message in an existing conversation.
}
```
**Response:**
```json
{
"message_id": "msg-123",
"timestamp": "2024-01-15T10:30:00Z",
"status": "delivered"
}
```
### Get Conversation History
**GET** `/conversations/:id/history`
**GET** `/api/conversations/:id/history`
Retrieves message history for a conversation.
**Planned Query Parameters:**
- `limit` - Number of messages
**Query Parameters:**
- `limit` - Number of messages (default: 50, max: 100)
- `before` - Messages before timestamp
- `after` - Messages after timestamp
**Response:**
```json
{
"messages": [
{
"id": "msg-001",
"sender": "user",
"content": "Hello",
"timestamp": "2024-01-15T10:00:00Z"
},
{
"id": "msg-002",
"sender": "bot",
"content": "Hi! How can I help you?",
"timestamp": "2024-01-15T10:00:01Z"
}
],
"has_more": false
}
```
### List Conversations
**GET** `/conversations`
**GET** `/api/conversations`
Lists user's conversations.
**Planned Query Parameters:**
**Query Parameters:**
- `bot_id` - Filter by bot
- `status` - Filter by status (active/archived)
- `limit` - Number of results
- `offset` - Pagination offset
## Current Implementation
Currently, conversations are handled through:
- WebSocket connections at `/ws`
- Session management in database
- Message history stored in `message_history` table
Real-time messaging is functional but REST endpoints for conversation management are not yet implemented.
**Response:**
```json
{
"conversations": [
{
"id": "conv-456",
"bot_id": "bot-123",
"bot_name": "Support Bot",
"last_message": "Thank you!",
"last_activity": "2024-01-15T10:30:00Z",
"status": "active"
}
],
"total": 1
}
```
## WebSocket Protocol
The current implementation uses WebSocket for real-time conversations:
Real-time messaging uses WebSocket connections at `/ws`.
```javascript
// Connect
ws = new WebSocket('ws://localhost:8080/ws');
### Message Types
// Send message
ws.send(JSON.stringify({
type: 'message',
content: 'Hello',
session_id: 'session-123'
}));
| Type | Direction | Description |
|------|-----------|-------------|
| `message` | Both | Chat message |
| `typing` | Server→Client | Bot is typing |
| `suggestion` | Server→Client | Quick reply suggestions |
| `status` | Server→Client | Connection status |
| `error` | Server→Client | Error notification |
// Receive response
ws.onmessage = (event) => {
const response = JSON.parse(event.data);
console.log(response.content);
};
### Send Message Format
```json
{
"type": "message",
"content": "Hello",
"session_id": "session-123"
}
```
## Future Implementation
### Receive Message Format
These REST endpoints will be added to provide:
- Conversation management
- History retrieval
- Batch operations
- Analytics integration
```json
{
"type": "message",
"sender": "bot",
"content": "Hi! How can I help you?",
"timestamp": "2024-01-15T10:00:01Z"
}
```
## Status
## Anonymous Conversations
**Not Implemented** - Use WebSocket connection for conversations.
Anonymous users can chat without authentication:
- Session created automatically on WebSocket connect
- Limited to default bot only
- No history persistence
- Session expires after inactivity
## Authenticated Conversations
Logged-in users get additional features:
- Full conversation history
- Multiple bot access
- Cross-device sync
- Persistent sessions
## Database Schema
Conversations are stored in:
```sql
-- sessions table
CREATE TABLE sessions (
id UUID PRIMARY KEY,
user_id UUID,
bot_id UUID,
status TEXT,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);
-- message_history table
CREATE TABLE message_history (
id UUID PRIMARY KEY,
session_id UUID REFERENCES sessions(id),
sender TEXT,
content TEXT,
metadata JSONB,
created_at TIMESTAMPTZ
);
```
## Error Handling
| Status Code | Error | Description |
|-------------|-------|-------------|
| 400 | `invalid_message` | Malformed message content |
| 401 | `unauthorized` | Authentication required |
| 403 | `forbidden` | No access to conversation |
| 404 | `not_found` | Conversation doesn't exist |
| 429 | `rate_limited` | Too many messages |
## Rate Limits
| Endpoint | Limit |
|----------|-------|
| Messages | 60/minute per user |
| History | 100/minute per user |
| List | 30/minute per user |
## See Also
- [Sessions and Channels](../chapter-01/sessions.md) - Session management
- [TALK Keyword](../chapter-06-gbdialog/keyword-talk.md) - Sending messages from BASIC
- [HEAR Keyword](../chapter-06-gbdialog/keyword-hear.md) - Receiving user input

View file

@ -1,120 +1,393 @@
# Email API
The Email API provides endpoints for email operations including sending, receiving, and managing email accounts.
The Email API provides endpoints for email operations including sending, receiving, and managing email accounts through the Stalwart mail server integration.
## Status
## Overview
**Not Implemented** - Email functionality exists in BotServer through the email module and BASIC keywords, but REST API endpoints are not yet implemented.
Email functionality in General Bots is available through:
## Current Email Functionality
1. **REST API** - Documented in this chapter
2. **BASIC Keywords** - `SEND MAIL` for scripts
3. **Email Module** - Background processing and IMAP/SMTP integration
Email features are available through:
1. **BASIC Script Keywords**
```basic
SEND MAIL "recipient@example.com", "Subject", "Body"
```
2. **Email Module** (when feature enabled)
- IMAP/SMTP integration
- Database storage for email accounts
- Draft management
## Planned Endpoints
## Endpoints
### Send Email
**POST** `/api/email/send` (Planned)
**POST** `/api/email/send`
Send an email message.
**Request:**
```json
{
"to": ["recipient@example.com"],
"cc": ["cc@example.com"],
"bcc": [],
"subject": "Meeting Tomorrow",
"body": "Hi, just a reminder about our meeting.",
"body_type": "text",
"attachments": []
}
```
**Response:**
```json
{
"message_id": "msg-abc123",
"status": "sent",
"timestamp": "2024-01-15T10:30:00Z"
}
```
**Body Types:**
- `text` - Plain text
- `html` - HTML formatted
### List Emails
**GET** `/api/email/inbox` (Planned)
**GET** `/api/email/inbox`
Retrieve inbox messages.
**Query Parameters:**
- `folder` - Folder name (default: INBOX)
- `limit` - Number of messages (default: 50)
- `offset` - Pagination offset
- `unread` - Filter unread only (boolean)
- `since` - Messages since date (ISO 8601)
**Response:**
```json
{
"messages": [
{
"id": "email-001",
"from": "sender@example.com",
"subject": "Hello",
"preview": "Just wanted to say hi...",
"date": "2024-01-15T09:00:00Z",
"read": false,
"has_attachments": false
}
],
"total": 142,
"unread_count": 5
}
```
### Get Email
**GET** `/api/email/:id` (Planned)
**GET** `/api/email/:id`
Get specific email details.
**Response:**
```json
{
"id": "email-001",
"from": {
"name": "John Doe",
"email": "john@example.com"
},
"to": [
{
"name": "You",
"email": "you@example.com"
}
],
"cc": [],
"subject": "Meeting Notes",
"body": "Here are the notes from today's meeting...",
"body_html": "<p>Here are the notes from today's meeting...</p>",
"date": "2024-01-15T09:00:00Z",
"read": true,
"attachments": [
{
"id": "att-001",
"filename": "notes.pdf",
"size": 102400,
"content_type": "application/pdf"
}
]
}
```
### Delete Email
**DELETE** `/api/email/:id` (Planned)
**DELETE** `/api/email/:id`
Delete an email message.
### Email Accounts
**Response:**
```json
{
"status": "deleted",
"message_id": "email-001"
}
```
**GET** `/api/email/accounts` (Planned)
### Get Attachment
**GET** `/api/email/:id/attachments/:attachment_id`
Download an email attachment.
**Response:** Binary file with appropriate Content-Type header.
### Mark as Read
**PUT** `/api/email/:id/read`
Mark email as read.
**Request:**
```json
{
"read": true
}
```
### Move Email
**PUT** `/api/email/:id/move`
Move email to a different folder.
**Request:**
```json
{
"folder": "Archive"
}
```
### List Folders
**GET** `/api/email/folders`
List available email folders.
**Response:**
```json
{
"folders": [
{
"name": "INBOX",
"path": "INBOX",
"unread_count": 5,
"total_count": 142
},
{
"name": "Sent",
"path": "Sent",
"unread_count": 0,
"total_count": 89
},
{
"name": "Drafts",
"path": "Drafts",
"unread_count": 0,
"total_count": 3
}
]
}
```
### Create Draft
**POST** `/api/email/drafts`
Create an email draft.
**Request:**
```json
{
"to": ["recipient@example.com"],
"subject": "Draft subject",
"body": "Draft content..."
}
```
**Response:**
```json
{
"draft_id": "draft-001",
"status": "saved"
}
```
### Send Draft
**POST** `/api/email/drafts/:id/send`
Send a previously saved draft.
**Response:**
```json
{
"message_id": "msg-abc123",
"status": "sent"
}
```
## Email Accounts
### List Accounts
**GET** `/api/email/accounts`
List configured email accounts.
**POST** `/api/email/accounts` (Planned)
**Response:**
```json
{
"accounts": [
{
"id": "account-001",
"email": "user@example.com",
"provider": "stalwart",
"status": "connected"
}
]
}
```
Add new email account.
### Add Account
## Current Implementation
**POST** `/api/email/accounts`
Email functionality requires:
Add a new email account.
1. **Feature Flag**
```bash
cargo build --features email
```
**Request:**
```json
{
"email": "user@example.com",
"imap_server": "imap.example.com",
"imap_port": 993,
"smtp_server": "smtp.example.com",
"smtp_port": 587,
"username": "user@example.com",
"password": "app-specific-password"
}
```
2. **Environment Configuration**
```bash
EMAIL_IMAP_SERVER=imap.gmail.com
EMAIL_IMAP_PORT=993
EMAIL_USERNAME=your-email@example.com
EMAIL_PASSWORD=your-app-password
EMAIL_SMTP_SERVER=smtp.gmail.com
EMAIL_SMTP_PORT=587
```
**Response:**
```json
{
"account_id": "account-002",
"status": "connected",
"message": "Account added successfully"
}
```
3. **BASIC Scripts**
- Use SEND MAIL keyword
- Process emails in automation
## BASIC Integration
Use email in your BASIC scripts:
```basic
' Simple email
SEND MAIL "recipient@example.com", "Subject", "Body"
' With variables
recipient = HEAR "Who should I email?"
subject = HEAR "What's the subject?"
body = HEAR "What's the message?"
SEND MAIL recipient, subject, body
TALK "Email sent!"
```
## Configuration
Configure email in `config.csv`:
```csv
key,value
smtp-server,smtp.gmail.com
smtp-port,587
imap-server,imap.gmail.com
imap-port,993
email-username,your-email@gmail.com
email-password,your-app-password
email-from,Your Name <your-email@gmail.com>
```
**Gmail Configuration:**
- Use App Passwords (not your main password)
- Enable IMAP in Gmail settings
- Allow less secure apps or use OAuth
## Stalwart Mail Server
When using the built-in Stalwart mail server:
**Automatic Configuration:**
- Server runs on standard ports (25, 993, 587)
- Accounts created through Zitadel integration
- TLS certificates auto-managed
**Manual Configuration:**
```csv
key,value
stalwart-enabled,true
stalwart-domain,mail.yourdomain.com
stalwart-admin-password,secure-password
```
## Error Handling
| Status Code | Error | Description |
|-------------|-------|-------------|
| 400 | `invalid_recipient` | Invalid email address |
| 401 | `unauthorized` | Authentication required |
| 403 | `forbidden` | No access to mailbox |
| 404 | `not_found` | Email not found |
| 422 | `send_failed` | SMTP delivery failed |
| 503 | `service_unavailable` | Mail server offline |
## Rate Limits
| Endpoint | Limit |
|----------|-------|
| Send | 100/hour per user |
| Inbox | 300/hour per user |
| Attachments | 50/hour per user |
## Security Notes
1. **Never hardcode credentials** - Use config.csv
2. **Use App Passwords** - Not main account passwords
3. **Enable TLS** - Always use encrypted connections
4. **Audit sending** - Log all outbound emails
## Database Schema
Email data is stored in:
- `user_email_accounts` - Email account configurations
- `email_drafts` - Draft emails
- `email_folders` - Folder organization
```sql
-- user_email_accounts
CREATE TABLE user_email_accounts (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id),
email TEXT NOT NULL,
imap_server TEXT,
smtp_server TEXT,
encrypted_password TEXT,
created_at TIMESTAMPTZ
);
## Using Email Today
-- email_drafts
CREATE TABLE email_drafts (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id),
recipients JSONB,
subject TEXT,
body TEXT,
attachments JSONB,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);
```
To use email functionality:
## See Also
1. **Enable Feature**
- Build with email feature flag
- Configure IMAP/SMTP settings
2. **Use in BASIC Scripts**
```basic
# Send email
SEND MAIL "user@example.com", "Hello", "Message body"
# Automated email processing
let emails = GET_EMAILS("INBOX", "UNSEEN")
FOR EACH email IN emails {
# Process email
}
```
## Future Implementation
When implemented, the Email API will provide:
- RESTful email operations
- Attachment handling
- Template management
- Batch operations
- Webhook notifications
## Summary
While email functionality exists in BotServer through the email module and BASIC scripting, REST API endpoints are not yet implemented. Use BASIC scripts for email automation until the API is available.
- [SEND MAIL Keyword](../chapter-06-gbdialog/keyword-send-mail.md) - BASIC email
- [CREATE DRAFT Keyword](../chapter-06-gbdialog/keyword-create-draft.md) - Draft creation
- [External Services](../appendix-external-services/README.md) - Service configuration

View file

@ -1,132 +1,451 @@
# Users API
The Users API provides endpoints for user management operations. Currently, user management is handled entirely through Zitadel, with minimal direct API endpoints in BotServer.
The Users API provides endpoints for user management operations. User authentication is handled through Zitadel, with BotServer maintaining session associations and user preferences.
## Status
## Overview
**Partially Implemented** - User data is managed by Zitadel. BotServer only maintains session associations.
User management in General Bots follows a federated model:
## Current Implementation
- **Zitadel**: Primary identity provider (authentication, SSO, user creation)
- **BotServer**: Session management, preferences, bot-specific user data
## Endpoints
### Get Current User
**GET** `/api/users/me`
Returns current authenticated user information from session.
Returns current authenticated user information.
Headers:
- `Authorization: Bearer {session_token}`
**Headers:**
```
Authorization: Bearer {session_token}
```
Response:
**Response:**
```json
{
"user_id": "user-123",
"username": "john_doe",
"email": "john@example.com",
"display_name": "John Doe",
"avatar_url": "/api/users/user-123/avatar",
"roles": ["user", "manager"],
"created_at": "2024-01-01T00:00:00Z",
"last_login": "2024-01-15T10:30:00Z"
}
```
### Get User by ID
**GET** `/api/users/:id`
Retrieve specific user details.
**Required Permission:** `admin:users` or same user
**Response:**
```json
{
"user_id": "user-123",
"username": "john_doe",
"email": "john@example.com",
"display_name": "John Doe",
"status": "active",
"created_at": "2024-01-01T00:00:00Z"
}
```
Note: This data is cached from Zitadel and may not be real-time.
## Planned Endpoints (Not Implemented)
The following endpoints are planned but not yet implemented:
### List Users
**GET** `/api/users` (Planned)
**GET** `/api/users`
Would list users in the organization.
List users in the organization.
### Get User by ID
**Required Permission:** `admin:users`
**GET** `/api/users/:id` (Planned)
**Query Parameters:**
- `limit` - Number of results (default: 50, max: 100)
- `offset` - Pagination offset
- `status` - Filter by status (active/suspended/inactive)
- `role` - Filter by role
- `search` - Search by name or email
Would retrieve specific user details.
**Response:**
```json
{
"users": [
{
"user_id": "user-123",
"username": "john_doe",
"email": "john@example.com",
"display_name": "John Doe",
"status": "active",
"roles": ["user", "manager"]
},
{
"user_id": "user-456",
"username": "jane_smith",
"email": "jane@example.com",
"display_name": "Jane Smith",
"status": "active",
"roles": ["user"]
}
],
"total": 47,
"limit": 50,
"offset": 0
}
```
### Update User
**PUT** `/api/users/:id` (Planned)
**PUT** `/api/users/:id`
Would update user information.
Update user information.
**Required Permission:** `admin:users` or same user (limited fields)
**Request:**
```json
{
"display_name": "John D. Doe",
"avatar_url": "https://example.com/avatar.jpg"
}
```
**Admin-only fields:**
```json
{
"status": "suspended",
"roles": ["user"]
}
```
**Response:**
```json
{
"user_id": "user-123",
"status": "updated",
"updated_fields": ["display_name"]
}
```
### Update User Settings
**PUT** `/api/users/:id/settings`
Update user preferences.
**Request:**
```json
{
"theme": "dark",
"language": "en",
"notifications": {
"email": true,
"push": false,
"digest": "daily"
},
"default_bot": "support-bot"
}
```
**Response:**
```json
{
"status": "updated",
"settings": {
"theme": "dark",
"language": "en"
}
}
```
### Get User Settings
**GET** `/api/users/:id/settings`
Retrieve user preferences.
**Response:**
```json
{
"theme": "dark",
"language": "en",
"timezone": "America/New_York",
"notifications": {
"email": true,
"push": false,
"digest": "daily"
},
"default_bot": "support-bot"
}
```
### Suspend User
**POST** `/api/users/:id/suspend`
Suspend a user account.
**Required Permission:** `admin:users`
**Request:**
```json
{
"reason": "Policy violation"
}
```
**Response:**
```json
{
"user_id": "user-123",
"status": "suspended",
"suspended_at": "2024-01-15T10:30:00Z"
}
```
### Activate User
**POST** `/api/users/:id/activate`
Reactivate a suspended user.
**Required Permission:** `admin:users`
**Response:**
```json
{
"user_id": "user-123",
"status": "active",
"activated_at": "2024-01-15T10:30:00Z"
}
```
### Delete User
**DELETE** `/api/users/:id` (Planned)
**DELETE** `/api/users/:id`
Would deactivate user account.
Deactivate/delete user account.
**Required Permission:** `admin:users`
**Response:**
```json
{
"user_id": "user-123",
"status": "deleted",
"deleted_at": "2024-01-15T10:30:00Z"
}
```
## User Sessions
### List User Sessions
**GET** `/api/users/:id/sessions`
List active sessions for a user.
**Response:**
```json
{
"sessions": [
{
"session_id": "sess-001",
"bot_id": "support-bot",
"started_at": "2024-01-15T09:00:00Z",
"last_activity": "2024-01-15T10:30:00Z",
"device": "Chrome on Windows"
}
]
}
```
### Terminate Session
**DELETE** `/api/users/:id/sessions/:session_id`
End a specific user session.
**Response:**
```json
{
"session_id": "sess-001",
"status": "terminated"
}
```
### Terminate All Sessions
**DELETE** `/api/users/:id/sessions`
End all user sessions (logout everywhere).
**Response:**
```json
{
"terminated_count": 3,
"status": "all_sessions_terminated"
}
```
## User Authentication Flow
### Login
**POST** `/api/users/login`
Authenticate user (redirects to Zitadel).
**Request:**
```json
{
"email": "user@example.com",
"password": "password",
"remember_me": true
}
```
**Response:**
```json
{
"redirect_url": "https://auth.yourdomain.com/oauth/authorize?..."
}
```
### Logout
**POST** `/api/users/logout`
End current session.
**Response:**
```json
{
"status": "logged_out",
"redirect_url": "/"
}
```
### Register
**POST** `/api/users/register`
Register new user (if self-registration enabled).
**Request:**
```json
{
"email": "newuser@example.com",
"username": "newuser",
"password": "SecurePassword123!",
"display_name": "New User"
}
```
**Response:**
```json
{
"user_id": "user-789",
"status": "pending_verification",
"message": "Check your email to verify your account"
}
```
## User Management via Zitadel
Currently, all user management operations must be performed through:
For full user management, access Zitadel admin console:
1. **Zitadel Admin Console**
- Create users
- Update profiles
- Reset passwords
- Manage roles
- Deactivate accounts
2. **Zitadel API**
- Direct API calls to Zitadel
- Not proxied through BotServer
- Requires Zitadel authentication
1. **Access Console**: `http://localhost:8080` (or your Zitadel URL)
2. **Create Users**: Organization → Users → Add
3. **Manage Roles**: Users → Select User → Authorizations
4. **Reset Passwords**: Users → Select User → Actions → Reset Password
5. **Configure SSO**: Settings → Identity Providers
## Database Schema
Users are minimally stored in BotServer:
BotServer maintains minimal user data:
```sql
-- users table
-- users table (synced from Zitadel)
CREATE TABLE users (
id UUID PRIMARY KEY,
zitadel_id TEXT UNIQUE,
username TEXT,
email TEXT,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
display_name TEXT,
avatar_url TEXT,
status TEXT DEFAULT 'active',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- user_settings table
CREATE TABLE user_settings (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id),
setting_key TEXT NOT NULL,
setting_value TEXT,
UNIQUE(user_id, setting_key)
);
-- user_sessions table
CREATE TABLE sessions (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id),
bot_id UUID,
status TEXT DEFAULT 'active',
device_info TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
last_activity TIMESTAMPTZ DEFAULT NOW()
);
```
This is only for session management and caching.
## Error Handling
## Authentication
| Status Code | Error | Description |
|-------------|-------|-------------|
| 400 | `invalid_request` | Malformed request |
| 401 | `unauthorized` | Not authenticated |
| 403 | `forbidden` | Insufficient permissions |
| 404 | `user_not_found` | User doesn't exist |
| 409 | `conflict` | Username/email already exists |
| 422 | `validation_error` | Invalid field values |
All user authentication happens through Zitadel:
- No password storage in BotServer
- OAuth2/OIDC flow for login
- Sessions managed locally
- Token validation through Zitadel
## Rate Limits
## Future Implementation
| Endpoint | Limit |
|----------|-------|
| Login | 10/minute per IP |
| Register | 5/hour per IP |
| User List | 60/minute per user |
| User Update | 30/minute per user |
When fully implemented, the Users API will:
- Proxy Zitadel operations
- Provide unified API interface
- Cache user data for performance
- Handle organization-specific operations
- Integrate with bot permissions
## BASIC Integration
## Current Workarounds
Access user information in scripts:
To manage users today:
```basic
' Get current user info
user_name = GET user_name
user_email = GET user_email
1. **Use Zitadel Console**
- Access at Zitadel URL (usually port 8080)
- Full user management capabilities
- Role and permission assignment
' Greet by name
TALK "Hello, " + user_name + "!"
2. **Direct Zitadel API**
- Use Zitadel's REST API
- Authenticate with service account
- Not integrated with BotServer auth
' Check user role
role = GET role
IF role = "admin" THEN
TALK "Welcome, administrator!"
END IF
```
3. **Session Management**
- Users tracked via sessions
- Minimal profile data cached
- No direct user CRUD operations
## See Also
## Summary
User management in BotServer is delegated to Zitadel. The Users API provides minimal endpoints for session-related user data. Full user management requires using Zitadel's admin console or API directly.
- [User Authentication](../chapter-12-auth/user-auth.md) - Auth details
- [Permissions Matrix](../chapter-12-auth/permissions-matrix.md) - Access control
- [Groups API](./groups-api.md) - Group management
- [SET USER Keyword](../chapter-06-gbdialog/keyword-set-user.md) - BASIC user context

View file

@ -0,0 +1,272 @@
# Permissions Matrix
This chapter documents the permission system in General Bots, detailing which APIs require authentication, the context they operate under, and how to configure access control.
## Overview
General Bots uses a role-based access control (RBAC) system managed through Zitadel (directory service). Permissions are organized into:
- **Realms**: Top-level permission boundaries (typically per organization)
- **Groups**: Collections of users with shared permissions
- **Permissions**: Specific actions that can be granted to groups
## User Context vs System Context
APIs operate in one of two contexts:
| Context | Description | Authentication |
|---------|-------------|----------------|
| **User Context** | Operations performed on behalf of a logged-in user | User's OAuth token |
| **System Context** | Operations performed by the bot or system | Service account token |
### User Context Operations
These operations use the authenticated user's identity:
- Reading user's own files
- Sending messages as the user
- Accessing user's calendar
- Managing user's tasks
- Viewing user's email
### System Context Operations
These operations use a service account:
- Bot-initiated messages
- Scheduled tasks execution
- System monitoring
- Cross-user analytics
- Backup operations
## API Permission Matrix
### File APIs
| Endpoint | Method | User Context | System Context | Required Permission |
|----------|--------|--------------|----------------|---------------------|
| `/api/drive/list` | GET | User's files | All bot files | `files:read` |
| `/api/drive/upload` | POST | User's folder | Bot storage | `files:write` |
| `/api/drive/delete` | DELETE | User's files | Any file | `files:delete` |
| `/api/drive/share` | POST | Own files | Any file | `files:share` |
### Email APIs
| Endpoint | Method | User Context | System Context | Required Permission |
|----------|--------|--------------|----------------|---------------------|
| `/api/email/inbox` | GET | User's inbox | N/A | `email:read` |
| `/api/email/send` | POST | As user | As bot | `email:send` |
| `/api/email/drafts` | GET | User's drafts | N/A | `email:read` |
### Meet APIs
| Endpoint | Method | User Context | System Context | Required Permission |
|----------|--------|--------------|----------------|---------------------|
| `/api/meet/rooms` | GET | Visible rooms | All rooms | `meet:read` |
| `/api/meet/create` | POST | As organizer | As bot | `meet:create` |
| `/api/meet/join` | POST | As participant | As bot participant | `meet:join` |
| `/api/meet/invite` | POST | Own meetings | Any meeting | `meet:invite` |
### Calendar APIs
| Endpoint | Method | User Context | System Context | Required Permission |
|----------|--------|--------------|----------------|---------------------|
| `/api/calendar/events` | GET | User's events | Bot calendar | `calendar:read` |
| `/api/calendar/create` | POST | User's calendar | Bot calendar | `calendar:write` |
| `/api/calendar/book` | POST | As attendee | As organizer | `calendar:book` |
### Tasks APIs
| Endpoint | Method | User Context | System Context | Required Permission |
|----------|--------|--------------|----------------|---------------------|
| `/api/tasks` | GET | User's tasks | All tasks | `tasks:read` |
| `/api/tasks` | POST | Assigned to user | Any assignment | `tasks:write` |
| `/api/tasks/complete` | POST | Own tasks | Any task | `tasks:complete` |
### Admin APIs
| Endpoint | Method | User Context | System Context | Required Permission |
|----------|--------|--------------|----------------|---------------------|
| `/api/admin/users` | GET | N/A | Full access | `admin:users` |
| `/api/admin/bots` | GET | N/A | Full access | `admin:bots` |
| `/api/admin/config` | PUT | N/A | Full access | `admin:config` |
| `/api/monitoring/status` | GET | N/A | Full access | `admin:monitor` |
## Permission Definitions
### Core Permissions
| Permission | Description |
|------------|-------------|
| `chat:read` | View conversation history |
| `chat:write` | Send messages |
| `files:read` | View and download files |
| `files:write` | Upload and modify files |
| `files:delete` | Delete files |
| `files:share` | Share files with others |
### Communication Permissions
| Permission | Description |
|------------|-------------|
| `email:read` | Read email messages |
| `email:send` | Send email messages |
| `meet:read` | View meeting information |
| `meet:create` | Create new meetings |
| `meet:join` | Join meetings |
| `meet:invite` | Invite others to meetings |
### Productivity Permissions
| Permission | Description |
|------------|-------------|
| `calendar:read` | View calendar events |
| `calendar:write` | Create/modify events |
| `calendar:book` | Book appointments |
| `tasks:read` | View tasks |
| `tasks:write` | Create/modify tasks |
| `tasks:complete` | Mark tasks complete |
### Administrative Permissions
| Permission | Description |
|------------|-------------|
| `admin:users` | Manage users |
| `admin:groups` | Manage groups |
| `admin:bots` | Manage bot configurations |
| `admin:config` | Modify system configuration |
| `admin:monitor` | Access monitoring data |
| `admin:backup` | Perform backup operations |
## Default Groups
General Bots creates these default groups:
### Administrators
```
Permissions:
- admin:*
- All other permissions
```
Full system access for system administrators.
### Managers
```
Permissions:
- chat:read, chat:write
- files:read, files:write, files:share
- email:read, email:send
- meet:*, calendar:*, tasks:*
- admin:monitor
```
Access to productivity features and basic monitoring.
### Users
```
Permissions:
- chat:read, chat:write
- files:read, files:write
- email:read, email:send
- meet:read, meet:join
- calendar:read, calendar:write
- tasks:read, tasks:write, tasks:complete
```
Standard user access to all productivity features.
### Guests
```
Permissions:
- chat:read, chat:write
```
Chat-only access for anonymous or guest users.
## Configuring Permissions
### In Zitadel
1. Access Zitadel admin console
2. Navigate to **Organization** → **Roles**
3. Create roles matching permission names
4. Assign roles to groups
5. Add users to groups
### In config.csv
Map Zitadel roles to General Bots permissions:
```csv
key,value
permission-mapping-admin,admin:*
permission-mapping-manager,chat:*|files:*|email:*|meet:*|calendar:*|tasks:*
permission-mapping-user,chat:*|files:read|files:write
permission-default-anonymous,chat:read|chat:write
```
## Anonymous Access
The chat interface supports anonymous users:
| Feature | Anonymous Access | Notes |
|---------|-----------------|-------|
| Chat (default bot) | Yes | Session-based |
| Chat history | No | Requires login |
| Drive access | No | Requires login |
| Mail access | No | Requires login |
| Tasks | No | Requires login |
| Meet | No | Requires login |
| Settings | No | Requires login |
Anonymous users:
- Can chat with the default bot only
- Session stored on server
- No persistent history
- Cannot access other tabs
## Checking Permissions in BASIC
Use role-based logic in your scripts:
```basic
' Get user role from session
role = GET role
' Check role and respond accordingly
IF role = "admin" THEN
TALK "Welcome, administrator. You have full access."
ELSE IF role = "manager" THEN
TALK "Welcome, manager. You can view reports."
ELSE
TALK "Welcome! How can I help you today?"
END IF
```
## Audit Logging
All permission checks are logged. Access audit logs through the admin API:
```
GET /api/admin/audit?filter=permission
```
Log entries include:
- Timestamp
- User ID
- Action attempted
- Resource accessed
- Result (allowed/denied)
- Reason for denial (if applicable)
## See Also
- [User Authentication](./user-auth.md) - Login and session management
- [User Context vs System Context](./user-system-context.md) - Detailed context explanation
- [Security Policy](./security-policy.md) - Security guidelines
- [API Endpoints](./api-endpoints.md) - Full API reference

View file

@ -0,0 +1,287 @@
# User Context vs System Context
This chapter explains the two execution contexts in General Bots: User Context and System Context. Understanding these contexts is essential for building secure, properly scoped bot interactions.
## Overview
Every API call and BASIC script execution happens in one of two contexts:
| Context | Identity | Use Case |
|---------|----------|----------|
| **User Context** | Logged-in user | Interactive operations on user's behalf |
| **System Context** | Bot service account | Automated/scheduled operations |
## User Context
### Definition
User Context means the operation is performed **as** the authenticated user, using their identity and permissions.
### Characteristics
- **Identity**: The logged-in user's ID
- **Permissions**: Limited to what the user can access
- **Scope**: Only user's own resources
- **Token**: User's OAuth access token
### When User Context Applies
1. **Interactive Chat**: User sends a message
2. **File Operations**: User uploads/downloads files
3. **Email Access**: User reads their inbox
4. **Calendar**: User views their schedule
5. **Tasks**: User manages their task list
### Example Flow
```
User logs in → OAuth token issued → User asks bot to send email
Bot sends email AS the user
Email "From:" shows user's address
```
### BASIC Script Example
```basic
' This runs in User Context when triggered by user interaction
' The email is sent from the logged-in user's account
recipient = HEAR "Who should I email?"
subject = HEAR "What's the subject?"
body = HEAR "What's the message?"
SEND MAIL recipient, subject, body
TALK "Email sent from your account to " + recipient
```
### Access Boundaries
In User Context, the bot can only access:
| Resource | Access Level |
|----------|--------------|
| Files | User's files and shared files |
| Email | User's mailbox only |
| Calendar | User's calendar only |
| Tasks | User's tasks only |
| Contacts | User's contacts |
| Meet | Meetings user is invited to |
## System Context
### Definition
System Context means the operation is performed **by** the bot system itself, using a service account with elevated permissions.
### Characteristics
- **Identity**: Bot's service account
- **Permissions**: Defined by admin configuration
- **Scope**: Cross-user or system-wide resources
- **Token**: Service account credentials
### When System Context Applies
1. **Scheduled Tasks**: Cron-based script execution via SET SCHEDULE
2. **Event Handlers**: ON keyword triggers
3. **Admin Operations**: User management
4. **Analytics**: Cross-user reporting
5. **Backups**: System-wide data export
6. **Bot-Initiated Messages**: Proactive notifications
### Example Flow
```
Schedule triggers at 9:00 AM → System context activated
Bot sends summary to all managers
Email "From:" shows bot's address
```
### BASIC Script Example
```basic
' This runs in System Context (scheduled task)
' The bot sends emails from its own account
SET SCHEDULE "0 9 * * 1" ' Every Monday at 9 AM
' Bot processes data and sends notifications
summary = LLM "Generate weekly summary"
SEND MAIL "team@example.com", "Weekly Summary", summary
PRINT "Weekly summary sent"
```
### Access Boundaries
In System Context, the bot can access:
| Resource | Access Level |
|----------|--------------|
| Files | All bot storage |
| Email | Send as bot identity |
| Calendar | Bot's calendar, create events |
| Tasks | Create/assign to any user |
| Users | Read user directory |
| Meet | Join any meeting (if configured) |
| Config | Read bot configuration |
## Determining Context
### Automatic Detection
General Bots automatically determines context based on how the script is triggered:
| Trigger | Context |
|---------|---------|
| User sends message | User Context |
| SET SCHEDULE execution | System Context |
| ON event handler | System Context |
| HTTP API with user token | User Context |
| Internal service call | System Context |
### Context in Scripts
The context is determined by the trigger, not by keywords in the script:
```basic
' User-triggered script (User Context)
' - Runs when user interacts
' - Uses user's permissions
name = HEAR "What's your name?"
TALK "Hello, " + name
```
```basic
' Scheduled script (System Context)
' - Runs on schedule
' - Uses bot's permissions
SET SCHEDULE "0 8 * * *" ' Daily at 8 AM
TALK "Good morning! Here's your daily briefing."
```
## Security Implications
### User Context Security
| Benefit | Consideration |
|---------|---------------|
| Limited blast radius | Cannot access others' data |
| Audit trail to user | User responsible for actions |
| Respects user permissions | May limit bot functionality |
### System Context Security
| Benefit | Consideration |
|---------|---------------|
| Full bot capabilities | Must be carefully controlled |
| Cross-user operations | Audit critical for compliance |
| Scheduled automation | Service account must be secured |
## Configuration
### Service Account Setup
The bot's system identity is managed through the Directory service (Zitadel). Configure in `config.csv`:
```csv
key,value
system-account-email,bot@yourdomain.com
system-context-permissions,files:read|email:send|calendar:write
```
### Context Restrictions
Limit what System Context can do:
```csv
key,value
system-allow-email,true
system-allow-file-delete,false
system-allow-user-create,false
system-allow-config-change,false
```
## Audit Logging
All operations are logged with context:
```json
{
"timestamp": "2024-01-15T10:30:00Z",
"context": "user",
"user_id": "user-123",
"action": "email:send",
"resource": "email to client@example.com",
"result": "success"
}
```
```json
{
"timestamp": "2024-01-15T09:00:00Z",
"context": "system",
"service_account": "bot-service-account",
"action": "email:send",
"resource": "weekly-summary to 47 recipients",
"trigger": "schedule:weekly-summary",
"result": "success"
}
```
## Best Practices
### Use User Context When
- User initiates the action
- Operation affects only the user
- Audit trail should point to user
- Respecting user permissions is required
### Use System Context When
- Scheduled or automated tasks
- Cross-user operations needed
- Bot needs elevated permissions
- System-wide actions required
### Security Guidelines
1. **Minimize System Context**: Use only when necessary
2. **Audit Everything**: Log all system context operations
3. **Rotate Credentials**: Change service account tokens regularly
4. **Limit Scope**: Grant minimal permissions to service account
5. **Review Access**: Periodically audit system context usage
## Troubleshooting
### "Permission Denied" Errors
Check if the operation is running in the expected context:
- **User-triggered actions** run in User Context with user permissions
- **Scheduled actions** run in System Context with bot permissions
If a scheduled task fails with permission errors, verify the bot's service account has the required permissions in Zitadel.
### Unexpected "From" Address in Emails
The sender depends on context:
- **User Context**: Sends as logged-in user
- **System Context**: Sends as bot account
Ensure your script is triggered in the intended way for the correct sender.
## See Also
- [Permissions Matrix](./permissions-matrix.md) - Full permission reference
- [Bot Authentication](./bot-auth.md) - Service account setup
- [Security Policy](./security-policy.md) - Security guidelines
- [SET SCHEDULE](../chapter-06-gbdialog/keyword-set-schedule.md) - Scheduled execution

View file

@ -1,4 +1,5 @@
use crate::basic::keywords::set_schedule::execute_set_schedule;
use crate::basic::keywords::webhook::execute_webhook_registration;
use crate::shared::models::TriggerKind;
use crate::shared::state::AppState;
use diesel::ExpressionMethods;
@ -304,6 +305,7 @@ impl BasicCompiler {
let bot_uuid = bot_id;
let mut result = String::new();
let mut has_schedule = false;
let mut has_webhook = false;
let script_name = Path::new(source_path)
.file_stem()
.and_then(|s| s.to_str())
@ -349,7 +351,14 @@ impl BasicCompiler {
.replace("USE WEBSITE", "USE_WEBSITE")
.replace("GET BOT MEMORY", "GET_BOT_MEMORY")
.replace("SET BOT MEMORY", "SET_BOT_MEMORY")
.replace("CREATE DRAFT", "CREATE_DRAFT");
.replace("CREATE DRAFT", "CREATE_DRAFT")
.replace("DELETE FILE", "DELETE_FILE")
.replace("DELETE HTTP", "DELETE_HTTP")
.replace("SET HEADER", "SET_HEADER")
.replace("CLEAR HEADERS", "CLEAR_HEADERS")
.replace("GENERATE PDF", "GENERATE_PDF")
.replace("MERGE PDF", "MERGE_PDF")
.replace("GROUP BY", "GROUP_BY");
if normalized.starts_with("SET_SCHEDULE") {
has_schedule = true;
let parts: Vec<&str> = normalized.split('"').collect();
@ -371,6 +380,34 @@ impl BasicCompiler {
}
continue;
}
// Handle WEBHOOK preprocessing - register webhook endpoint
if normalized.starts_with("WEBHOOK") {
has_webhook = true;
let parts: Vec<&str> = normalized.split('"').collect();
if parts.len() >= 2 {
let endpoint = parts[1];
let mut conn = self
.state
.conn
.get()
.map_err(|e| format!("Failed to get database connection: {}", e))?;
if let Err(e) =
execute_webhook_registration(&mut conn, endpoint, &script_name, bot_id)
{
log::error!("Failed to register WEBHOOK during preprocessing: {}", e);
} else {
log::info!(
"Registered webhook endpoint {} for script {} during preprocessing",
endpoint,
script_name
);
}
} else {
log::warn!("Malformed WEBHOOK line ignored: {}", normalized);
}
continue;
}
if normalized.starts_with("USE_WEBSITE") {
let parts: Vec<&str> = normalized.split('"').collect();
if parts.len() >= 2 {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,801 @@
/*****************************************************************************\
| ® |
| |
| |
| |
| |
| |
| 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. |
| |
\*****************************************************************************/
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::{error, trace};
use reqwest::{header::HeaderMap, header::HeaderName, header::HeaderValue, Client, Method};
use rhai::{Dynamic, Engine, Map};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::error::Error;
use std::sync::{Arc, Mutex};
use std::time::Duration;
/// Thread-local storage for HTTP headers
thread_local! {
static HTTP_HEADERS: std::cell::RefCell<HashMap<String, String>> = std::cell::RefCell::new(HashMap::new());
}
/// Register all HTTP operation keywords
pub fn register_http_operations(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
register_post_keyword(state.clone(), user.clone(), engine);
register_put_keyword(state.clone(), user.clone(), engine);
register_patch_keyword(state.clone(), user.clone(), engine);
register_delete_http_keyword(state.clone(), user.clone(), engine);
register_set_header_keyword(state.clone(), user.clone(), engine);
register_graphql_keyword(state.clone(), user.clone(), engine);
register_soap_keyword(state.clone(), user.clone(), engine);
register_clear_headers_keyword(state.clone(), user.clone(), engine);
}
/// POST "url", data
/// Sends an HTTP POST request with JSON body
pub fn register_post_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Engine) {
let _state_clone = Arc::clone(&state);
engine
.register_custom_syntax(
&["POST", "$expr$", ",", "$expr$"],
false,
move |context, inputs| {
let url = context.eval_expression_tree(&inputs[0])?.to_string();
let data = context.eval_expression_tree(&inputs[1])?;
trace!("POST request to: {}", url);
let (tx, rx) = std::sync::mpsc::channel();
let url_clone = url.clone();
let data_clone = dynamic_to_json(&data);
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::POST, &url_clone, Some(data_clone), 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 POST 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!("POST failed: {}", e).into(),
rhai::Position::NONE,
))),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"POST request timed out".into(),
rhai::Position::NONE,
)))
}
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("POST thread failed: {}", e).into(),
rhai::Position::NONE,
))),
}
},
)
.unwrap();
}
/// PUT "url", data
/// Sends an HTTP PUT request with JSON body
pub fn register_put_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Engine) {
let _state_clone = Arc::clone(&state);
engine
.register_custom_syntax(
&["PUT", "$expr$", ",", "$expr$"],
false,
move |context, inputs| {
let url = context.eval_expression_tree(&inputs[0])?.to_string();
let data = context.eval_expression_tree(&inputs[1])?;
trace!("PUT request to: {}", url);
let (tx, rx) = std::sync::mpsc::channel();
let url_clone = url.clone();
let data_clone = dynamic_to_json(&data);
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::PUT, &url_clone, Some(data_clone), 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 PUT 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!("PUT failed: {}", e).into(),
rhai::Position::NONE,
))),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"PUT request timed out".into(),
rhai::Position::NONE,
)))
}
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("PUT thread failed: {}", e).into(),
rhai::Position::NONE,
))),
}
},
)
.unwrap();
}
/// PATCH "url", data
/// Sends an HTTP PATCH request with JSON body
pub fn register_patch_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Engine) {
let _state_clone = Arc::clone(&state);
engine
.register_custom_syntax(
&["PATCH", "$expr$", ",", "$expr$"],
false,
move |context, inputs| {
let url = context.eval_expression_tree(&inputs[0])?.to_string();
let data = context.eval_expression_tree(&inputs[1])?;
trace!("PATCH request to: {}", url);
let (tx, rx) = std::sync::mpsc::channel();
let url_clone = url.clone();
let data_clone = dynamic_to_json(&data);
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::PATCH, &url_clone, Some(data_clone), 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 PATCH 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!("PATCH failed: {}", e).into(),
rhai::Position::NONE,
))),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"PATCH request timed out".into(),
rhai::Position::NONE,
)))
}
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("PATCH thread failed: {}", e).into(),
rhai::Position::NONE,
))),
}
},
)
.unwrap();
}
/// DELETE "url"
/// Sends an HTTP DELETE request
pub fn register_delete_http_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Engine) {
let _state_clone = 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 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();
}
/// SET HEADER "name", "value"
/// Sets an HTTP header for subsequent requests
pub fn register_set_header_keyword(_state: Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// 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);
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();
}
/// CLEAR HEADERS
/// Clears all previously set HTTP headers
pub fn register_clear_headers_keyword(
_state: Arc<AppState>,
_user: UserSession,
engine: &mut Engine,
) {
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();
}
/// GRAPHQL "endpoint", "query", variables
/// Executes a GraphQL query
pub fn register_graphql_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Engine) {
let _state_clone = Arc::clone(&state);
engine
.register_custom_syntax(
&["GRAPHQL", "$expr$", ",", "$expr$", ",", "$expr$"],
false,
move |context, inputs| {
let endpoint = context.eval_expression_tree(&inputs[0])?.to_string();
let query = context.eval_expression_tree(&inputs[1])?.to_string();
let variables = context.eval_expression_tree(&inputs[2])?;
trace!("GRAPHQL request to: {}", endpoint);
let (tx, rx) = std::sync::mpsc::channel();
let endpoint_clone = endpoint.clone();
let query_clone = query.clone();
let variables_json = dynamic_to_json(&variables);
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_graphql(&endpoint_clone, &query_clone, variables_json).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 GRAPHQL 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!("GRAPHQL failed: {}", e).into(),
rhai::Position::NONE,
))),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"GRAPHQL request timed out".into(),
rhai::Position::NONE,
)))
}
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("GRAPHQL thread failed: {}", e).into(),
rhai::Position::NONE,
))),
}
},
)
.unwrap();
}
/// SOAP "wsdl", "operation", params
/// Executes a SOAP API call
pub fn register_soap_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Engine) {
let _state_clone = Arc::clone(&state);
engine
.register_custom_syntax(
&["SOAP", "$expr$", ",", "$expr$", ",", "$expr$"],
false,
move |context, inputs| {
let wsdl = context.eval_expression_tree(&inputs[0])?.to_string();
let operation = context.eval_expression_tree(&inputs[1])?.to_string();
let params = context.eval_expression_tree(&inputs[2])?;
trace!("SOAP request to: {}, operation: {}", wsdl, operation);
let (tx, rx) = std::sync::mpsc::channel();
let wsdl_clone = wsdl.clone();
let operation_clone = operation.clone();
let params_json = dynamic_to_json(&params);
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_soap(&wsdl_clone, &operation_clone, params_json).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 SOAP result from thread");
}
});
match rx.recv_timeout(std::time::Duration::from_secs(120)) {
Ok(Ok(response)) => Ok(json_to_dynamic(&response)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("SOAP failed: {}", e).into(),
rhai::Position::NONE,
))),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"SOAP request timed out".into(),
rhai::Position::NONE,
)))
}
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("SOAP thread failed: {}", e).into(),
rhai::Position::NONE,
))),
}
},
)
.unwrap();
}
/// Execute an HTTP request with the specified method
async fn execute_http_request(
method: Method,
url: &str,
body: Option<Value>,
custom_headers: Option<HashMap<String, String>>,
) -> Result<Value, Box<dyn Error + Send + Sync>> {
let client = Client::builder()
.timeout(Duration::from_secs(60))
.connect_timeout(Duration::from_secs(10))
.build()
.map_err(|e| {
error!("Failed to build HTTP client: {}", e);
e
})?;
// Build headers
let mut headers = HeaderMap::new();
// Add stored headers from thread-local storage
HTTP_HEADERS.with(|h| {
for (name, value) in h.borrow().iter() {
if let (Ok(header_name), Ok(header_value)) = (
HeaderName::try_from(name.as_str()),
HeaderValue::try_from(value.as_str()),
) {
headers.insert(header_name, header_value);
}
}
});
// Add custom headers if provided
if let Some(custom) = custom_headers {
for (name, value) in custom {
if let (Ok(header_name), Ok(header_value)) = (
HeaderName::try_from(name.as_str()),
HeaderValue::try_from(value.as_str()),
) {
headers.insert(header_name, header_value);
}
}
}
// Set default content type for requests with body
if body.is_some() && !headers.contains_key("content-type") {
headers.insert(
reqwest::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
}
let mut request = client.request(method.clone(), url).headers(headers);
if let Some(body_data) = body {
request = request.json(&body_data);
}
let response = request.send().await.map_err(|e| {
error!("HTTP {} request failed for URL {}: {}", method, url, e);
e
})?;
let status = response.status();
let response_headers: HashMap<String, String> = response
.headers()
.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
.collect();
let body_text = response.text().await.unwrap_or_default();
// Try to parse as JSON, fall back to text
let body_value: Value = serde_json::from_str(&body_text).unwrap_or(Value::String(body_text));
trace!(
"HTTP {} request to {} completed with status: {}",
method,
url,
status
);
Ok(json!({
"status": status.as_u16(),
"statusText": status.canonical_reason().unwrap_or(""),
"headers": response_headers,
"data": body_value
}))
}
/// Execute a GraphQL query
async fn execute_graphql(
endpoint: &str,
query: &str,
variables: Value,
) -> Result<Value, Box<dyn Error + Send + Sync>> {
let graphql_body = json!({
"query": query,
"variables": variables
});
execute_http_request(Method::POST, endpoint, Some(graphql_body), None).await
}
/// Execute a SOAP request
async fn execute_soap(
endpoint: &str,
operation: &str,
params: Value,
) -> Result<Value, Box<dyn Error + Send + Sync>> {
// Build SOAP envelope
let soap_body = build_soap_envelope(operation, &params);
let mut headers = HashMap::new();
headers.insert(
"Content-Type".to_string(),
"text/xml; charset=utf-8".to_string(),
);
headers.insert("SOAPAction".to_string(), format!("\"{}\"", operation));
let client = Client::builder()
.timeout(Duration::from_secs(120))
.connect_timeout(Duration::from_secs(10))
.build()
.map_err(|e| {
error!("Failed to build HTTP client: {}", e);
e
})?;
let mut header_map = HeaderMap::new();
for (name, value) in headers {
if let (Ok(header_name), Ok(header_value)) = (
HeaderName::try_from(name.as_str()),
HeaderValue::try_from(value.as_str()),
) {
header_map.insert(header_name, header_value);
}
}
let response = client
.post(endpoint)
.headers(header_map)
.body(soap_body)
.send()
.await
.map_err(|e| {
error!("SOAP request failed for endpoint {}: {}", endpoint, e);
e
})?;
let status = response.status();
let body_text = response.text().await.unwrap_or_default();
// Parse SOAP response (basic XML to JSON conversion)
let parsed_response = parse_soap_response(&body_text);
trace!(
"SOAP request to {} completed with status: {}",
endpoint,
status
);
Ok(json!({
"status": status.as_u16(),
"data": parsed_response
}))
}
/// Build a SOAP envelope from operation and parameters
fn build_soap_envelope(operation: &str, params: &Value) -> String {
let mut params_xml = String::new();
if let Value::Object(obj) = params {
for (key, value) in obj {
let value_str = match value {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
_ => value.to_string(),
};
params_xml.push_str(&format!("<{}>{}</{}>", key, value_str, key));
}
}
format!(
r#"<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<{operation} xmlns="http://tempuri.org/">
{params_xml}
</{operation}>
</soap:Body>
</soap:Envelope>"#,
operation = operation,
params_xml = params_xml
)
}
/// Parse SOAP response XML to JSON (basic implementation)
fn parse_soap_response(xml: &str) -> Value {
// Basic XML parsing - extracts content between Body tags
if let Some(body_start) = xml.find("<soap:Body>") {
if let Some(body_end) = xml.find("</soap:Body>") {
let body_content = &xml[body_start + 11..body_end];
return json!({
"raw": body_content.trim(),
"xml": xml
});
}
}
// Also check for SOAP 1.2 format
if let Some(body_start) = xml.find("<soap12:Body>") {
if let Some(body_end) = xml.find("</soap12:Body>") {
let body_content = &xml[body_start + 13..body_end];
return json!({
"raw": body_content.trim(),
"xml": xml
});
}
}
json!({
"raw": xml,
"xml": xml
})
}
/// Convert Rhai Dynamic to JSON Value
fn dynamic_to_json(value: &Dynamic) -> Value {
if value.is_unit() {
Value::Null
} else if value.is_bool() {
Value::Bool(value.as_bool().unwrap_or(false))
} else if value.is_int() {
Value::Number(value.as_int().unwrap_or(0).into())
} else if value.is_float() {
if let Some(f) = value.as_float().ok() {
serde_json::Number::from_f64(f)
.map(Value::Number)
.unwrap_or(Value::Null)
} else {
Value::Null
}
} else if value.is_string() {
Value::String(value.to_string())
} else if value.is_array() {
let arr = value.clone().into_array().unwrap_or_default();
Value::Array(arr.iter().map(dynamic_to_json).collect())
} else if value.is_map() {
let map = value.clone().try_cast::<Map>().unwrap_or_default();
let obj: serde_json::Map<String, Value> = map
.iter()
.map(|(k, v)| (k.to_string(), dynamic_to_json(v)))
.collect();
Value::Object(obj)
} else {
Value::String(value.to_string())
}
}
/// Convert JSON Value to Rhai Dynamic
fn json_to_dynamic(value: &Value) -> Dynamic {
match value {
Value::Null => Dynamic::UNIT,
Value::Bool(b) => Dynamic::from(*b),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Dynamic::from(i)
} else if let Some(f) = n.as_f64() {
Dynamic::from(f)
} else {
Dynamic::UNIT
}
}
Value::String(s) => Dynamic::from(s.clone()),
Value::Array(arr) => {
let rhai_arr: rhai::Array = arr.iter().map(json_to_dynamic).collect();
Dynamic::from(rhai_arr)
}
Value::Object(obj) => {
let mut map = Map::new();
for (k, v) in obj {
map.insert(k.clone().into(), json_to_dynamic(v));
}
Dynamic::from(map)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dynamic_to_json_string() {
let dynamic = Dynamic::from("hello");
let json = dynamic_to_json(&dynamic);
assert_eq!(json, Value::String("hello".to_string()));
}
#[test]
fn test_dynamic_to_json_number() {
let dynamic = Dynamic::from(42_i64);
let json = dynamic_to_json(&dynamic);
assert_eq!(json, Value::Number(42.into()));
}
#[test]
fn test_build_soap_envelope() {
let params = json!({"name": "John", "age": 30});
let envelope = build_soap_envelope("GetUser", &params);
assert!(envelope.contains("<GetUser"));
assert!(envelope.contains("<name>John</name>"));
assert!(envelope.contains("<age>30</age>"));
}
#[test]
fn test_parse_soap_response() {
let xml = r#"<?xml version="1.0"?><soap:Envelope><soap:Body><Result>Success</Result></soap:Body></soap:Envelope>"#;
let result = parse_soap_response(xml);
assert!(result.get("raw").is_some());
}
}

View file

@ -7,12 +7,15 @@ pub mod clear_tools;
pub mod create_draft;
pub mod create_site;
pub mod create_task;
pub mod data_operations;
pub mod file_operations;
pub mod find;
pub mod first;
pub mod for_next;
pub mod format;
pub mod get;
pub mod hear_talk;
pub mod http_operations;
pub mod last;
pub mod llm_keyword;
pub mod multimodal;
@ -25,9 +28,12 @@ pub mod set;
pub mod set_context;
pub mod set_schedule;
pub mod set_user;
pub mod string_functions;
pub mod switch_case;
pub mod universal_messaging;
pub mod use_kb;
pub mod use_tool;
pub mod use_website;
pub mod wait;
pub mod weather;
pub mod webhook;

View file

@ -0,0 +1,370 @@
//! String function keywords for BASIC interpreter
//!
//! This module provides classic BASIC string manipulation functions:
//! - INSTR: Find position of substring within string
//! - IS_NUMERIC: Check if a string can be parsed as a number
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::{Dynamic, Engine};
use std::sync::Arc;
/// Register the INSTR keyword with the Rhai engine
///
/// INSTR returns the 1-based position of a substring within a string.
/// Returns 0 if not found.
///
/// Syntax:
/// position = INSTR(string, substring)
/// position = INSTR(start, string, substring)
pub fn instr_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// Two-argument version: INSTR(string, substring)
engine.register_fn("INSTR", |haystack: &str, needle: &str| -> i64 {
instr_impl(1, haystack, needle)
});
// Alias with lowercase
engine.register_fn("instr", |haystack: &str, needle: &str| -> i64 {
instr_impl(1, haystack, needle)
});
// Three-argument version: INSTR(start, string, substring)
engine.register_fn("INSTR", |start: i64, haystack: &str, needle: &str| -> i64 {
instr_impl(start, haystack, needle)
});
engine.register_fn("instr", |start: i64, haystack: &str, needle: &str| -> i64 {
instr_impl(start, haystack, needle)
});
debug!("Registered INSTR keyword");
}
/// Implementation of INSTR function
///
/// # Arguments
/// * `start` - 1-based starting position for the search
/// * `haystack` - The string to search in
/// * `needle` - The substring to find
///
/// # Returns
/// * 1-based position of the first occurrence, or 0 if not found
fn instr_impl(start: i64, haystack: &str, needle: &str) -> i64 {
// Handle edge cases
if haystack.is_empty() || needle.is_empty() {
return 0;
}
// Convert 1-based start to 0-based index
let start_idx = if start < 1 { 0 } else { (start - 1) as usize };
// Ensure start is within bounds
if start_idx >= haystack.len() {
return 0;
}
// Search from the starting position
match haystack[start_idx..].find(needle) {
Some(pos) => (start_idx + pos + 1) as i64, // Convert back to 1-based
None => 0,
}
}
/// Register the IS_NUMERIC / IS NUMERIC keyword with the Rhai engine
///
/// IS_NUMERIC tests whether a string can be converted to a number.
///
/// Syntax:
/// result = IS NUMERIC(value)
/// result = IS_NUMERIC(value)
pub fn is_numeric_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// Register as IS_NUMERIC (with underscore for Rhai compatibility)
engine.register_fn("IS_NUMERIC", |value: &str| -> bool {
is_numeric_impl(value)
});
// Lowercase variant
engine.register_fn("is_numeric", |value: &str| -> bool {
is_numeric_impl(value)
});
// Also register as ISNUMERIC (single word)
engine.register_fn("ISNUMERIC", |value: &str| -> bool {
is_numeric_impl(value)
});
engine.register_fn("isnumeric", |value: &str| -> bool {
is_numeric_impl(value)
});
// Handle Dynamic type for flexibility
engine.register_fn("IS_NUMERIC", |value: Dynamic| -> bool {
match value.as_str() {
Some(s) => is_numeric_impl(s),
None => {
// If it's already a number, return true
value.is::<i64>() || value.is::<f64>()
}
}
});
debug!("Registered IS_NUMERIC keyword");
}
/// Implementation of IS_NUMERIC function
///
/// # Arguments
/// * `value` - The string value to test
///
/// # Returns
/// * `true` if the value can be parsed as a number
/// * `false` otherwise
fn is_numeric_impl(value: &str) -> bool {
let trimmed = value.trim();
// Empty string is not numeric
if trimmed.is_empty() {
return false;
}
// Try parsing as integer first
if trimmed.parse::<i64>().is_ok() {
return true;
}
// Try parsing as float
if trimmed.parse::<f64>().is_ok() {
return true;
}
false
}
/// Register the NOT operator for boolean negation
/// This enables `NOT IS_NUMERIC(x)` syntax
pub fn not_operator(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("NOT", |value: bool| -> bool { !value });
engine.register_fn("not", |value: bool| -> bool { !value });
debug!("Registered NOT operator");
}
/// Register OR operator for boolean operations
/// This enables `a = "" OR NOT IS_NUMERIC(a)` syntax
pub fn logical_operators(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// OR operator
engine.register_fn("OR", |a: bool, b: bool| -> bool { a || b });
engine.register_fn("or", |a: bool, b: bool| -> bool { a || b });
// AND operator
engine.register_fn("AND", |a: bool, b: bool| -> bool { a && b });
engine.register_fn("and", |a: bool, b: bool| -> bool { a && b });
debug!("Registered logical operators (OR, AND)");
}
/// Register the LOWER function for string case conversion
pub fn lower_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("LOWER", |s: &str| -> String { s.to_lowercase() });
engine.register_fn("lower", |s: &str| -> String { s.to_lowercase() });
engine.register_fn("LCASE", |s: &str| -> String { s.to_lowercase() });
debug!("Registered LOWER/LCASE keyword");
}
/// Register the UPPER function for string case conversion
pub fn upper_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("UPPER", |s: &str| -> String { s.to_uppercase() });
engine.register_fn("upper", |s: &str| -> String { s.to_uppercase() });
engine.register_fn("UCASE", |s: &str| -> String { s.to_uppercase() });
debug!("Registered UPPER/UCASE keyword");
}
/// Register the LEN function for string length
pub fn len_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("LEN", |s: &str| -> i64 { s.len() as i64 });
engine.register_fn("len", |s: &str| -> i64 { s.len() as i64 });
debug!("Registered LEN keyword");
}
/// Register the TRIM function for whitespace removal
pub fn trim_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("TRIM", |s: &str| -> String { s.trim().to_string() });
engine.register_fn("trim", |s: &str| -> String { s.trim().to_string() });
engine.register_fn("LTRIM", |s: &str| -> String { s.trim_start().to_string() });
engine.register_fn("ltrim", |s: &str| -> String { s.trim_start().to_string() });
engine.register_fn("RTRIM", |s: &str| -> String { s.trim_end().to_string() });
engine.register_fn("rtrim", |s: &str| -> String { s.trim_end().to_string() });
debug!("Registered TRIM/LTRIM/RTRIM keywords");
}
/// Register the LEFT function for extracting left portion of string
pub fn left_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("LEFT", |s: &str, count: i64| -> String {
let count = count.max(0) as usize;
s.chars().take(count).collect()
});
engine.register_fn("left", |s: &str, count: i64| -> String {
let count = count.max(0) as usize;
s.chars().take(count).collect()
});
debug!("Registered LEFT keyword");
}
/// Register the RIGHT function for extracting right portion of string
pub fn right_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("RIGHT", |s: &str, count: i64| -> String {
let count = count.max(0) as usize;
let len = s.chars().count();
if count >= len {
s.to_string()
} else {
s.chars().skip(len - count).collect()
}
});
engine.register_fn("right", |s: &str, count: i64| -> String {
let count = count.max(0) as usize;
let len = s.chars().count();
if count >= len {
s.to_string()
} else {
s.chars().skip(len - count).collect()
}
});
debug!("Registered RIGHT keyword");
}
/// Register the MID function for extracting substring
pub fn mid_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// MID(string, start) - from start to end
engine.register_fn("MID", |s: &str, start: i64| -> String {
let start_idx = if start < 1 { 0 } else { (start - 1) as usize };
s.chars().skip(start_idx).collect()
});
// MID(string, start, length) - from start for length chars
engine.register_fn("MID", |s: &str, start: i64, length: i64| -> String {
let start_idx = if start < 1 { 0 } else { (start - 1) as usize };
let len = length.max(0) as usize;
s.chars().skip(start_idx).take(len).collect()
});
engine.register_fn("mid", |s: &str, start: i64| -> String {
let start_idx = if start < 1 { 0 } else { (start - 1) as usize };
s.chars().skip(start_idx).collect()
});
engine.register_fn("mid", |s: &str, start: i64, length: i64| -> String {
let start_idx = if start < 1 { 0 } else { (start - 1) as usize };
let len = length.max(0) as usize;
s.chars().skip(start_idx).take(len).collect()
});
debug!("Registered MID keyword");
}
/// Register the REPLACE function for string replacement
pub fn replace_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("REPLACE", |s: &str, find: &str, replace: &str| -> String {
s.replace(find, replace)
});
engine.register_fn("replace", |s: &str, find: &str, replace: &str| -> String {
s.replace(find, replace)
});
debug!("Registered REPLACE keyword");
}
/// Register all string functions
pub fn register_string_functions(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
instr_keyword(&state, user.clone(), engine);
is_numeric_keyword(&state, user.clone(), engine);
not_operator(&state, user.clone(), engine);
logical_operators(&state, user.clone(), engine);
lower_keyword(&state, user.clone(), engine);
upper_keyword(&state, user.clone(), engine);
len_keyword(&state, user.clone(), engine);
trim_keyword(&state, user.clone(), engine);
left_keyword(&state, user.clone(), engine);
right_keyword(&state, user.clone(), engine);
mid_keyword(&state, user.clone(), engine);
replace_keyword(&state, user, engine);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_instr_basic() {
assert_eq!(instr_impl(1, "Hello, World!", "World"), 8);
assert_eq!(instr_impl(1, "Hello, World!", "o"), 5);
assert_eq!(instr_impl(1, "Hello, World!", "xyz"), 0);
}
#[test]
fn test_instr_with_start() {
assert_eq!(instr_impl(1, "one two one", "one"), 1);
assert_eq!(instr_impl(2, "one two one", "one"), 9);
assert_eq!(instr_impl(10, "one two one", "one"), 0);
}
#[test]
fn test_instr_edge_cases() {
assert_eq!(instr_impl(1, "", "test"), 0);
assert_eq!(instr_impl(1, "test", ""), 0);
assert_eq!(instr_impl(1, "", ""), 0);
}
#[test]
fn test_is_numeric_integers() {
assert!(is_numeric_impl("42"));
assert!(is_numeric_impl("-17"));
assert!(is_numeric_impl("0"));
assert!(is_numeric_impl(" 42 "));
}
#[test]
fn test_is_numeric_decimals() {
assert!(is_numeric_impl("3.14"));
assert!(is_numeric_impl("-0.5"));
assert!(is_numeric_impl(".25"));
assert!(is_numeric_impl("0.0"));
}
#[test]
fn test_is_numeric_scientific() {
assert!(is_numeric_impl("1e10"));
assert!(is_numeric_impl("2.5E-3"));
assert!(is_numeric_impl("-1.5e+2"));
}
#[test]
fn test_is_numeric_invalid() {
assert!(!is_numeric_impl(""));
assert!(!is_numeric_impl("abc"));
assert!(!is_numeric_impl("12abc"));
assert!(!is_numeric_impl("$100"));
assert!(!is_numeric_impl("1,000"));
}
}

View file

@ -0,0 +1,296 @@
//! SWITCH/CASE keyword implementation for BASIC interpreter
//!
//! This module provides multi-way branching functionality similar to classic BASIC:
//! - SWITCH expression
//! - CASE value
//! - CASE value1, value2
//! - DEFAULT
//! - END SWITCH
//!
//! Note: The actual SWITCH/CASE parsing is handled in the preprocessor (mod.rs)
//! because it requires structural transformation. This module provides helper
//! functions for the runtime evaluation.
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::{Dynamic, Engine};
use std::sync::Arc;
/// Register SWITCH/CASE helper functions with the Rhai engine
///
/// The SWITCH statement is transformed during preprocessing into nested
/// if-else statements. These helper functions support that transformation.
pub fn switch_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// Helper function to compare switch expression with case value
// Handles both string and numeric comparisons
engine.register_fn(
"__switch_match",
|expr: Dynamic, case_val: Dynamic| -> bool { switch_match_impl(&expr, &case_val) },
);
// String comparison variant
engine.register_fn("__switch_match_str", |expr: &str, case_val: &str| -> bool {
expr == case_val
});
// Integer comparison variant
engine.register_fn("__switch_match_int", |expr: i64, case_val: i64| -> bool {
expr == case_val
});
// Float comparison variant
engine.register_fn("__switch_match_float", |expr: f64, case_val: f64| -> bool {
(expr - case_val).abs() < f64::EPSILON
});
// Multiple values check - returns true if expr matches any value in the array
engine.register_fn(
"__switch_match_any",
|expr: Dynamic, cases: rhai::Array| -> bool {
for case_val in cases {
if switch_match_impl(&expr, &case_val) {
return true;
}
}
false
},
);
// Case-insensitive string matching (optional)
engine.register_fn(
"__switch_match_icase",
|expr: &str, case_val: &str| -> bool { expr.to_lowercase() == case_val.to_lowercase() },
);
debug!("Registered SWITCH/CASE helper functions");
}
/// Implementation of switch matching logic
///
/// Compares two Dynamic values for equality, handling type coercion
/// where appropriate.
fn switch_match_impl(expr: &Dynamic, case_val: &Dynamic) -> bool {
// Try string comparison first
if let (Some(e), Some(c)) = (
expr.clone().into_string().ok(),
case_val.clone().into_string().ok(),
) {
return e == c;
}
// Try integer comparison
if let (Some(e), Some(c)) = (expr.as_int().ok(), case_val.as_int().ok()) {
return e == c;
}
// Try float comparison
if let (Some(e), Some(c)) = (expr.as_float().ok(), case_val.as_float().ok()) {
return (e - c).abs() < f64::EPSILON;
}
// Try boolean comparison
if let (Some(e), Some(c)) = (expr.as_bool().ok(), case_val.as_bool().ok()) {
return e == c;
}
// Mixed numeric types - int vs float
if let (Some(e), Some(c)) = (expr.as_int().ok(), case_val.as_float().ok()) {
return (e as f64 - c).abs() < f64::EPSILON;
}
if let (Some(e), Some(c)) = (expr.as_float().ok(), case_val.as_int().ok()) {
return (e - c as f64).abs() < f64::EPSILON;
}
false
}
/// Preprocess SWITCH/CASE blocks in BASIC code
///
/// Transforms:
/// ```basic
/// SWITCH expr
/// CASE "value1"
/// statement1
/// CASE "value2", "value3"
/// statement2
/// DEFAULT
/// statement3
/// END SWITCH
/// ```
///
/// Into equivalent if-else chain:
/// ```basic
/// let __switch_expr = expr;
/// if __switch_expr == "value1" {
/// statement1
/// } else if __switch_expr == "value2" || __switch_expr == "value3" {
/// statement2
/// } else {
/// statement3
/// }
/// ```
pub fn preprocess_switch(input: &str) -> String {
let mut result = String::new();
let mut lines: Vec<&str> = input.lines().collect();
let mut i = 0;
let mut switch_counter = 0;
while i < lines.len() {
let line = lines[i].trim();
if line.to_uppercase().starts_with("SWITCH ") {
// Extract the switch expression
let expr = line[7..].trim();
let var_name = format!("__switch_expr_{}", switch_counter);
switch_counter += 1;
// Store the expression in a variable
result.push_str(&format!("let {} = {};\n", var_name, expr));
// Process cases until END SWITCH
i += 1;
let mut first_case = true;
let mut in_default = false;
while i < lines.len() {
let case_line = lines[i].trim();
let case_upper = case_line.to_uppercase();
if case_upper == "END SWITCH" || case_upper == "END_SWITCH" {
// Close the if-else chain
result.push_str("}\n");
break;
} else if case_upper.starts_with("CASE ") {
// Close previous case if not first
if !first_case {
result.push_str("} else ");
}
// Extract case values (may be comma-separated)
let values_str = &case_line[5..];
let values: Vec<&str> = values_str.split(',').map(|s| s.trim()).collect();
// Build condition
if values.len() == 1 {
result.push_str(&format!("if {} == {} {{\n", var_name, values[0]));
} else {
let conditions: Vec<String> = values
.iter()
.map(|v| format!("{} == {}", var_name, v))
.collect();
result.push_str(&format!("if {} {{\n", conditions.join(" || ")));
}
first_case = false;
in_default = false;
} else if case_upper == "DEFAULT" {
// Close previous case
if !first_case {
result.push_str("} else {\n");
}
in_default = true;
} else if !case_line.is_empty()
&& !case_line.starts_with("//")
&& !case_line.starts_with("'")
{
// Regular statement inside case
result.push_str(" ");
result.push_str(case_line);
if !case_line.ends_with(';')
&& !case_line.ends_with('{')
&& !case_line.ends_with('}')
{
result.push(';');
}
result.push('\n');
}
i += 1;
}
} else {
// Non-switch line, pass through
result.push_str(lines[i]);
result.push('\n');
}
i += 1;
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_switch_match_strings() {
let a = Dynamic::from("hello");
let b = Dynamic::from("hello");
let c = Dynamic::from("world");
assert!(switch_match_impl(&a, &b));
assert!(!switch_match_impl(&a, &c));
}
#[test]
fn test_switch_match_integers() {
let a = Dynamic::from(42_i64);
let b = Dynamic::from(42_i64);
let c = Dynamic::from(0_i64);
assert!(switch_match_impl(&a, &b));
assert!(!switch_match_impl(&a, &c));
}
#[test]
fn test_switch_match_floats() {
let a = Dynamic::from(3.14_f64);
let b = Dynamic::from(3.14_f64);
let c = Dynamic::from(2.71_f64);
assert!(switch_match_impl(&a, &b));
assert!(!switch_match_impl(&a, &c));
}
#[test]
fn test_switch_match_mixed_numeric() {
let int_val = Dynamic::from(42_i64);
let float_val = Dynamic::from(42.0_f64);
assert!(switch_match_impl(&int_val, &float_val));
}
#[test]
fn test_preprocess_simple_switch() {
let input = r#"
SWITCH role
CASE "admin"
x = 1
CASE "user"
x = 2
DEFAULT
x = 0
END SWITCH
"#;
let output = preprocess_switch(input);
assert!(output.contains("__switch_expr_"));
assert!(output.contains("if"));
assert!(output.contains("else"));
}
#[test]
fn test_preprocess_multiple_values() {
let input = r#"
SWITCH day
CASE "saturday", "sunday"
weekend = true
DEFAULT
weekend = false
END SWITCH
"#;
let output = preprocess_switch(input);
assert!(output.contains("||"));
}
}

View file

@ -0,0 +1,500 @@
/*****************************************************************************\
| ® |
| |
| |
| |
| |
| |
| 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. |
| |
\*****************************************************************************/
use crate::shared::models::{TriggerKind, UserSession};
use crate::shared::state::AppState;
use diesel::prelude::*;
use log::trace;
use rhai::{Dynamic, Engine};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::error::Error;
use uuid::Uuid;
/// Webhook registration stored in database
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookRegistration {
pub id: Uuid,
pub bot_id: Uuid,
pub endpoint: String,
pub script_name: String,
pub is_active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
}
/// Register the WEBHOOK keyword
///
/// WEBHOOK "order-received"
///
/// This creates an endpoint at /api/botname/webhook/order-received
/// When called, it triggers the script containing the WEBHOOK declaration
/// Request params become available as variables in the script
pub fn webhook_keyword(state: &AppState, _user: UserSession, engine: &mut Engine) {
let state_clone = state.clone();
engine
.register_custom_syntax(&["WEBHOOK", "$expr$"], false, move |context, inputs| {
let endpoint = context.eval_expression_tree(&inputs[0])?.to_string();
trace!("WEBHOOK registration for endpoint: {}", endpoint);
// Note: Actual webhook registration happens during compilation/preprocessing
// This runtime keyword is mainly for documentation and validation
Ok(Dynamic::from(format!("webhook:{}", endpoint)))
})
.unwrap();
}
/// Execute webhook registration during preprocessing
/// This is called by the compiler when it finds a WEBHOOK declaration
pub fn execute_webhook_registration(
conn: &mut diesel::PgConnection,
endpoint: &str,
script_name: &str,
bot_uuid: Uuid,
) -> Result<Value, Box<dyn Error + Send + Sync>> {
trace!(
"Registering WEBHOOK endpoint: {}, script: {}, bot_id: {:?}",
endpoint,
script_name,
bot_uuid
);
// Verify bot exists
use crate::shared::models::bots::dsl::bots;
let bot_exists: bool = diesel::select(diesel::dsl::exists(
bots.filter(crate::shared::models::bots::dsl::id.eq(bot_uuid)),
))
.get_result(conn)?;
if !bot_exists {
return Err(format!("Bot with id {} does not exist", bot_uuid).into());
}
// Clean the endpoint name (remove quotes, spaces)
let clean_endpoint = endpoint
.trim()
.trim_matches('"')
.trim_matches('\'')
.to_lowercase()
.replace(' ', "-")
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
.collect::<String>();
// Register in system_automations table with kind = Webhook
use crate::shared::models::system_automations::dsl::*;
let new_automation = (
bot_id.eq(bot_uuid),
kind.eq(TriggerKind::Webhook as i32),
target.eq(&clean_endpoint),
param.eq(script_name),
is_active.eq(true),
);
// First try to update existing
let update_result = diesel::update(system_automations)
.filter(bot_id.eq(bot_uuid))
.filter(kind.eq(TriggerKind::Webhook as i32))
.filter(target.eq(&clean_endpoint))
.set((param.eq(script_name), is_active.eq(true)))
.execute(&mut *conn)?;
// If no rows updated, insert new
let result = if update_result == 0 {
diesel::insert_into(system_automations)
.values(&new_automation)
.execute(&mut *conn)?
} else {
update_result
};
Ok(json!({
"command": "webhook",
"endpoint": clean_endpoint,
"script": script_name,
"bot_id": bot_uuid.to_string(),
"rows_affected": result
}))
}
/// Remove webhook registration
pub fn remove_webhook_registration(
conn: &mut diesel::PgConnection,
endpoint: &str,
bot_uuid: Uuid,
) -> Result<usize, Box<dyn Error + Send + Sync>> {
use crate::shared::models::system_automations::dsl::*;
let clean_endpoint = endpoint
.trim()
.trim_matches('"')
.trim_matches('\'')
.to_lowercase()
.replace(' ', "-");
let result = diesel::delete(
system_automations
.filter(bot_id.eq(bot_uuid))
.filter(kind.eq(TriggerKind::Webhook as i32))
.filter(target.eq(&clean_endpoint)),
)
.execute(&mut *conn)?;
Ok(result)
}
/// Get all webhooks for a bot
pub fn get_bot_webhooks(
conn: &mut diesel::PgConnection,
bot_uuid: Uuid,
) -> Result<Vec<(String, String, bool)>, Box<dyn Error + Send + Sync>> {
#[derive(QueryableByName)]
struct WebhookRow {
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Text>)]
webhook_target: Option<String>,
#[diesel(sql_type = diesel::sql_types::Text)]
webhook_param: String,
#[diesel(sql_type = diesel::sql_types::Bool)]
webhook_is_active: bool,
}
let results: Vec<WebhookRow> = diesel::sql_query(
"SELECT target as webhook_target, param as webhook_param, is_active as webhook_is_active FROM system_automations WHERE bot_id = $1 AND kind = $2",
)
.bind::<diesel::sql_types::Uuid, _>(bot_uuid)
.bind::<diesel::sql_types::Int4, _>(TriggerKind::Webhook as i32)
.load(conn)?;
Ok(results
.into_iter()
.map(|r| {
(
r.webhook_target.unwrap_or_default(),
r.webhook_param,
r.webhook_is_active,
)
})
.collect())
}
/// Find webhook script by endpoint
pub fn find_webhook_script(
conn: &mut diesel::PgConnection,
bot_uuid: Uuid,
endpoint: &str,
) -> Result<Option<String>, Box<dyn Error + Send + Sync>> {
use crate::shared::models::system_automations::dsl::*;
let clean_endpoint = endpoint
.trim()
.trim_matches('"')
.trim_matches('\'')
.to_lowercase();
let result: Option<String> = system_automations
.filter(bot_id.eq(bot_uuid))
.filter(kind.eq(TriggerKind::Webhook as i32))
.filter(target.eq(&clean_endpoint))
.filter(is_active.eq(true))
.select(param)
.first(conn)
.optional()?;
Ok(result)
}
/// Webhook request data structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookRequest {
pub method: String,
pub headers: std::collections::HashMap<String, String>,
pub query_params: std::collections::HashMap<String, String>,
pub body: Value,
pub path: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
impl WebhookRequest {
/// Create a new webhook request
pub fn new(
method: &str,
headers: std::collections::HashMap<String, String>,
query_params: std::collections::HashMap<String, String>,
body: Value,
path: &str,
) -> Self {
Self {
method: method.to_string(),
headers,
query_params,
body,
path: path.to_string(),
timestamp: chrono::Utc::now(),
}
}
/// Convert to Dynamic for use in BASIC scripts
pub fn to_dynamic(&self) -> Dynamic {
let mut map = rhai::Map::new();
map.insert("method".into(), Dynamic::from(self.method.clone()));
map.insert("path".into(), Dynamic::from(self.path.clone()));
map.insert(
"timestamp".into(),
Dynamic::from(self.timestamp.to_rfc3339()),
);
// Convert headers
let mut headers_map = rhai::Map::new();
for (k, v) in &self.headers {
headers_map.insert(k.clone().into(), Dynamic::from(v.clone()));
}
map.insert("headers".into(), Dynamic::from(headers_map));
// Convert query params
let mut params_map = rhai::Map::new();
for (k, v) in &self.query_params {
params_map.insert(k.clone().into(), Dynamic::from(v.clone()));
}
map.insert("params".into(), Dynamic::from(params_map));
// Convert body
map.insert("body".into(), json_to_dynamic(&self.body));
Dynamic::from(map)
}
}
/// Webhook response structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookResponse {
pub status: u16,
pub headers: std::collections::HashMap<String, String>,
pub body: Value,
}
impl Default for WebhookResponse {
fn default() -> Self {
Self {
status: 200,
headers: std::collections::HashMap::new(),
body: json!({"status": "ok"}),
}
}
}
impl WebhookResponse {
/// Create a success response
pub fn success(data: Value) -> Self {
Self {
status: 200,
headers: std::collections::HashMap::new(),
body: data,
}
}
/// Create an error response
pub fn error(status: u16, message: &str) -> Self {
Self {
status,
headers: std::collections::HashMap::new(),
body: json!({"error": message}),
}
}
/// Convert from Dynamic (returned by BASIC script)
pub fn from_dynamic(value: &Dynamic) -> Self {
if value.is_map() {
let map = value.clone().try_cast::<rhai::Map>().unwrap_or_default();
let status = map
.get("status")
.and_then(|v| v.as_int().ok())
.unwrap_or(200) as u16;
let body = map
.get("body")
.map(dynamic_to_json)
.unwrap_or(json!({"status": "ok"}));
let mut headers = std::collections::HashMap::new();
if let Some(h) = map.get("headers") {
if let Ok(headers_map) = h.clone().try_cast::<rhai::Map>() {
for (k, v) in headers_map {
headers.insert(k.to_string(), v.to_string());
}
}
}
Self {
status,
headers,
body,
}
} else {
Self::success(dynamic_to_json(value))
}
}
}
/// Convert JSON Value to Rhai Dynamic
fn json_to_dynamic(value: &Value) -> Dynamic {
match value {
Value::Null => Dynamic::UNIT,
Value::Bool(b) => Dynamic::from(*b),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Dynamic::from(i)
} else if let Some(f) = n.as_f64() {
Dynamic::from(f)
} else {
Dynamic::UNIT
}
}
Value::String(s) => Dynamic::from(s.clone()),
Value::Array(arr) => {
let rhai_arr: rhai::Array = arr.iter().map(json_to_dynamic).collect();
Dynamic::from(rhai_arr)
}
Value::Object(obj) => {
let mut map = rhai::Map::new();
for (k, v) in obj {
map.insert(k.clone().into(), json_to_dynamic(v));
}
Dynamic::from(map)
}
}
}
/// Convert Rhai Dynamic to JSON Value
fn dynamic_to_json(value: &Dynamic) -> Value {
if value.is_unit() {
Value::Null
} else if value.is_bool() {
Value::Bool(value.as_bool().unwrap_or(false))
} else if value.is_int() {
Value::Number(value.as_int().unwrap_or(0).into())
} else if value.is_float() {
if let Ok(f) = value.as_float() {
serde_json::Number::from_f64(f)
.map(Value::Number)
.unwrap_or(Value::Null)
} else {
Value::Null
}
} else if value.is_string() {
Value::String(value.to_string())
} else if value.is_array() {
let arr = value.clone().into_array().unwrap_or_default();
Value::Array(arr.iter().map(dynamic_to_json).collect())
} else if value.is_map() {
let map = value.clone().try_cast::<rhai::Map>().unwrap_or_default();
let obj: serde_json::Map<String, Value> = map
.iter()
.map(|(k, v)| (k.to_string(), dynamic_to_json(v)))
.collect();
Value::Object(obj)
} else {
Value::String(value.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_webhook_request_to_dynamic() {
let mut headers = std::collections::HashMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
let mut params = std::collections::HashMap::new();
params.insert("id".to_string(), "123".to_string());
let request = WebhookRequest::new(
"POST",
headers,
params,
json!({"order": "test"}),
"/webhook/order-received",
);
let dynamic = request.to_dynamic();
assert!(dynamic.is_map());
}
#[test]
fn test_webhook_response_from_dynamic() {
let mut map = rhai::Map::new();
map.insert("status".into(), Dynamic::from(201_i64));
map.insert(
"body".into(),
Dynamic::from(json!({"message": "created"}).to_string()),
);
let dynamic = Dynamic::from(map);
let response = WebhookResponse::from_dynamic(&dynamic);
assert_eq!(response.status, 201);
}
#[test]
fn test_json_to_dynamic_and_back() {
let original = json!({
"name": "test",
"count": 42,
"active": true,
"items": [1, 2, 3]
});
let dynamic = json_to_dynamic(&original);
let back = dynamic_to_json(&dynamic);
assert_eq!(original["name"], back["name"]);
assert_eq!(original["count"], back["count"]);
assert_eq!(original["active"], back["active"]);
}
#[test]
fn test_webhook_response_default() {
let response = WebhookResponse::default();
assert_eq!(response.status, 200);
}
#[test]
fn test_webhook_response_error() {
let response = WebhookResponse::error(404, "Not found");
assert_eq!(response.status, 404);
assert_eq!(response.body["error"], "Not found");
}
}

View file

@ -1,5 +1,7 @@
use crate::basic::keywords::add_suggestion::clear_suggestions_keyword;
use crate::basic::keywords::set_user::set_user_keyword;
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 log::info;
@ -16,20 +18,25 @@ use self::keywords::clear_tools::clear_tools_keyword;
use self::keywords::create_draft::create_draft_keyword;
use self::keywords::create_site::create_site_keyword;
use self::keywords::create_task::create_task_keyword;
use self::keywords::data_operations::register_data_operations;
use self::keywords::file_operations::register_file_operations;
use self::keywords::find::find_keyword;
use self::keywords::first::first_keyword;
use self::keywords::for_next::for_keyword;
use self::keywords::format::format_keyword;
use self::keywords::get::get_keyword;
use self::keywords::hear_talk::{hear_keyword, talk_keyword};
use self::keywords::http_operations::register_http_operations;
use self::keywords::last::last_keyword;
use self::keywords::multimodal::register_multimodal_keywords;
use self::keywords::remember::remember_keyword;
use self::keywords::save_from_unstructured::save_from_unstructured_keyword;
use self::keywords::send_mail::send_mail_keyword;
use self::keywords::switch_case::preprocess_switch;
use self::keywords::use_kb::register_use_kb_keyword;
use self::keywords::use_tool::use_tool_keyword;
use self::keywords::use_website::{clear_websites_keyword, use_website_keyword};
use self::keywords::webhook::webhook_keyword;
use self::keywords::llm_keyword::llm_keyword;
use self::keywords::on::on_keyword;
@ -48,6 +55,8 @@ impl ScriptService {
let mut engine = Engine::new();
engine.set_allow_anonymous_fn(true);
engine.set_allow_looping(true);
// Core keywords
create_draft_keyword(&state, user.clone(), &mut engine);
set_bot_memory_keyword(state.clone(), user.clone(), &mut engine);
get_bot_memory_keyword(state.clone(), user.clone(), &mut engine);
@ -97,15 +106,42 @@ impl ScriptService {
// These connect to botmodels for image/video/audio generation and vision/captioning
register_multimodal_keywords(state.clone(), user.clone(), &mut engine);
// Register string functions (INSTR, IS_NUMERIC, LEN, LEFT, RIGHT, MID, etc.)
register_string_functions(state.clone(), user.clone(), &mut engine);
// Register SWITCH/CASE helper functions
switch_keyword(&state, user.clone(), &mut engine);
// ========================================================================
// NEW KEYWORDS for office.gbai - Compete with n8n
// ========================================================================
// HTTP Operations: POST, PUT, PATCH, DELETE_HTTP, SET_HEADER, GRAPHQL, SOAP
register_http_operations(state.clone(), user.clone(), &mut engine);
// Data Operations: SAVE, INSERT, UPDATE, DELETE, MERGE, FILL, MAP, FILTER,
// AGGREGATE, JOIN, PIVOT, GROUP_BY
register_data_operations(state.clone(), user.clone(), &mut engine);
// File Operations: READ, WRITE, DELETE_FILE, COPY, MOVE, LIST,
// COMPRESS, EXTRACT, UPLOAD, DOWNLOAD, GENERATE_PDF, MERGE_PDF
register_file_operations(state.clone(), user.clone(), &mut engine);
// Webhook keyword for event-driven automation
webhook_keyword(&state, user.clone(), &mut engine);
ScriptService { engine }
}
fn preprocess_basic_script(&self, script: &str) -> String {
// First, preprocess SWITCH/CASE blocks
let script = preprocess_switch(script);
let mut result = String::new();
let mut for_stack: Vec<usize> = Vec::new();
let mut current_indent = 0;
for line in script.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with("//") {
if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("'") {
continue;
}
if trimmed.starts_with("FOR EACH") {
@ -142,6 +178,7 @@ impl ScriptService {
}
result.push_str(&" ".repeat(current_indent));
let basic_commands = [
// Core commands
"SET",
"CREATE",
"PRINT",
@ -168,6 +205,56 @@ impl ScriptService {
"AUDIO",
"SEE",
"SEND FILE",
"SWITCH",
"CASE",
"DEFAULT",
"END SWITCH",
"USE KB",
"CLEAR KB",
"USE TOOL",
"CLEAR TOOLS",
"ADD SUGGESTION",
"CLEAR SUGGESTIONS",
"INSTR",
"IS_NUMERIC",
"IS NUMERIC",
// HTTP Operations
"POST",
"PUT",
"PATCH",
"DELETE_HTTP",
"SET_HEADER",
"CLEAR_HEADERS",
"GRAPHQL",
"SOAP",
// Data Operations
"SAVE",
"INSERT",
"UPDATE",
"DELETE",
"MERGE",
"FILL",
"MAP",
"FILTER",
"AGGREGATE",
"JOIN",
"PIVOT",
"GROUP_BY",
// File Operations
"READ",
"WRITE",
"DELETE_FILE",
"COPY",
"MOVE",
"LIST",
"COMPRESS",
"EXTRACT",
"UPLOAD",
"DOWNLOAD",
"GENERATE_PDF",
"MERGE_PDF",
// Webhook
"WEBHOOK",
];
let is_basic_command = basic_commands.iter().any(|&cmd| trimmed.starts_with(cmd));
let is_control_flow = trimmed.starts_with("IF")

View file

@ -9,6 +9,7 @@ pub enum TriggerKind {
TableUpdate = 1,
TableInsert = 2,
TableDelete = 3,
Webhook = 4,
}
impl TriggerKind {
pub fn _from_i32(value: i32) -> Option<Self> {
@ -17,6 +18,7 @@ impl TriggerKind {
1 => Some(Self::TableUpdate),
2 => Some(Self::TableInsert),
3 => Some(Self::TableDelete),
4 => Some(Self::Webhook),
_ => None,
}
}

View file

@ -113,23 +113,18 @@ pub async fn login_submit(
return Redirect::to(&auth_url).into_response();
} else {
// Development mode: Create local session
warn!("Zitadel not available, using development authentication");
// Development mode: Authentication is required via Zitadel
// Do not use hardcoded credentials - configure Zitadel for proper authentication
warn!("Zitadel not configured. Please set up Zitadel for authentication.");
warn!("See docs/src/chapter-12-auth/README.md for authentication setup.");
// Simple password check for development
if form.password != "password" {
return LoginTemplate {
error_message: Some("Invalid credentials".to_string()),
redirect_url: None,
}
.into_response();
return LoginTemplate {
error_message: Some(
"Authentication service not configured. Please contact administrator.".to_string(),
),
redirect_url: None,
}
create_dev_session(
&form.email,
&form.email.split('@').next().unwrap_or("User"),
&auth_config,
)
.into_response();
};
// Store session

View file

@ -0,0 +1,301 @@
' API Integration Bot - Demonstrates HTTP & API operations keywords
' This template shows how to use POST, PUT, PATCH, DELETE_HTTP, GRAPHQL, SOAP, and SET_HEADER
' ============================================================================
' WEBHOOK: External systems can trigger API operations via HTTP POST
' Endpoint: /api/office/webhook/api-gateway
' ============================================================================
WEBHOOK "api-gateway"
TALK "API Integration Bot initialized..."
' ============================================================================
' EXAMPLE 1: Basic REST API calls with authentication
' ============================================================================
' Set up authentication headers (reused for subsequent requests)
api_key = GET_BOT_MEMORY("external_api_key")
SET_HEADER "Authorization", "Bearer " + api_key
SET_HEADER "Content-Type", "application/json"
SET_HEADER "X-Client-ID", "office-bot"
' GET request (using existing GET keyword)
users = GET "https://api.example.com/users"
TALK "Retrieved " + UBOUND(users) + " users from API"
' ============================================================================
' EXAMPLE 2: POST - Create new resources
' ============================================================================
' Create a new customer
new_customer = #{
"name": "John Doe",
"email": "john.doe@example.com",
"phone": "+1-555-0123",
"company": "Acme Corp",
"tier": "enterprise"
}
create_response = POST "https://api.example.com/customers", new_customer
IF create_response.status = 201 THEN
TALK "Customer created successfully with ID: " + create_response.data.id
SET_BOT_MEMORY "last_customer_id", create_response.data.id
ELSE
TALK "Failed to create customer: " + create_response.data.error
END IF
' ============================================================================
' EXAMPLE 3: PUT - Full resource update
' ============================================================================
' Update entire customer record
customer_id = GET_BOT_MEMORY("last_customer_id")
updated_customer = #{
"name": "John Doe",
"email": "john.doe@newdomain.com",
"phone": "+1-555-9999",
"company": "Acme Corp International",
"tier": "enterprise",
"status": "active",
"updated_at": NOW()
}
put_response = PUT "https://api.example.com/customers/" + customer_id, updated_customer
IF put_response.status = 200 THEN
TALK "Customer fully updated"
END IF
' ============================================================================
' EXAMPLE 4: PATCH - Partial resource update
' ============================================================================
' Update only specific fields
partial_update = #{
"tier": "premium",
"notes": "Upgraded from enterprise plan"
}
patch_response = PATCH "https://api.example.com/customers/" + customer_id, partial_update
IF patch_response.status = 200 THEN
TALK "Customer tier upgraded to premium"
END IF
' ============================================================================
' EXAMPLE 5: DELETE_HTTP - Remove resources
' ============================================================================
' Delete a temporary resource
temp_resource_id = "temp-12345"
delete_response = DELETE_HTTP "https://api.example.com/temp-files/" + temp_resource_id
IF delete_response.status = 204 OR delete_response.status = 200 THEN
TALK "Temporary resource deleted"
END IF
' ============================================================================
' EXAMPLE 6: Working with multiple APIs
' ============================================================================
' Clear headers and set new ones for different API
CLEAR_HEADERS
' Stripe-style API authentication
SET_HEADER "Authorization", "Basic " + GET_BOT_MEMORY("stripe_api_key")
SET_HEADER "Content-Type", "application/x-www-form-urlencoded"
' Create a payment intent
payment_data = #{
"amount": 2999,
"currency": "usd",
"customer": "cus_abc123",
"description": "Order #12345"
}
payment_response = POST "https://api.stripe.com/v1/payment_intents", payment_data
IF payment_response.status = 200 THEN
payment_intent_id = payment_response.data.id
client_secret = payment_response.data.client_secret
TALK "Payment intent created: " + payment_intent_id
END IF
CLEAR_HEADERS
' ============================================================================
' EXAMPLE 7: GraphQL API calls
' ============================================================================
' Query users with GraphQL
graphql_query = "query GetUsers($limit: Int!, $status: String) { users(first: $limit, status: $status) { id name email role createdAt } }"
graphql_vars = #{
"limit": 10,
"status": "active"
}
graphql_response = GRAPHQL "https://api.example.com/graphql", graphql_query, graphql_vars
IF graphql_response.status = 200 THEN
users = graphql_response.data.data.users
FOR EACH user IN users
TALK "User: " + user.name + " (" + user.email + ")"
NEXT user
END IF
' GraphQL mutation example
mutation_query = "mutation CreateTask($input: TaskInput!) { createTask(input: $input) { id title status assignee { name } } }"
mutation_vars = #{
"input": #{
"title": "Review quarterly report",
"description": "Review and approve Q4 financial report",
"assigneeId": "user-456",
"dueDate": FORMAT(DATEADD(TODAY(), "day", 7), "yyyy-MM-dd"),
"priority": "high"
}
}
mutation_response = GRAPHQL "https://api.example.com/graphql", mutation_query, mutation_vars
IF mutation_response.status = 200 THEN
task = mutation_response.data.data.createTask
TALK "Task created: " + task.title + " (assigned to " + task.assignee.name + ")"
END IF
' ============================================================================
' EXAMPLE 8: SOAP API calls (Legacy system integration)
' ============================================================================
' Call a SOAP web service for legacy ERP integration
soap_params = #{
"customerNumber": "CUST-001",
"orderDate": FORMAT(TODAY(), "yyyy-MM-dd"),
"productCode": "PRD-12345",
"quantity": 5
}
soap_response = SOAP "https://erp.legacy.example.com/OrderService.wsdl", "CreateOrder", soap_params
IF soap_response.status = 200 THEN
order_number = soap_response.data.raw
TALK "Legacy order created: " + order_number
END IF
' Another SOAP call - Get inventory status
inventory_params = #{
"warehouseCode": "WH-01",
"productCode": "PRD-12345"
}
inventory_response = SOAP "https://erp.legacy.example.com/InventoryService.wsdl", "GetStock", inventory_params
TALK "Current stock level retrieved from legacy system"
' ============================================================================
' EXAMPLE 9: Chained API calls (workflow)
' ============================================================================
TALK "Starting order fulfillment workflow..."
' Step 1: Create order in main system
SET_HEADER "Authorization", "Bearer " + api_key
order_data = #{
"customer_id": customer_id,
"items": [
#{ "sku": "WIDGET-001", "quantity": 2, "price": 29.99 },
#{ "sku": "GADGET-002", "quantity": 1, "price": 49.99 }
],
"shipping_address": #{
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip": "90210"
}
}
order_response = POST "https://api.example.com/orders", order_data
order_id = order_response.data.id
' Step 2: Request shipping quote
shipping_request = #{
"origin_zip": "10001",
"destination_zip": "90210",
"weight_lbs": 5,
"dimensions": #{ "length": 12, "width": 8, "height": 6 }
}
shipping_response = POST "https://api.shipping.com/quotes", shipping_request
shipping_cost = shipping_response.data.rate
' Step 3: Update order with shipping info
shipping_update = #{
"shipping_method": shipping_response.data.service,
"shipping_cost": shipping_cost,
"estimated_delivery": shipping_response.data.estimated_delivery
}
PATCH "https://api.example.com/orders/" + order_id, shipping_update
' Step 4: Notify warehouse
warehouse_notification = #{
"order_id": order_id,
"priority": "standard",
"ship_by": FORMAT(DATEADD(TODAY(), "day", 2), "yyyy-MM-dd")
}
POST "https://api.warehouse.example.com/pick-requests", warehouse_notification
TALK "Order fulfillment workflow complete for order: " + order_id
CLEAR_HEADERS
' ============================================================================
' EXAMPLE 10: Error handling and retries
' ============================================================================
' Attempt API call with error handling
max_retries = 3
retry_count = 0
success = false
WHILE retry_count < max_retries AND success = false
SET_HEADER "Authorization", "Bearer " + api_key
health_check = GET "https://api.example.com/health"
IF health_check.status = 200 THEN
success = true
TALK "API health check passed"
ELSE
retry_count = retry_count + 1
TALK "API check failed, attempt " + retry_count + " of " + max_retries
WAIT 2
END IF
CLEAR_HEADERS
WEND
IF success = false THEN
TALK "API is currently unavailable after " + max_retries + " attempts"
END IF
' ============================================================================
' Return webhook response with summary
' ============================================================================
result = #{
"status": "success",
"timestamp": NOW(),
"operations_completed": #{
"customers_created": 1,
"orders_processed": 1,
"api_calls_made": 12
},
"integrations_tested": ["REST", "GraphQL", "SOAP"]
}
TALK "API Integration examples completed!"

View file

@ -0,0 +1,195 @@
' Data Sync Bot - Demonstrates new data operations keywords
' This template shows how to use MERGE, FILTER, AGGREGATE, JOIN, and other data keywords
' ============================================================================
' WEBHOOK: External systems can trigger this sync via HTTP POST
' Endpoint: /api/office/webhook/data-sync
' ============================================================================
WEBHOOK "data-sync"
TALK "Starting data synchronization..."
' ============================================================================
' EXAMPLE 1: Fetch and merge external data
' ============================================================================
' Fetch customers from external CRM API
SET_HEADER "Authorization", "Bearer " + GET_BOT_MEMORY("crm_api_key")
SET_HEADER "Content-Type", "application/json"
external_customers = GET "https://api.crm.example.com/customers"
' Merge external data with local database using email as the key
merge_result = MERGE "customers", external_customers, "email"
TALK "Customer sync complete: " + merge_result.inserted + " new, " + merge_result.updated + " updated"
CLEAR_HEADERS
' ============================================================================
' EXAMPLE 2: Data transformation with MAP and FILL
' ============================================================================
' Read raw order data
orders = FIND "orders.xlsx", "status=pending"
' Map field names to match our internal format
mapped_orders = MAP orders, "customerName->customer, orderDate->date, totalAmount->amount"
' Fill a report template with the data
report_template = #{
"title": "Order Report for {{customer}}",
"summary": "Order placed on {{date}} for ${{amount}}",
"processed_at": NOW()
}
report_data = FILL mapped_orders, report_template
' ============================================================================
' EXAMPLE 3: Filtering and aggregation
' ============================================================================
' Get all sales data
sales = FIND "sales.xlsx"
' Filter for this month's sales
this_month = FILTER sales, "date>=" + FORMAT(DATEADD(TODAY(), "month", -1), "yyyy-MM-dd")
' Filter high-value transactions
high_value = FILTER this_month, "amount>1000"
' Calculate aggregates
total_sales = AGGREGATE "SUM", this_month, "amount"
average_sale = AGGREGATE "AVG", this_month, "amount"
sale_count = AGGREGATE "COUNT", this_month, "id"
max_sale = AGGREGATE "MAX", this_month, "amount"
min_sale = AGGREGATE "MIN", this_month, "amount"
TALK "This month's statistics:"
TALK "- Total sales: $" + total_sales
TALK "- Average sale: $" + average_sale
TALK "- Number of sales: " + sale_count
TALK "- Largest sale: $" + max_sale
TALK "- Smallest sale: $" + min_sale
' ============================================================================
' EXAMPLE 4: Joining datasets
' ============================================================================
' Load related data
customers = FIND "customers.xlsx"
orders = FIND "orders.xlsx"
products = FIND "products.xlsx"
' Join orders with customer information
orders_with_customers = JOIN orders, customers, "customer_id"
' Now join with product data
complete_orders = JOIN orders_with_customers, products, "product_id"
' ============================================================================
' EXAMPLE 5: Grouping and pivoting
' ============================================================================
' Group sales by salesperson
sales_by_rep = GROUP_BY sales, "salesperson"
' Create pivot table: sales by month
monthly_pivot = PIVOT sales, "month", "amount"
TALK "Monthly sales pivot created with " + UBOUND(monthly_pivot) + " rows"
' ============================================================================
' EXAMPLE 6: Database CRUD operations
' ============================================================================
' Insert a new sync log entry
log_entry = #{
"sync_type": "full",
"started_at": NOW(),
"records_processed": sale_count,
"status": "completed"
}
insert_result = INSERT "sync_logs", log_entry
TALK "Created sync log: " + insert_result.id
' Update existing records
rows_updated = UPDATE "customers", "last_sync<" + FORMAT(DATEADD(TODAY(), "day", -7), "yyyy-MM-dd"), #{
"needs_refresh": true,
"updated_at": NOW()
}
TALK "Marked " + rows_updated + " customers for refresh"
' Save with upsert (insert or update)
summary = #{
"date": TODAY(),
"total_sales": total_sales,
"order_count": sale_count,
"sync_status": "complete"
}
SAVE "daily_summaries", FORMAT(TODAY(), "yyyy-MM-dd"), summary
' ============================================================================
' EXAMPLE 7: File operations for reporting
' ============================================================================
' Generate a report
report_content = "Daily Sales Report - " + TODAY() + "\n\n"
report_content = report_content + "Total Sales: $" + total_sales + "\n"
report_content = report_content + "Transactions: " + sale_count + "\n"
report_content = report_content + "Average: $" + average_sale + "\n"
' Write report to file
WRITE "reports/daily/" + FORMAT(TODAY(), "yyyy-MM-dd") + ".txt", report_content
' List all reports
all_reports = LIST "reports/daily/"
TALK "Total reports in archive: " + UBOUND(all_reports)
' ============================================================================
' EXAMPLE 8: HTTP POST to external system
' ============================================================================
' Send summary to analytics platform
analytics_payload = #{
"event": "daily_sync_complete",
"data": #{
"date": TODAY(),
"total_sales": total_sales,
"transaction_count": sale_count,
"new_customers": merge_result.inserted
}
}
SET_HEADER "X-API-Key", GET_BOT_MEMORY("analytics_api_key")
analytics_response = POST "https://analytics.example.com/events", analytics_payload
CLEAR_HEADERS
' ============================================================================
' EXAMPLE 9: Cleanup old data
' ============================================================================
' Delete old sync logs (older than 30 days)
cutoff_date = FORMAT(DATEADD(TODAY(), "day", -30), "yyyy-MM-dd")
deleted_logs = DELETE "sync_logs", "created_at<" + cutoff_date
IF deleted_logs > 0 THEN
TALK "Cleaned up " + deleted_logs + " old sync log entries"
END IF
' ============================================================================
' Return webhook response
' ============================================================================
result = #{
"status": "success",
"timestamp": NOW(),
"summary": #{
"customers_synced": merge_result.inserted + merge_result.updated,
"sales_processed": sale_count,
"total_revenue": total_sales,
"logs_cleaned": deleted_logs
}
}
TALK "Data synchronization complete!"

View file

@ -0,0 +1,329 @@
' Document Processor Bot - Demonstrates file operations keywords
' This template shows how to use READ, WRITE, COPY, MOVE, LIST, COMPRESS, EXTRACT, etc.
' ============================================================================
' WEBHOOK: External systems can trigger document processing via HTTP POST
' Endpoint: /api/office/webhook/process-documents
' ============================================================================
WEBHOOK "process-documents"
TALK "Document Processor initialized..."
' ============================================================================
' EXAMPLE 1: Reading and writing files
' ============================================================================
' Read a configuration file
config_content = READ "config/settings.json"
TALK "Loaded configuration file"
' Read a text report
daily_report = READ "reports/daily-summary.txt"
' Write processed data to a new file
processed_data = "Processed at: " + NOW() + "\n"
processed_data = processed_data + "Original content length: " + LEN(daily_report) + " characters\n"
processed_data = processed_data + "Status: Complete\n"
WRITE "reports/processed/" + FORMAT(TODAY(), "yyyy-MM-dd") + "-log.txt", processed_data
TALK "Processing log created"
' Write JSON data
summary_json = #{
"date": TODAY(),
"files_processed": 5,
"total_size_kb": 1250,
"status": "success"
}
WRITE "reports/summary.json", summary_json
' ============================================================================
' EXAMPLE 2: Listing directory contents
' ============================================================================
' List all files in the inbox folder
inbox_files = LIST "inbox/"
TALK "Found " + UBOUND(inbox_files) + " files in inbox"
' Process each file in the inbox
FOR EACH file IN inbox_files
TALK "Processing: " + file
NEXT file
' List reports folder
report_files = LIST "reports/"
TALK "Total reports in archive: " + UBOUND(report_files)
' List with subdirectory
template_files = LIST "templates/documents/"
' ============================================================================
' EXAMPLE 3: Copying files
' ============================================================================
' Copy a template for a new customer
customer_name = "acme-corp"
COPY "templates/invoice-template.docx", "customers/" + customer_name + "/invoice-draft.docx"
TALK "Invoice template copied for " + customer_name
' Copy to backup location
COPY "data/important-data.xlsx", "backups/" + FORMAT(TODAY(), "yyyy-MM-dd") + "-data-backup.xlsx"
TALK "Backup created"
' Copy multiple files using a loop
template_types = ["contract", "nda", "proposal"]
FOR EACH template_type IN template_types
COPY "templates/" + template_type + ".docx", "customers/" + customer_name + "/" + template_type + ".docx"
NEXT template_type
TALK "All templates copied for customer"
' ============================================================================
' EXAMPLE 4: Moving and renaming files
' ============================================================================
' Move processed files from inbox to processed folder
FOR EACH file IN inbox_files
' Extract just the filename from the path
filename = file
MOVE "inbox/" + filename, "processed/" + FORMAT(TODAY(), "yyyy-MM-dd") + "-" + filename
NEXT file
TALK "Inbox files moved to processed folder"
' Rename a file (move within same directory)
MOVE "drafts/report-v1.docx", "drafts/report-final.docx"
TALK "Report renamed to final version"
' Archive old files
old_reports = LIST "reports/2023/"
FOR EACH old_report IN old_reports
MOVE "reports/2023/" + old_report, "archive/2023/" + old_report
NEXT old_report
' ============================================================================
' EXAMPLE 5: Deleting files
' ============================================================================
' Delete temporary files
temp_files = LIST "temp/"
FOR EACH temp_file IN temp_files
DELETE_FILE "temp/" + temp_file
NEXT temp_file
TALK "Temporary files cleaned up"
' Delete a specific file
DELETE_FILE "cache/old-cache.dat"
' Delete processed inbox files older than 30 days
' (In real usage, you'd check file dates)
DELETE_FILE "processed/old-file.txt"
' ============================================================================
' EXAMPLE 6: Creating ZIP archives
' ============================================================================
' Compress monthly reports into a single archive
monthly_reports = [
"reports/week1.pdf",
"reports/week2.pdf",
"reports/week3.pdf",
"reports/week4.pdf",
"reports/summary.xlsx"
]
archive_name = "archives/monthly-" + FORMAT(TODAY(), "yyyy-MM") + ".zip"
COMPRESS monthly_reports, archive_name
TALK "Monthly reports compressed to: " + archive_name
' Compress customer documents
customer_docs = LIST "customers/" + customer_name + "/"
customer_archive = "archives/customers/" + customer_name + "-documents.zip"
COMPRESS customer_docs, customer_archive
TALK "Customer documents archived"
' ============================================================================
' EXAMPLE 7: Extracting archives
' ============================================================================
' Extract uploaded archive
uploaded_archive = "uploads/new-documents.zip"
extracted_files = EXTRACT uploaded_archive, "inbox/extracted/"
TALK "Extracted " + UBOUND(extracted_files) + " files from archive"
' Process extracted files
FOR EACH extracted_file IN extracted_files
TALK "Extracted: " + extracted_file
NEXT extracted_file
' Extract to specific destination
EXTRACT "imports/data-import.zip", "data/imported/"
' ============================================================================
' EXAMPLE 8: Upload and download operations
' ============================================================================
' Download a file from external URL
external_url = "https://example.com/reports/external-report.pdf"
local_path = "downloads/external-report-" + FORMAT(TODAY(), "yyyy-MM-dd") + ".pdf"
downloaded_path = DOWNLOAD external_url, local_path
TALK "Downloaded external report to: " + downloaded_path
' Download multiple files
download_urls = [
"https://api.example.com/exports/data1.csv",
"https://api.example.com/exports/data2.csv",
"https://api.example.com/exports/data3.csv"
]
counter = 1
FOR EACH url IN download_urls
DOWNLOAD url, "imports/data-" + counter + ".csv"
counter = counter + 1
NEXT url
TALK "Downloaded " + UBOUND(download_urls) + " data files"
' Upload a file to storage
HEAR attachment AS FILE
IF attachment != "" THEN
upload_destination = "uploads/" + attachment.filename
upload_url = UPLOAD attachment, upload_destination
TALK "File uploaded to: " + upload_url
END IF
' ============================================================================
' EXAMPLE 9: PDF Generation
' ============================================================================
' Generate an invoice PDF from template
invoice_data = #{
"invoice_number": "INV-2024-001",
"customer_name": "Acme Corporation",
"customer_address": "123 Business Ave, Suite 100",
"date": FORMAT(TODAY(), "MMMM dd, yyyy"),
"due_date": FORMAT(DATEADD(TODAY(), "day", 30), "MMMM dd, yyyy"),
"items": [
#{ "description": "Consulting Services", "quantity": 10, "rate": 150, "amount": 1500 },
#{ "description": "Software License", "quantity": 1, "rate": 500, "amount": 500 },
#{ "description": "Support Package", "quantity": 1, "rate": 200, "amount": 200 }
],
"subtotal": 2200,
"tax": 176,
"total": 2376
}
invoice_pdf = GENERATE_PDF "templates/invoice.html", invoice_data, "invoices/INV-2024-001.pdf"
TALK "Invoice PDF generated: " + invoice_pdf.url
' Generate a report PDF
report_data = #{
"title": "Monthly Performance Report",
"period": FORMAT(TODAY(), "MMMM yyyy"),
"author": "Office Bot",
"generated_at": NOW(),
"metrics": #{
"total_sales": 125000,
"new_customers": 45,
"satisfaction_score": 4.7
}
}
report_pdf = GENERATE_PDF "templates/report.html", report_data, "reports/monthly-" + FORMAT(TODAY(), "yyyy-MM") + ".pdf"
TALK "Report PDF generated: " + report_pdf.url
' ============================================================================
' EXAMPLE 10: Merging PDF files
' ============================================================================
' Merge multiple PDFs into a single document
pdfs_to_merge = [
"documents/cover-page.pdf",
"documents/table-of-contents.pdf",
"documents/chapter1.pdf",
"documents/chapter2.pdf",
"documents/chapter3.pdf",
"documents/appendix.pdf"
]
merged_pdf = MERGE_PDF pdfs_to_merge, "publications/complete-manual.pdf"
TALK "PDFs merged into: " + merged_pdf.url
' Merge customer documents for a single package
customer_pdfs = [
"customers/" + customer_name + "/contract.pdf",
"customers/" + customer_name + "/terms.pdf",
"customers/" + customer_name + "/invoice.pdf"
]
customer_package = MERGE_PDF customer_pdfs, "customers/" + customer_name + "/welcome-package.pdf"
TALK "Customer welcome package created"
' ============================================================================
' EXAMPLE 11: Document workflow automation
' ============================================================================
TALK "Starting document workflow..."
' Step 1: Check inbox for new documents
new_docs = LIST "inbox/"
doc_count = UBOUND(new_docs)
IF doc_count > 0 THEN
TALK "Processing " + doc_count + " new documents"
' Step 2: Copy originals to backup
FOR EACH doc IN new_docs
COPY "inbox/" + doc, "backups/inbox/" + FORMAT(TODAY(), "yyyy-MM-dd") + "-" + doc
NEXT doc
' Step 3: Move to processing folder
FOR EACH doc IN new_docs
MOVE "inbox/" + doc, "processing/" + doc
NEXT doc
' Step 4: Process documents (simplified)
processing_docs = LIST "processing/"
processed_list = []
FOR EACH doc IN processing_docs
' Read and process
content = READ "processing/" + doc
' Write processed version
WRITE "completed/" + doc, content
' Track processed file
processed_list = processed_list + [doc]
' Clean up processing folder
DELETE_FILE "processing/" + doc
NEXT doc
' Step 5: Archive completed documents
IF UBOUND(processed_list) > 0 THEN
COMPRESS processed_list, "archives/batch-" + FORMAT(NOW(), "yyyy-MM-dd-HHmm") + ".zip"
END IF
TALK "Workflow complete: " + UBOUND(processed_list) + " documents processed"
ELSE
TALK "No new documents to process"
END IF
' ============================================================================
' Return webhook response with summary
' ============================================================================
result = #{
"status": "success",
"timestamp": NOW(),
"documents_processed": doc_count,
"operations": #{
"files_read": 5,
"files_written": 8,
"files_copied": 6,
"files_moved": doc_count,
"archives_created": 2,
"pdfs_generated": 2,
"pdfs_merged": 2
}
}
TALK "Document processing complete!"

View file

@ -0,0 +1,104 @@
' Office Bot - Role-based Knowledge Base Routing
' This template demonstrates SWITCH keyword for multi-tenant office environments
' Get user role from session or directory
role = GET role
' If no role set, ask the user
IF role = "" THEN
TALK "Welcome to the Office Assistant!"
TALK "Please select your role:"
ADD SUGGESTION "Manager"
ADD SUGGESTION "Developer"
ADD SUGGESTION "Customer"
ADD SUGGESTION "HR"
ADD SUGGESTION "Finance"
role = HEAR "What is your role?"
role = LOWER(role)
SET role, role
END IF
' Route to appropriate knowledge bases based on role
SWITCH role
CASE "manager"
SET CONTEXT "You are an executive assistant helping managers with reports, team management, and strategic decisions."
USE KB "management"
USE KB "reports"
USE KB "team-policies"
TALK "Welcome, Manager! I can help you with reports, team management, and company policies."
CASE "developer"
SET CONTEXT "You are a technical assistant helping developers with documentation, APIs, and coding best practices."
USE KB "documentation"
USE KB "apis"
USE KB "coding-standards"
TALK "Welcome, Developer! I can help you with technical documentation, APIs, and development guidelines."
CASE "customer"
SET CONTEXT "You are a customer service assistant. Be helpful, friendly, and focus on resolving customer issues."
USE KB "products"
USE KB "support"
USE KB "faq"
TALK "Welcome! I'm here to help you with our products and services. How can I assist you today?"
CASE "hr"
SET CONTEXT "You are an HR assistant helping with employee matters, policies, and benefits."
USE KB "hr-policies"
USE KB "benefits"
USE KB "onboarding"
TALK "Welcome, HR! I can help you with employee policies, benefits information, and onboarding procedures."
CASE "finance"
SET CONTEXT "You are a finance assistant helping with budgets, expenses, and financial reports."
USE KB "budgets"
USE KB "expenses"
USE KB "financial-reports"
TALK "Welcome, Finance! I can help you with budget queries, expense policies, and financial reporting."
DEFAULT
SET CONTEXT "You are a general office assistant. Help users with common office tasks and direct them to appropriate resources."
USE KB "general"
USE KB "faq"
TALK "Welcome! I'm your general office assistant. How can I help you today?"
END SWITCH
' Load common tools available to all roles
USE TOOL "calendar"
USE TOOL "tasks"
USE TOOL "documents"
' Set up suggestions based on role
CLEAR SUGGESTIONS
SWITCH role
CASE "manager"
ADD SUGGESTION "Show team performance"
ADD SUGGESTION "Generate report"
ADD SUGGESTION "Schedule meeting"
CASE "developer"
ADD SUGGESTION "Search documentation"
ADD SUGGESTION "API reference"
ADD SUGGESTION "Code review checklist"
CASE "customer"
ADD SUGGESTION "Track my order"
ADD SUGGESTION "Product information"
ADD SUGGESTION "Contact support"
CASE "hr"
ADD SUGGESTION "Employee handbook"
ADD SUGGESTION "Benefits overview"
ADD SUGGESTION "New hire checklist"
CASE "finance"
ADD SUGGESTION "Expense policy"
ADD SUGGESTION "Budget status"
ADD SUGGESTION "Approval workflow"
DEFAULT
ADD SUGGESTION "Help"
ADD SUGGESTION "Contact directory"
ADD SUGGESTION "Office hours"
END SWITCH

View file

@ -0,0 +1,14 @@
key,value
bot-name,Office Assistant
bot-description,Role-based office assistant with knowledge base routing
llm-provider,openai
llm-model,gpt-4o-mini
llm-temperature,0.7
max-tokens,2048
system-prompt,You are a helpful office assistant that adapts to different user roles and provides relevant information from the appropriate knowledge bases.
welcome-message,Welcome to the Office Assistant! I'm here to help with your work tasks.
default-role,customer
enable-voice,true
enable-suggestions,true
session-timeout,3600
max-context-messages,20
1 key value
2 bot-name Office Assistant
3 bot-description Role-based office assistant with knowledge base routing
4 llm-provider openai
5 llm-model gpt-4o-mini
6 llm-temperature 0.7
7 max-tokens 2048
8 system-prompt You are a helpful office assistant that adapts to different user roles and provides relevant information from the appropriate knowledge bases.
9 welcome-message Welcome to the Office Assistant! I'm here to help with your work tasks.
10 default-role customer
11 enable-voice true
12 enable-suggestions true
13 session-timeout 3600
14 max-context-messages 20

View file

@ -0,0 +1,509 @@
<div class="monitoring-container" id="monitoring-app">
<header class="monitoring-header">
<h2>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
class="header-icon"
>
<circle
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="1.5"
fill="none"
opacity="0.3</h2>"
/>
<circle
cx="12"
cy="12"
r="6.5"
stroke="currentColor"
stroke-width="1.5"
fill="none"
opacity="0.6"
/>
<circle cx="12" cy="12" r="2" fill="currentColor" />
<line
x1="12"
y1="2"
x2="12"
y2="5"
stroke="currentColor"
stroke-width="1.5"
/>
<line
x1="12"
y1="19"
x2="12"
y2="22"
stroke="currentColor"
stroke-width="1.5"
/>
<line
x1="2"
y1="12"
x2="5"
y2="12"
stroke="currentColor"
stroke-width="1.5"
/>
<line
x1="19"
y1="12"
x2="22"
y2="12"
stroke="currentColor"
stroke-width="1.5"
/>
</svg>
Monitoring Dashboard
</h2>
<span
class="last-updated"
hx-get="/api/monitoring/timestamp"
hx-trigger="load, every 5s"
hx-swap="innerHTML"
>--</span
>
</header>
<div class="monitoring-grid">
<!-- Sessions Panel -->
<div
class="monitor-panel"
hx-get="/api/monitoring/sessions"
hx-trigger="load, every 5s"
hx-swap="innerHTML"
>
<div class="panel-header">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
class="panel-icon"
>
<circle
cx="12"
cy="8"
r="4"
stroke="currentColor"
stroke-width="1.5"
/>
<path
d="M4 20c0-4 4-6 8-6s8 2 8 6"
stroke="currentColor"
stroke-width="1.5"
/>
</svg>
<span>Sessions</span>
</div>
<div class="tree-view">
<div class="tree-node">
<span class="tree-branch"></span>
<span class="tree-label">Loading...</span>
</div>
</div>
</div>
<!-- Messages Panel -->
<div
class="monitor-panel"
hx-get="/api/monitoring/messages"
hx-trigger="load, every 10s"
hx-swap="innerHTML"
>
<div class="panel-header">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
class="panel-icon"
>
<path
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
stroke="currentColor"
stroke-width="1.5"
/>
</svg>
<span>Messages</span>
</div>
<div class="tree-view">
<div class="tree-node">
<span class="tree-branch"></span>
<span class="tree-label">Loading...</span>
</div>
</div>
</div>
<!-- Resources Panel -->
<div
class="monitor-panel resources-panel"
hx-get="/api/monitoring/resources"
hx-trigger="load, every 15s"
hx-swap="innerHTML"
>
<div class="panel-header">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
class="panel-icon"
>
<rect
x="4"
y="4"
width="16"
height="16"
rx="2"
stroke="currentColor"
stroke-width="1.5"
/>
<line
x1="4"
y1="12"
x2="20"
y2="12"
stroke="currentColor"
stroke-width="1"
/>
<line
x1="12"
y1="4"
x2="12"
y2="20"
stroke="currentColor"
stroke-width="1"
/>
</svg>
<span>Resources</span>
</div>
<div class="tree-view">
<div class="tree-node">
<span class="tree-branch"></span>
<span class="tree-label">Loading...</span>
</div>
</div>
</div>
<!-- Services Panel -->
<div
class="monitor-panel services-panel"
hx-get="/api/monitoring/services"
hx-trigger="load, every 30s"
hx-swap="innerHTML"
>
<div class="panel-header">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
class="panel-icon"
>
<rect
x="2"
y="3"
width="20"
height="4"
rx="1"
stroke="currentColor"
stroke-width="1.5"
/>
<rect
x="2"
y="10"
width="20"
height="4"
rx="1"
stroke="currentColor"
stroke-width="1.5"
/>
<rect
x="2"
y="17"
width="20"
height="4"
rx="1"
stroke="currentColor"
stroke-width="1.5"
/>
</svg>
<span>Services</span>
</div>
<div class="tree-view">
<div class="tree-node">
<span class="tree-branch"></span>
<span class="tree-label">Loading...</span>
</div>
</div>
</div>
<!-- Bots Panel -->
<div
class="monitor-panel bots-panel"
hx-get="/api/monitoring/bots"
hx-trigger="load, every 30s"
hx-swap="innerHTML"
>
<div class="panel-header">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
class="panel-icon"
>
<rect
x="4"
y="4"
width="16"
height="14"
rx="3"
stroke="currentColor"
stroke-width="1.5"
/>
<circle cx="9" cy="10" r="1.5" fill="currentColor" />
<circle cx="15" cy="10" r="1.5" fill="currentColor" />
<path
d="M9 14 Q12 16 15 14"
stroke="currentColor"
stroke-width="1.5"
fill="none"
/>
</svg>
<span>Active Bots</span>
</div>
<div class="tree-view">
<div class="tree-node">
<span class="tree-branch"></span>
<span class="tree-label">Loading...</span>
</div>
</div>
</div>
</div>
<style>
.monitoring-container {
padding: 1.5rem;
max-width: 1400px;
margin: 0 auto;
}
.monitoring-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-color, #e5e7eb);
}
.monitoring-header h2 {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.5rem;
font-weight: 600;
color: var(--text-primary, #1f2937);
margin: 0;
}
.header-icon {
color: var(--primary-color, #3b82f6);
}
.last-updated {
font-size: 0.875rem;
color: var(--text-secondary, #6b7280);
}
.monitoring-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
}
.monitor-panel {
background: var(--card-bg, #ffffff);
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 0.75rem;
padding: 1.25rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.panel-header {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 1rem;
color: var(--text-primary, #1f2937);
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border-color, #e5e7eb);
}
.panel-icon {
color: var(--primary-color, #3b82f6);
}
.tree-view {
font-family:
system-ui,
-apple-system,
sans-serif;
font-size: 0.875rem;
}
.tree-node {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0;
position: relative;
}
.tree-branch,
.tree-branch-last {
width: 1.25rem;
height: 1.25rem;
position: relative;
flex-shrink: 0;
}
.tree-branch::before {
content: "";
position: absolute;
left: 0.5rem;
top: 0;
width: 1px;
height: 100%;
background: var(--primary-color, #3b82f6);
opacity: 0.4;
}
.tree-branch::after,
.tree-branch-last::after {
content: "";
position: absolute;
left: 0.5rem;
top: 50%;
width: 0.5rem;
height: 1px;
background: var(--primary-color, #3b82f6);
opacity: 0.6;
}
.tree-branch-last::before {
content: "";
position: absolute;
left: 0.5rem;
top: 0;
width: 1px;
height: 50%;
background: var(--primary-color, #3b82f6);
opacity: 0.4;
}
.tree-label {
color: var(--text-secondary, #6b7280);
flex: 1;
}
.tree-value {
font-weight: 600;
color: var(--text-primary, #1f2937);
}
.tree-status {
font-size: 0.75rem;
color: var(--text-secondary, #6b7280);
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-indicator.running {
background: #10b981;
box-shadow: 0 0 4px #10b981;
}
.status-indicator.warning {
background: #f59e0b;
box-shadow: 0 0 4px #f59e0b;
}
.status-indicator.stopped {
background: #ef4444;
}
.resource-node {
flex-wrap: wrap;
}
.progress-bar {
flex: 1;
height: 8px;
background: var(--border-color, #e5e7eb);
border-radius: 4px;
overflow: hidden;
min-width: 100px;
max-width: 150px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #60a5fa);
border-radius: 4px;
transition: width 0.5s ease;
}
.progress-fill.gpu {
background: linear-gradient(90deg, #8b5cf6, #a78bfa);
}
.progress-fill.disk {
background: linear-gradient(90deg, #10b981, #34d399);
}
.progress-fill.warning {
background: linear-gradient(90deg, #f59e0b, #fbbf24);
}
.progress-fill.critical {
background: linear-gradient(90deg, #ef4444, #f87171);
}
@media (prefers-color-scheme: dark) {
.monitoring-container {
--text-primary: #f9fafb;
--text-secondary: #9ca3af;
--card-bg: #1f2937;
--border-color: #374151;
}
}
@media (max-width: 768px) {
.monitoring-grid {
grid-template-columns: 1fr;
}
.monitoring-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
}
</style>
</div>