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.
36
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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> <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>
|
||||
|
|
@ -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)
|
||||
139
docs/src/appendix-env-vars/README.md
Normal 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
|
||||
94
docs/src/appendix-external-services/README.md
Normal 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
|
||||
273
docs/src/appendix-external-services/catalog.md
Normal 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
|
||||
```
|
||||
1
docs/src/appendix-external-services/channels.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Channel Integrations
|
||||
1
docs/src/appendix-external-services/directory.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Directory Services
|
||||
1
docs/src/appendix-external-services/llm-providers.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# LLM Providers
|
||||
1
docs/src/appendix-external-services/storage.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Storage Services
|
||||
1
docs/src/appendix-external-services/weather.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Weather API
|
||||
143
docs/src/assets/directory-tree.svg
Normal 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 |
16
docs/src/assets/icons/gb-bot.svg
Normal 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 |
27
docs/src/assets/icons/gb-chart.svg
Normal 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 |
21
docs/src/assets/icons/gb-check.svg
Normal 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 |
29
docs/src/assets/icons/gb-database.svg
Normal 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 |
20
docs/src/assets/icons/gb-folder.svg
Normal 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 |
4
docs/src/assets/icons/gb-gear.svg
Normal 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 |
32
docs/src/assets/icons/gb-globe.svg
Normal 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 |
20
docs/src/assets/icons/gb-lightbulb.svg
Normal 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 |
43
docs/src/assets/icons/gb-lock.svg
Normal 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 |
16
docs/src/assets/icons/gb-note.svg
Normal 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 |
13
docs/src/assets/icons/gb-package.svg
Normal 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 |
32
docs/src/assets/icons/gb-palette.svg
Normal 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 |
31
docs/src/assets/icons/gb-rocket.svg
Normal 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 |
18
docs/src/assets/icons/gb-search.svg
Normal 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 |
33
docs/src/assets/icons/gb-signal.svg
Normal 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 |
16
docs/src/assets/icons/gb-target.svg
Normal 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 |
42
docs/src/assets/icons/gb-tree.svg
Normal 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 |
25
docs/src/assets/icons/gb-warning.svg
Normal 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 |
200
docs/src/chapter-04-gbui/monitoring.md
Normal 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
|
||||
248
docs/src/chapter-04-gbui/player.md
Normal 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
|
||||
150
docs/src/chapter-06-gbdialog/keyword-instr.md
Normal 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
|
||||
214
docs/src/chapter-06-gbdialog/keyword-is-numeric.md
Normal 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
|
||||
249
docs/src/chapter-06-gbdialog/keyword-switch.md
Normal 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
|
||||
219
docs/src/chapter-06-gbdialog/keyword-webhook.md
Normal 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
|
||||
|
|
@ -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
|
||||
1
docs/src/chapter-08-config/drive.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Drive Integration
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
272
docs/src/chapter-12-auth/permissions-matrix.md
Normal 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
|
||||
287
docs/src/chapter-12-auth/user-system-context.md
Normal 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
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
1012
src/basic/keywords/data_operations.rs
Normal file
1531
src/basic/keywords/file_operations.rs
Normal file
801
src/basic/keywords/http_operations.rs
Normal 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(¶ms);
|
||||
|
||||
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, ¶ms);
|
||||
|
||||
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", ¶ms);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
370
src/basic/keywords/string_functions.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
296
src/basic/keywords/switch_case.rs
Normal 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("||"));
|
||||
}
|
||||
}
|
||||
500
src/basic/keywords/webhook.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
301
templates/office.gbai/office.gbdialog/api-integration.bas
Normal 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!"
|
||||
195
templates/office.gbai/office.gbdialog/data-sync.bas
Normal 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!"
|
||||
329
templates/office.gbai/office.gbdialog/document-processor.bas
Normal 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!"
|
||||
104
templates/office.gbai/office.gbdialog/start.bas
Normal 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
|
||||
14
templates/office.gbai/office.gbot/config.csv
Normal 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
|
||||
|
509
ui/suite/monitoring/monitoring.html
Normal 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>
|
||||