feat: Major workspace reorganization and documentation update

- Add comprehensive documentation in botbook/ with 12 chapters
- Add botapp/ Tauri desktop application
- Add botdevice/ IoT device support
- Add botlib/ shared library crate
- Add botmodels/ Python ML models service
- Add botplugin/ browser extension
- Add botserver/ reorganized server code
- Add bottemplates/ bot templates
- Add bottest/ integration tests
- Add botui/ web UI server
- Add CI/CD workflows in .forgejo/workflows/
- Add AGENTS.md and PROD.md documentation
- Add dependency management scripts (DEPENDENCIES.sh/ps1)
- Remove legacy src/ structure and migrations
- Clean up temporary and backup files
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-04-19 08:14:25 -03:00
parent fc322618f3
commit 037db5c381
2761 changed files with 1854582 additions and 8685 deletions

View file

@ -1,5 +1,6 @@
[build]
jobs = 8
# rustc-wrapper = "sccache"
[target.x86_64-unknown-linux-gnu]
linker = "clang"
# rustflags = ["-C", "link-arg=-fuse-ld=mold"]

View file

@ -1,36 +1,8 @@
# BotServer Environment Configuration
# =====================================
#
# ONLY VAULT VARIABLES ARE ALLOWED IN THIS FILE!
# All secrets (DATABASE_URL, API keys, etc.) MUST be stored in Vault.
# NO LEGACY FALLBACK - Vault is mandatory.
#
# Vault paths for secrets:
# - gbo/tables - PostgreSQL credentials (host, port, database, username, password)
# - gbo/drive - MinIO/S3 credentials (accesskey, secret)
# - gbo/cache - Redis credentials (password)
# - gbo/directory - Zitadel credentials (url, project_id, client_id, client_secret)
# - gbo/email - Email credentials (username, password)
# - gbo/llm - LLM API keys (openai_key, anthropic_key, groq_key)
# - gbo/encryption - Encryption keys (master_key)
# - gbo/meet - LiveKit credentials (api_key, api_secret)
# - gbo/alm - Forgejo credentials (url, admin_password, runner_token)
# - gbo/vectordb - Qdrant credentials (url, api_key)
# - gbo/observability - InfluxDB credentials (url, org, bucket, token)
# General Bots Environment Configuration
# Copy this file to .env and fill in values
# NEVER commit .env to version control
# =====================
# VAULT CONFIGURATION - ONLY THESE VARS ARE ALLOWED
# =====================
# Vault server address
VAULT_ADDR=https://localhost:8200
# Vault authentication token (generated during vault init)
# This will be populated automatically after first bootstrap
VAULT_TOKEN=
# Skip TLS verification for development (set to false in production)
VAULT_SKIP_VERIFY=true
# Cache TTL for secrets in seconds (default: 300 = 5 minutes)
VAULT_CACHE_TTL=300
# Vault connection
VAULT_ADDR=https://127.0.0.1:8200
VAULT_TOKEN=<your-vault-token-here>
VAULT_CACERT=./botserver-stack/vault/certs/ca.crt

View file

@ -0,0 +1,16 @@
name: Botapp CI
on:
push:
branches: [main]
paths:
- 'botapp/**'
jobs:
build:
runs-on: gbo
steps:
- name: Build
run: |
cd /opt/gbo/work/botapp
git reset --hard HEAD && git clean -fd
git pull
cargo build

View file

@ -0,0 +1,16 @@
name: Botbook CI
on:
push:
branches: [main]
paths:
- 'botbook/**'
jobs:
build:
runs-on: gbo
steps:
- name: Build
run: |
cd /opt/gbo/work/botbook
git reset --hard HEAD && git clean -fd
git pull
cargo build

View file

@ -0,0 +1,16 @@
name: Botdevice CI
on:
push:
branches: [main]
paths:
- 'botdevice/**'
jobs:
build:
runs-on: gbo
steps:
- name: Build
run: |
cd /opt/gbo/work/botdevice
git reset --hard HEAD && git clean -fd
git pull
cargo build

View file

@ -0,0 +1,16 @@
name: Botmodels CI
on:
push:
branches: [main]
paths:
- 'botmodels/**'
jobs:
build:
runs-on: gbo
steps:
- name: Build
run: |
cd /opt/gbo/work/botmodels
git reset --hard HEAD && git clean -fd
git pull
cargo build

View file

@ -0,0 +1,16 @@
name: Botplugin CI
on:
push:
branches: [main]
paths:
- 'botplugin/**'
jobs:
build:
runs-on: gbo
steps:
- name: Build
run: |
cd /opt/gbo/work/botplugin
git reset --hard HEAD && git clean -fd
git pull
cargo build

View file

@ -0,0 +1,29 @@
name: BotServer CI
on:
push:
branches: [main]
paths:
- 'botserver/**'
- 'botlib/**'
- 'Cargo.lock'
- '.forgejo/workflows/botserver.yaml'
jobs:
build:
runs-on: gbo
steps:
- name: Build
run: |
cd /opt/gbo/work/botserver
git reset --hard HEAD && git clean -fd
git pull
git submodule update --init --recursive botlib botserver
cargo build -p botserver
- name: Deploy
run: |
sudo incus exec system -- systemctl stop botserver || true
sudo incus exec system -- pkill -x botserver || true
sleep 1
sudo incus file push /opt/gbo/work/botserver/target/debug/botserver system:/opt/gbo/bin/botserver --mode=0755
sudo incus exec system -- systemctl start botserver
sleep 2
sudo incus exec system -- pgrep -x botserver && echo "✅ BotServer Deployed" || echo "❌ Failed"

View file

@ -0,0 +1,16 @@
name: Bottest CI
on:
push:
branches: [main]
paths:
- 'bottest/**'
jobs:
build:
runs-on: gbo
steps:
- name: Build
run: |
cd /opt/gbo/work/bottest
git reset --hard HEAD && git clean -fd
git pull
cargo build

View file

@ -0,0 +1,20 @@
name: BotUI CI
on:
push:
branches: [main]
paths:
- 'botui/**'
- 'botlib/**'
jobs:
build:
runs-on: gbo
steps:
- name: Build
run: |
cd /opt/gbo/work/botui
git reset --hard HEAD && git clean -fd
git pull
cargo build
- name: Deploy
run: |
echo "BotUI deployed"

8
.github/.gitattributes vendored Normal file
View file

@ -0,0 +1,8 @@
# .github repository - Only documentation and configuration
# Exclude from language statistics
*.md linguist-documentation
profile/README.md linguist-documentation
# Mark as not vendored
* !linguist-vendored

117
.github/profile/README.md vendored Normal file
View file

@ -0,0 +1,117 @@
<a href="https://github.com/generalbots/gb/graphs/contributors">
<img src="https://contrib.rocks/image?repo=generalbots/botserver" />
</a>
![General Bots Logo](https://github.com/GeneralBots/botserver/blob/main/logo.png?raw=true)
# General Bots
**Enterprise-Grade LLM Orchestrator and AI Automation Platform**
A strongly-typed, self-hosted conversational platform focused on convention over configuration and code-less approaches.
---
## Architecture
![General Bots Architecture](architecture.svg)
---
## Platform Data Flow
![General Bots Platform](platform.svg)
---
## Repository Structure
The **[gb](https://github.com/GeneralBots/gb)** repository is the root monorepo containing all components as Git submodules:
```
gb/ ← Root repository (clone this!)
├── Cargo.toml ← Workspace configuration
├── README.md ← Project overview
├── PROMPT.md ← Development guide
├── .gitmodules ← Submodule definitions
├── botserver/ ← [submodule] Core API server
├── botui/ ← [submodule] Pure web UI
├── botapp/ ← [submodule] Tauri desktop app
├── botlib/ ← [submodule] Shared Rust library
├── botbook/ ← [submodule] Documentation
├── bottest/ ← [submodule] Integration tests
├── botdevice/ ← [submodule] Device integration
├── botmodels/ ← [submodule] AI models
├── botplugin/ ← [submodule] Plugin system
├── bottemplates/ ← [submodule] Templates
└── .github/ ← [submodule] GitHub org config
```
---
## Quick Start
### Clone Everything
```bash
git clone --recursive https://github.com/GeneralBots/gb.git
cd gb
cargo build
```
### Update All Submodules
```bash
git submodule update --init --recursive
git submodule foreach git pull origin master
```
---
## Components
| Component | Description | Status |
|-----------|-------------|--------|
| [**gb**](https://github.com/GeneralBots/gb) | Root monorepo - workspace config, submodules | Production |
| [**botserver**](https://github.com/GeneralBots/botserver) | Core API server - LLM orchestration, automation | Production |
| [**botui**](https://github.com/GeneralBots/botui) | Pure web UI - HTMX-based minimal interface | Production |
| [**botapp**](https://github.com/GeneralBots/botapp) | Tauri desktop wrapper - native file access | Production |
| [**botlib**](https://github.com/GeneralBots/botlib) | Shared Rust library - common types, utilities | Production |
| [**botbook**](https://github.com/GeneralBots/botbook) | Documentation - mdBook format | Production |
| [**bottest**](https://github.com/GeneralBots/bottest) | Integration tests | Production |
| [**botdevice**](https://github.com/GeneralBots/botdevice) | Device integration | Production |
| [**botmodels**](https://github.com/GeneralBots/botmodels) | AI models | Production |
| [**botplugin**](https://github.com/GeneralBots/botplugin) | Plugin system | Production |
| [**bottemplates**](https://github.com/GeneralBots/bottemplates) | Templates - bots, apps, prompts | Production |
---
## Key Features
| Feature | Description |
|---------|-------------|
| Multi-Vendor LLM | Unified API for OpenAI, Groq, Claude, Anthropic |
| MCP and Tools | Instant tool creation from code and functions |
| Semantic Cache | 70% cost reduction on LLM calls |
| Web Automation | Browser automation with AI intelligence |
| Enterprise Connectors | CRM, ERP, database integrations |
| Version Control | Git-like history with rollback |
---
## Documentation
- [Complete Docs](https://github.com/GeneralBots/botbook)
- [Quick Start](https://github.com/GeneralBots/botserver/blob/main/docs/QUICK_START.md)
- [API Reference](https://github.com/GeneralBots/botserver/blob/main/docs/src/chapter-10-api/README.md)
---
## License
**AGPL-3.0** - True open source with dual licensing option.
---
Code Name: Guaribas

174
.github/profile/architecture.svg vendored Normal file
View file

@ -0,0 +1,174 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 600">
<defs>
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0d1117"/>
<stop offset="100%" style="stop-color:#161b22"/>
</linearGradient>
<linearGradient id="rootGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#238636"/>
<stop offset="100%" style="stop-color:#1a7f37"/>
</linearGradient>
<linearGradient id="coreGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#1f6feb"/>
<stop offset="100%" style="stop-color:#1958c7"/>
</linearGradient>
<linearGradient id="uiGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#a371f7"/>
<stop offset="100%" style="stop-color:#8957e5"/>
</linearGradient>
<linearGradient id="libGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#f78166"/>
<stop offset="100%" style="stop-color:#da6d53"/>
</linearGradient>
<linearGradient id="toolGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#7ee787"/>
<stop offset="100%" style="stop-color:#56d364"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="2" dy="2" stdDeviation="3" flood-opacity="0.3"/>
</filter>
</defs>
<!-- Background -->
<rect width="900" height="600" fill="url(#bgGrad)"/>
<!-- Title -->
<text x="450" y="45" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="28" font-weight="bold" fill="#ffffff">General Bots Architecture</text>
<text x="450" y="70" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#8b949e">Monorepo with Git Submodules</text>
<!-- Root Repository (gb) -->
<g filter="url(#shadow)">
<rect x="350" y="95" width="200" height="60" rx="8" fill="url(#rootGrad)"/>
<text x="450" y="125" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="#ffffff">gb</text>
<text x="450" y="143" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="11" fill="#d1fae5">Root Repository</text>
</g>
<!-- Connection lines from root -->
<g stroke="#30363d" stroke-width="2">
<!-- To botserver -->
<path d="M 450 155 L 450 180 L 150 180 L 150 210" fill="none"/>
<!-- To botui -->
<path d="M 450 155 L 450 180 L 350 180 L 350 210" fill="none"/>
<!-- To botapp -->
<path d="M 450 155 L 450 180 L 550 180 L 550 210" fill="none"/>
<!-- To botlib -->
<path d="M 450 155 L 450 180 L 750 180 L 750 210" fill="none"/>
</g>
<!-- Core Layer -->
<g filter="url(#shadow)">
<rect x="50" y="210" width="200" height="80" rx="8" fill="url(#coreGrad)"/>
<text x="150" y="245" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#ffffff">botserver</text>
<text x="150" y="265" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="11" fill="#a5d6ff">Core API Server</text>
<text x="150" y="280" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#79c0ff">LLM • Automation • API</text>
</g>
<!-- UI Layer -->
<g filter="url(#shadow)">
<rect x="275" y="210" width="150" height="80" rx="8" fill="url(#uiGrad)"/>
<text x="350" y="245" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#ffffff">botui</text>
<text x="350" y="265" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="11" fill="#e2d5f7">Pure Web UI</text>
<text x="350" y="280" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#d2a8ff">HTMX • HTML • CSS</text>
</g>
<g filter="url(#shadow)">
<rect x="475" y="210" width="150" height="80" rx="8" fill="url(#uiGrad)"/>
<text x="550" y="245" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#ffffff">botapp</text>
<text x="550" y="265" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="11" fill="#e2d5f7">Desktop App</text>
<text x="550" y="280" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#d2a8ff">Tauri • Native</text>
</g>
<!-- Shared Library -->
<g filter="url(#shadow)">
<rect x="650" y="210" width="200" height="80" rx="8" fill="url(#libGrad)"/>
<text x="750" y="245" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#ffffff">botlib</text>
<text x="750" y="265" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="11" fill="#ffddd2">Shared Library</text>
<text x="750" y="280" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#ffa28b">Types • Utils • HTTP</text>
</g>
<!-- Connection lines to second row -->
<g stroke="#30363d" stroke-width="2">
<path d="M 150 290 L 150 330" fill="none"/>
<path d="M 350 290 L 350 320 L 150 320 L 150 330" fill="none" stroke-dasharray="5,3"/>
<path d="M 550 290 L 550 320 L 350 320" fill="none" stroke-dasharray="5,3"/>
<path d="M 750 290 L 750 320 L 550 320" fill="none" stroke-dasharray="5,3"/>
</g>
<!-- Second row - Extensions -->
<g filter="url(#shadow)">
<rect x="50" y="330" width="140" height="65" rx="6" fill="url(#toolGrad)"/>
<text x="120" y="360" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="bold" fill="#0d1117">botmodels</text>
<text x="120" y="378" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#1a4721">AI Models</text>
</g>
<g filter="url(#shadow)">
<rect x="210" y="330" width="140" height="65" rx="6" fill="url(#toolGrad)"/>
<text x="280" y="360" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="bold" fill="#0d1117">botplugin</text>
<text x="280" y="378" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#1a4721">Plugin System</text>
</g>
<g filter="url(#shadow)">
<rect x="370" y="330" width="140" height="65" rx="6" fill="url(#toolGrad)"/>
<text x="440" y="360" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="bold" fill="#0d1117">botdevice</text>
<text x="440" y="378" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#1a4721">Device Integration</text>
</g>
<g filter="url(#shadow)">
<rect x="530" y="330" width="140" height="65" rx="6" fill="url(#toolGrad)"/>
<text x="600" y="360" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="bold" fill="#0d1117">bottemplates</text>
<text x="600" y="378" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#1a4721">Templates</text>
</g>
<!-- Third row - Support -->
<g filter="url(#shadow)">
<rect x="210" y="420" width="140" height="65" rx="6" fill="#484f58"/>
<text x="280" y="450" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="bold" fill="#ffffff">botbook</text>
<text x="280" y="468" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#8b949e">Documentation</text>
</g>
<g filter="url(#shadow)">
<rect x="370" y="420" width="140" height="65" rx="6" fill="#484f58"/>
<text x="440" y="450" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="bold" fill="#ffffff">bottest</text>
<text x="440" y="468" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#8b949e">Integration Tests</text>
</g>
<g filter="url(#shadow)">
<rect x="530" y="420" width="140" height="65" rx="6" fill="#484f58"/>
<text x="600" y="450" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="bold" fill="#ffffff">.github</text>
<text x="600" y="468" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#8b949e">GitHub Config</text>
</g>
<!-- Connection lines to third row -->
<g stroke="#30363d" stroke-width="1.5" stroke-dasharray="3,3">
<line x1="280" y1="395" x2="280" y2="420"/>
<line x1="440" y1="395" x2="440" y2="420"/>
<line x1="600" y1="395" x2="600" y2="420"/>
</g>
<!-- Legend -->
<g transform="translate(690, 340)">
<text x="0" y="0" font-family="system-ui, -apple-system, sans-serif" font-size="12" font-weight="bold" fill="#8b949e">LEGEND</text>
<rect x="0" y="15" width="16" height="12" rx="2" fill="url(#rootGrad)"/>
<text x="22" y="25" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#8b949e">Root Repo</text>
<rect x="0" y="35" width="16" height="12" rx="2" fill="url(#coreGrad)"/>
<text x="22" y="45" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#8b949e">Core Server</text>
<rect x="0" y="55" width="16" height="12" rx="2" fill="url(#uiGrad)"/>
<text x="22" y="65" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#8b949e">User Interface</text>
<rect x="0" y="75" width="16" height="12" rx="2" fill="url(#libGrad)"/>
<text x="22" y="85" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#8b949e">Shared Lib</text>
<rect x="0" y="95" width="16" height="12" rx="2" fill="url(#toolGrad)"/>
<text x="22" y="105" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#8b949e">Extensions</text>
<rect x="0" y="115" width="16" height="12" rx="2" fill="#484f58"/>
<text x="22" y="125" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#8b949e">Support</text>
</g>
<!-- Footer -->
<text x="450" y="530" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#484f58">git clone --recursive https://github.com/GeneralBots/gb.git</text>
<text x="450" y="555" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="11" fill="#30363d">Code Name: Guaribas • AGPL-3.0</text>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

117
.github/profile/platform.svg vendored Normal file
View file

@ -0,0 +1,117 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450">
<defs>
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0d1117"/>
<stop offset="100%" style="stop-color:#161b22"/>
</linearGradient>
<linearGradient id="appGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#a371f7"/>
<stop offset="100%" style="stop-color:#8957e5"/>
</linearGradient>
<linearGradient id="uiGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#79c0ff"/>
<stop offset="100%" style="stop-color:#58a6ff"/>
</linearGradient>
<linearGradient id="serverGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#238636"/>
<stop offset="100%" style="stop-color:#1a7f37"/>
</linearGradient>
<linearGradient id="libGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#f78166"/>
<stop offset="100%" style="stop-color:#da6d53"/>
</linearGradient>
<linearGradient id="docsGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#7ee787"/>
<stop offset="100%" style="stop-color:#56d364"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="2" dy="3" stdDeviation="4" flood-opacity="0.4"/>
</filter>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#58a6ff"/>
</marker>
</defs>
<!-- Background -->
<rect width="800" height="450" fill="url(#bgGrad)"/>
<!-- Title -->
<text x="400" y="40" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="24" font-weight="bold" fill="#ffffff">General Bots Platform</text>
<text x="400" y="62" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#8b949e">Data Flow Architecture</text>
<!-- Top Layer - Client Apps -->
<!-- botapp -->
<g filter="url(#shadow)">
<rect x="60" y="100" width="160" height="90" rx="10" fill="url(#appGrad)"/>
<text x="140" y="135" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="#ffffff">botapp</text>
<text x="140" y="158" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#e2d5f7">Tauri Desktop</text>
<text x="140" y="178" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#d2a8ff">Native • System Tray</text>
</g>
<!-- botui -->
<g filter="url(#shadow)">
<rect x="260" y="100" width="160" height="90" rx="10" fill="url(#uiGrad)"/>
<text x="340" y="135" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="#ffffff">botui</text>
<text x="340" y="158" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#cae8ff">Pure Web UI</text>
<text x="340" y="178" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#a5d6ff">HTMX • HTML • CSS</text>
</g>
<!-- Arrow from botapp to botui -->
<path d="M 220 145 L 250 145" stroke="#58a6ff" stroke-width="2" fill="none" marker-end="url(#arrowhead)"/>
<text x="235" y="138" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="8" fill="#8b949e">wraps</text>
<!-- Arrow from botui to botserver -->
<path d="M 420 145 L 450 145" stroke="#58a6ff" stroke-width="2" fill="none" marker-end="url(#arrowhead)"/>
<text x="435" y="138" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="8" fill="#8b949e">HTTP</text>
<!-- botserver -->
<g filter="url(#shadow)">
<rect x="460" y="100" width="180" height="90" rx="10" fill="url(#serverGrad)"/>
<text x="550" y="135" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="#ffffff">botserver</text>
<text x="550" y="158" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#d1fae5">Core API Server</text>
<text x="550" y="178" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#7ee787">Rust • LLM • Automation</text>
</g>
<!-- Middle Layer Arrow -->
<path d="M 550 190 L 550 230" stroke="#58a6ff" stroke-width="2" fill="none" marker-end="url(#arrowhead)"/>
<!-- Connection hub -->
<circle cx="550" cy="260" r="25" fill="#21262d" stroke="#30363d" stroke-width="2"/>
<text x="550" y="265" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#8b949e">uses</text>
<!-- Arrows to bottom layer -->
<path d="M 525 275 L 390 320" stroke="#58a6ff" stroke-width="2" fill="none" marker-end="url(#arrowhead)"/>
<path d="M 575 275 L 650 320" stroke="#58a6ff" stroke-width="2" fill="none" marker-end="url(#arrowhead)"/>
<!-- Bottom Layer -->
<!-- botlib -->
<g filter="url(#shadow)">
<rect x="260" y="320" width="180" height="90" rx="10" fill="url(#libGrad)"/>
<text x="350" y="355" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="#ffffff">botlib</text>
<text x="350" y="378" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#ffddd2">Shared Library</text>
<text x="350" y="398" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#ffa28b">Types • HTTP • Utils</text>
</g>
<!-- botbook -->
<g filter="url(#shadow)">
<rect x="500" y="320" width="180" height="90" rx="10" fill="url(#docsGrad)"/>
<text x="590" y="355" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="#0d1117">botbook</text>
<text x="590" y="378" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#1a4721">Documentation</text>
<text x="590" y="398" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#196c2e">mdBook • Guides</text>
</g>
<!-- External Services Box -->
<g>
<rect x="680" y="100" width="100" height="90" rx="8" fill="none" stroke="#30363d" stroke-width="2" stroke-dasharray="5,3"/>
<text x="730" y="130" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#8b949e">External</text>
<text x="730" y="148" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="9" fill="#6e7681">OpenAI</text>
<text x="730" y="162" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="9" fill="#6e7681">Anthropic</text>
<text x="730" y="176" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="9" fill="#6e7681">Groq</text>
</g>
<!-- Arrow to External -->
<path d="M 640 145 L 670 145" stroke="#30363d" stroke-width="1.5" fill="none" marker-end="url(#arrowhead)" stroke-dasharray="4,2"/>
<!-- Footer -->
<text x="400" y="435" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="11" fill="#30363d">Code Name: Guaribas • AGPL-3.0</text>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

103
.gitignore vendored
View file

@ -1,17 +1,94 @@
.tmp*
.tmp/*
*.log
target*
.env
*.env
work
# Build artifacts
target/
*.out
bin
botserver-stack
bin/
*.png
*.jpg
# Logs
*.log
*logfile*
*-log*
docs/book
.vscode
.zed
.gemini
.claude
# Temporary files
.tmp*
.tmp/*
.tmp
# Environment files
.env
*.env
# Work directories
work/
# Redis
*.rdb
botserver-installers
.git-rewrite
vault-unseal-keys
# Documentation build
docs/book
.ruff_cache
.goutputstream*
# Installers (keep gitkeep)
botserver-installers/*
!botserver-installers/.gitkeep
botserver-stack
TODO*
work
.swp
# Lock file
# Cargo.lock (should be tracked)
.kiro
config
# Data directory (contains bot configs and API keys)
data/
# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
.playwright*
.ruff_cache
.opencode
config/directory_config.json
# CI cache bust: Fri Feb 13 22:33:51 UTC 2026
# Secrets - NEVER commit these files
vault-unseal-keys
start-and-unseal.sh
vault-token-*
init.json
*.pem
*.key
*.crt
*.cert
$null
AppData/
build_errors*.txt
build_errors_utf8.txt
check.json
clippy*.txt
errors.txt
errors_utf8.txt
vault-unseal-keysdefault-vault.tar
prompts/sec-bots.md
AGENTS-PROD.md
# Backup files
*.head
*.bad2
*.bad
*.check
*.fix
*.bak

1870
AGENTS.md Normal file

File diff suppressed because it is too large Load diff

11680
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,250 +1,269 @@
[package]
name = "botserver"
version = "6.3.1"
edition = "2021"
[workspace]
resolver = "2"
members = [
"botapp",
"botdevice",
"botlib",
"botserver",
"bottest",
"botui",
]
exclude = ["backup-to-s3"]
[dependencies.botlib]
workspace = true
features = ["database", "i18n"]
[workspace.lints.rust]
[features]
# ===== DEFAULT =====
default = ["chat", "automation", "cache", "llm", "vectordb", "crawler", "drive", "directory"]
[workspace.lints.clippy]
# ===== SECURITY MODES =====
# no-security: Minimal build - chat, automation, drive, cache only (no RBAC, directory, security, compliance)
# Build with: cargo build --no-default-features --features "no-security,chat,llm"
no-security = []
# ═══════════════════════════════════════════════════════════════════════════════
# WORKSPACE DEPENDENCIES - Single source of truth for all member crates
# Member crates reference these with: dependency = { workspace = true }
# ═══════════════════════════════════════════════════════════════════════════════
browser = ["automation", "drive", "cache"]
terminal = ["automation", "drive", "cache"]
external_sync = ["automation", "drive", "cache"]
[workspace.dependencies]
# ─── INTERNAL CRATES ───
botlib = { path = "botlib" }
# ===== CORE INFRASTRUCTURE (Can be used standalone) =====
scripting = ["dep:rhai"]
automation = ["scripting", "dep:cron"]
drive = ["dep:aws-config", "dep:aws-sdk-s3", "dep:aws-smithy-async", "dep:pdf-extract"]
cache = ["dep:redis"]
directory = ["rbac"]
rbac = []
crawler = ["drive", "cache"]
# ─── CORE / ERROR HANDLING ───
anyhow = "1.0"
thiserror = "2.0"
log = "0.4"
env_logger = "0.11"
color-eyre = "0.6.5"
# ===== APPS (Each includes what it needs from core) =====
# Communication
chat = ["automation", "cache"]
people = ["automation", "drive", "cache"]
mail = ["automation", "drive", "cache", "dep:lettre", "dep:mailparse", "dep:imap"]
meet = ["automation", "drive", "cache"]
social = ["automation", "drive", "cache"]
marketing = ["people", "automation", "drive", "cache"]
# ─── ASYNC RUNTIME ───
tokio = { version = "1.41", default-features = false }
tokio-stream = "0.1"
tokio-util = { version = "0.7", default-features = false }
tokio-rustls = "0.26"
tokio-test = "0.4"
tokio-tungstenite = { version = "0.24", default-features = false }
async-trait = "0.1"
async-lock = "2.8.0"
async-stream = "0.3"
futures = "0.3"
futures-util = { version = "0.3", default-features = false }
scopeguard = "1.2.0"
# Productivity
calendar = ["automation", "drive", "cache"]
tasks = ["automation", "drive", "cache", "dep:cron"]
project = ["automation", "drive", "cache", "quick-xml"]
goals = ["automation", "drive", "cache"]
workspaces = ["automation", "drive", "cache"]
tickets = ["automation", "drive", "cache"]
billing = ["automation", "drive", "cache"]
# ─── SERIALIZATION ───
serde = { version = "1.0", default-features = false }
serde_json = "1.0"
toml = "0.8"
ciborium = "0.2"
# Documents
docs = ["automation", "drive", "cache", "docx-rs", "ooxmlsdk"]
sheet = ["automation", "drive", "cache", "calamine", "dep:rust_xlsxwriter", "dep:umya-spreadsheet"]
slides = ["automation", "drive", "cache", "ooxmlsdk"]
paper = ["automation", "drive", "cache"]
# ─── DATE / TIME ───
chrono = { version = "0.4", default-features = false }
time = { version = "0.3", default-features = false }
cron = "0.15.0"
# Media
video = ["automation", "drive", "cache"]
player = ["automation", "drive", "cache"]
canvas = ["automation", "drive", "cache"]
# ─── UUID ───
uuid = { version = "1.11", default-features = false }
# Learning
learn = ["automation", "drive", "cache", "crawler"]
research = ["automation", "drive", "cache", "llm", "vectordb"]
sources = ["automation", "drive", "cache"]
# ─── WEB FRAMEWORK ───
axum = { version = "0.7.5", features = ["ws", "multipart", "macros"] }
axum-server = { version = "0.8", features = ["tls-rustls"] }
tower = "0.4"
tower-http = { version = "0.6", default-features = false }
tower-cookies = "0.10"
hyper = { version = "1.4", default-features = false }
hyper-rustls = { version = "0.27", default-features = false }
hyper-util = { version = "0.1.19", default-features = false }
http-body-util = "0.1.3"
# Analytics
analytics = ["automation", "drive", "cache"]
dashboards = ["automation", "drive", "cache"]
monitoring = ["automation", "drive", "cache", "dep:sysinfo"]
# ─── HTTP CLIENT ───
reqwest = { version = "0.12", default-features = false }
# Development
designer = ["automation", "drive", "cache"]
editor = ["automation", "drive", "cache"]
# ─── DATABASE ───
diesel = { version = "2.2" }
diesel_migrations = "2.2"
# Admin
attendant = ["automation", "drive", "cache"]
security = ["automation", "drive", "cache"]
settings = ["automation", "drive", "cache"]
# ─── CACHING ───
redis = { version = "0.27", default-features = false }
whatsapp = ["automation", "drive", "cache"]
telegram = ["automation", "drive", "cache"]
instagram = ["automation", "drive", "cache"]
msteams = ["automation", "drive", "cache"]
# Core Tech
llm = ["automation", "cache"]
vectordb = ["automation", "drive", "cache", "dep:qdrant-client"]
nvidia = ["automation", "drive", "cache"]
compliance = ["automation", "drive", "cache", "dep:csv"]
timeseries = ["automation", "drive", "cache"]
weba = ["automation", "drive", "cache"]
progress-bars = ["automation", "drive", "cache", "dep:indicatif"]
grpc = ["automation", "drive", "cache"]
jemalloc = ["automation", "drive", "cache", "dep:tikv-jemallocator", "dep:tikv-jemalloc-ctl"]
console = ["automation", "drive", "cache", "dep:crossterm", "dep:ratatui"]
# ─── SECURITY / CRYPTO ───
jsonwebtoken = "9.3"
base64 = "0.22"
aes-gcm = "0.10"
argon2 = "0.5"
hmac = "0.12.1"
sha2 = "0.10.9"
sha1 = "0.10.6"
hex = "0.4"
rand = "0.9.2"
ring = "0.17"
# ===== BUNDLES (Optional - for convenience) =====
minimal = ["chat"]
minimal-chat = ["chat", "automation", "cache"] # No security at all
lightweight = ["chat", "tasks", "people"]
full = ["chat", "people", "mail", "tasks", "calendar", "drive", "docs", "llm", "cache", "compliance"]
embed-ui = ["dep:rust-embed"]
# ─── TLS / CERTIFICATES ───
rustls = { version = "0.23", default-features = false }
rcgen = { version = "0.14", default-features = false }
x509-parser = "0.15"
rustls-native-certs = "0.8"
webpki-roots = "0.25"
native-tls = "0.2"
[dependencies]
diesel_migrations = { workspace = true }
bigdecimal = { workspace = true }
# ─── REGEX / TEXT ───
regex = "1.11"
smartstring = "1.0.1"
urlencoding = "2.1"
url = "2.5"
# === CORE RUNTIME ===
aes-gcm = { workspace = true }
anyhow = { workspace = true }
argon2 = { workspace = true }
async-trait = { workspace = true }
axum = { workspace = true }
axum-server = { workspace = true }
base64 = { workspace = true }
chrono = { workspace = true, features = ["clock", "std"] }
html2md = "0.2"
color-eyre = { workspace = true }
diesel = { workspace = true, features = ["postgres", "uuid", "chrono", "serde_json", "r2d2", "numeric", "32-column-tables"] }
dirs = { workspace = true }
dotenvy = { workspace = true }
futures = { workspace = true }
futures-util = { workspace = true }
git2 = "0.19"
hex = { workspace = true }
hmac = { workspace = true }
log = { workspace = true }
num-format = { workspace = true }
once_cell = { workspace = true }
rand = { workspace = true }
regex = { workspace = true }
reqwest = { workspace = true, features = ["rustls-tls", "multipart", "stream", "json"] }
serde = { workspace = true, features = ["derive", "std"] }
serde_json = { workspace = true }
toml = { workspace = true }
sha2 = { workspace = true }
sha1 = { workspace = true }
tokio = { workspace = true, features = ["full", "process"] }
tower-http = { workspace = true, features = ["cors", "fs", "trace"] }
tracing = { workspace = true }
url = { workspace = true }
urlencoding = { workspace = true }
uuid = { workspace = true, features = ["v4", "v5"] }
# ─── FILESYSTEM ───
dirs = "5.0"
tempfile = "3"
walkdir = "2.5.0"
notify = "8.0"
# === TLS/SECURITY DEPENDENCIES ===
rustls = { workspace = true, features = ["ring", "std", "tls12"] }
tokio-rustls = { workspace = true }
rcgen = { workspace = true, features = ["crypto", "ring", "pem"] }
x509-parser = { workspace = true }
ring = { workspace = true }
ciborium = { workspace = true }
time = { workspace = true, features = ["formatting"] }
jsonwebtoken = { workspace = true }
# ─── COMPRESSION / ARCHIVES ───
flate2 = "1.0"
zip = "2.2"
tar = "0.4"
# === APP-SPECIFIC DEPENDENCIES ===
# ─── DOCUMENT PROCESSING ───
docx-rs = "0.4"
ooxmlsdk = { version = "0.3", default-features = false }
calamine = "0.26"
spreadsheet-ods = "1.0"
pdf-extract = "0.10.0"
quick-xml = { version = "0.37", features = ["serialize"] }
rust_xlsxwriter = "0.77"
umya-spreadsheet = "2.1"
# mail Integration (mail feature)
imap = { workspace = true, optional = true }
lettre = { workspace = true, optional = true }
mailparse = { workspace = true, optional = true }
# ─── IMAGE / MEDIA ───
png = "0.18"
qrcode = { version = "0.14", default-features = false }
# Vector Database (vectordb feature)
qdrant-client = { workspace = true, optional = true }
# ─── EMAIL ───
imap = "3.0.0-alpha.15"
lettre = { version = "0.11", features = ["smtp-transport", "builder"] }
mailparse = "0.15"
# Document Processing
docx-rs = { workspace = true, optional = true }
ooxmlsdk = { workspace = true, optional = true, features = ["parts"] }
calamine = { workspace = true, optional = true }
rust_xlsxwriter = { workspace = true, optional = true }
umya-spreadsheet = { workspace = true, optional = true }
# ─── CONFIGURATION ───
dotenvy = "0.15"
figment = { version = "0.10", default-features = false }
once_cell = "1.18.0"
# File Storage & Drive (drive feature)
aws-config = { workspace = true, features = ["behavior-version-latest", "rt-tokio", "rustls"], optional = true }
aws-sdk-s3 = { workspace = true, features = ["rt-tokio", "rustls"], optional = true }
aws-smithy-async = { workspace = true, optional = true }
pdf-extract = { workspace = true, optional = true }
quick-xml = { workspace = true, optional = true }
flate2 = { workspace = true }
zip = { workspace = true }
tar = { workspace = true }
# ─── TRACING / LOGGING ───
tracing = "0.1"
tracing-subscriber = { version = "0.3", default-features = false }
# Task Management (tasks feature)
cron = { workspace = true, optional = true }
# ─── NUMERIC ───
num-format = "0.4"
bigdecimal = { version = "0.4", features = ["serde"] }
# Automation & Scripting (automation feature)
rhai = { workspace = true, optional = true }
# ─── UTILITIES ───
bytes = "1.8"
# Compliance & Reporting (compliance feature)
csv = { workspace = true, optional = true }
# ─── CLOUD / AWS ───
aws-config = { version = "1.8.8", default-features = false }
aws-sdk-s3 = { version = "1.120", default-features = false }
aws-smithy-async = { version = "1.2", features = ["rt-tokio"] }
# Console/TUI (console feature)
crossterm = { workspace = true, optional = true }
ratatui = { workspace = true, optional = true }
# ─── SCRIPTING ───
rhai = { version = "1.23", features = ["sync"] }
# QR Code Generation
png = { workspace = true }
qrcode = { workspace = true }
# ─── VECTOR DB ───
qdrant-client = "1.16"
# Error handling
thiserror = { workspace = true }
# ─── VIDEO / MEETINGS ───
livekit = "0.7"
# Caching/Sessions (cache feature)
redis = { workspace = true, features = ["tokio-comp"], optional = true }
# ─── SYSTEM MONITORING ───
sysinfo = "0.37.2"
# System Monitoring (monitoring feature)
sysinfo = { workspace = true, optional = true }
# ─── CONSOLE / TUI ───
crossterm = "0.29.0"
ratatui = "0.30"
indicatif = "0.18.0"
# UI Enhancement (progress-bars feature)
indicatif = { workspace = true, optional = true }
smartstring = { workspace = true }
# ─── MEMORY ALLOCATOR ───
tikv-jemallocator = "0.6"
tikv-jemalloc-ctl = { version = "0.6", default-features = false, features = ["stats"] }
# Memory allocator (jemalloc feature)
tikv-jemallocator = { workspace = true, optional = true }
tikv-jemalloc-ctl = { workspace = true, optional = true }
scopeguard = { workspace = true }
# ─── SECRETS / VAULT ───
vaultrs = "0.7"
# Vault secrets management
vaultrs = { workspace = true }
# ─── CALENDAR / RSS ───
icalendar = "0.17"
rss = "2.0"
# Calendar standards (RFC 5545)
icalendar = { workspace = true }
# ─── WEB SCRAPING ───
scraper = "0.25"
# Rate limiting
governor = { workspace = true }
# ─── RATE LIMITING ───
governor = "0.10"
# RSS feed parsing
rss = { workspace = true }
# ─── CSV ───
csv = "1.3"
# HTML parsing/web scraping
scraper = { workspace = true }
walkdir = { workspace = true }
# ─── GRPC ───
tonic = { version = "0.14.2", default-features = false }
# Embedded static files
rust-embed = { workspace = true, optional = true }
# ─── STATIC FILES ───
rust-embed = { version = "8.5", features = ["interpolate-folder-path"] }
mime_guess = "2.0"
# Blocking HTTP for Vault config
ureq = { version = "2", features = ["json"] }
# ─── TAURI (Desktop/Mobile) ───
tauri = { version = "2", features = ["unstable"] }
tauri-build = "2"
tauri-plugin-dialog = "2"
tauri-plugin-opener = "2"
tauri-plugin-notification = "2"
tauri-plugin-http = "2"
tauri-plugin-geolocation = "2"
[dev-dependencies]
mockito = { workspace = true }
tempfile = { workspace = true }
bigdecimal = { workspace = true }
# ─── TESTING ───
mockito = "1.7.0"
wiremock = "0.6"
cookie = "0.18"
chromiumoxide = { version = "0.7", features = ["tokio-runtime"], default-features = false }
pretty_assertions = "1.4"
insta = { version = "1.40", features = ["json", "yaml"] }
# Force libgit2-sys to use system libgit2
[patch.crates-io]
libgit2-sys = { git = "https://github.com/rust-lang/git2-rs", branch = "main" }
# ─── PROCESS CONTROL ───
nix = { version = "0.29", features = ["signal", "process"] }
which = "7"
[dependencies.git2]
version = "0.19"
features = ["vendored-libgit2"]
# ─── ANDROID ───
android_logger = "0.14"
# ─── VALIDATOR ───
validator = { version = "0.20", features = ["derive"] }
# ─── MISC ───
webbrowser = "0.8"
hostname = "0.4"
local-ip-address = "0.6.5"
image = "0.25"
libc = "0.2"
trayicon = "0.2"
# ═══════════════════════════════════════════════════════════════════════════════
# PROFILES
# ═══════════════════════════════════════════════════════════════════════════════
[profile.release]
lto = true
opt-level = "z"
strip = true
panic = "abort"
codegen-units = 1
[profile.ci]
inherits = "release"
lto = false
codegen-units = 8
debug = false
[profile.low-memory]
inherits = "release"
lto = "thin"
codegen-units = 16
debug = false
incremental = false
opt-level = "s"
[profile.dev]
debug = 1
incremental = true
codegen-units = 32
opt-level = 0

77
DEPENDENCIES.ps1 Normal file
View file

@ -0,0 +1,77 @@
<#
.SYNOPSIS
Installs runtime dependencies for General Bots on Windows.
.DESCRIPTION
This script downloads and configures the system libraries required to build
and run BotServer on Windows. It downloads PostgreSQL binaries (for libpq)
and sets the PQ_LIB_DIR environment variable permanently.
.EXAMPLE
PS> .\DEPENDENCIES.ps1
#>
$ErrorActionPreference = 'Stop'
# ─── COLORS ───
function Write-Step { param($msg) Write-Host " * $msg" -ForegroundColor Green }
function Write-Warn { param($msg) Write-Host " ! $msg" -ForegroundColor Yellow }
function Write-Err { param($msg) Write-Host " x $msg" -ForegroundColor Red }
Write-Host "========================================" -ForegroundColor Green
Write-Host " General Bots Runtime Dependencies" -ForegroundColor Green
Write-Host " (Windows)" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Write-Host ""
# ─── PostgreSQL binaries (libpq.lib for Diesel ORM) ───
$PgsqlDir = "C:\pgsql\pgsql"
$PgsqlLib = "$PgsqlDir\lib\libpq.lib"
$PgsqlZipUrl = "https://get.enterprisedb.com/postgresql/postgresql-17.4-1-windows-x64-binaries.zip"
$PgsqlZip = "$env:TEMP\pgsql.zip"
if (Test-Path $PgsqlLib) {
Write-Step "PostgreSQL binaries already present at $PgsqlDir"
} else {
Write-Host "`nDownloading PostgreSQL binaries..." -ForegroundColor Cyan
Write-Host " URL: $PgsqlZipUrl"
Write-Host " This may take a few minutes (~300MB)...`n"
Invoke-WebRequest -Uri $PgsqlZipUrl -OutFile $PgsqlZip -UseBasicParsing
Write-Host "Extracting to C:\pgsql ..."
if (Test-Path "C:\pgsql") { Remove-Item "C:\pgsql" -Recurse -Force }
Expand-Archive -Path $PgsqlZip -DestinationPath "C:\pgsql" -Force
Remove-Item $PgsqlZip -Force -ErrorAction SilentlyContinue
if (Test-Path $PgsqlLib) {
Write-Step "PostgreSQL binaries installed successfully."
} else {
Write-Err "Failed to find libpq.lib after extraction!"
exit 1
}
}
# Set PQ_LIB_DIR permanently for the current user
$CurrentPqDir = [System.Environment]::GetEnvironmentVariable("PQ_LIB_DIR", "User")
if ($CurrentPqDir -ne "$PgsqlDir\lib") {
[System.Environment]::SetEnvironmentVariable("PQ_LIB_DIR", "$PgsqlDir\lib", "User")
$env:PQ_LIB_DIR = "$PgsqlDir\lib"
Write-Step "PQ_LIB_DIR set to '$PgsqlDir\lib' (User environment variable)"
} else {
Write-Step "PQ_LIB_DIR already configured."
}
# ─── Summary ───
Write-Host ""
Write-Host "========================================" -ForegroundColor Green
Write-Host " Dependencies installed!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Write-Host ""
Write-Host "You can now build and run:" -ForegroundColor Cyan
Write-Host " cargo build -p botserver"
Write-Host " cargo build -p botui"
Write-Host " .\restart.ps1"
Write-Host ""
Write-Host "NOTE: If this is the first time, restart your terminal" -ForegroundColor Yellow
Write-Host " so PQ_LIB_DIR takes effect." -ForegroundColor Yellow

133
DEPENDENCIES.sh Normal file
View file

@ -0,0 +1,133 @@
#!/bin/bash
#
# DEPENDENCIES.sh - Runtime Dependencies for General Bots
#
# This script installs all system packages required to RUN botserver binary.
# These are the minimal dependencies needed for production deployment.
#
# Usage: sudo ./DEPENDENCIES.sh
#
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} General Bots Runtime Dependencies${NC}"
echo -e "${GREEN}========================================${NC}"
# Check root
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Error: Run as root (use sudo)${NC}"
exit 1
fi
# Detect OS
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$ID
else
echo -e "${RED}Error: Cannot detect OS${NC}"
exit 1
fi
echo -e "${YELLOW}OS: $OS${NC}"
install_debian_ubuntu() {
apt-get install -y \
libpq5 \
libssl3 \
liblzma5 \
zlib1g \
ca-certificates \
curl \
wget \
libclang1 \
pkg-config \
snapd
}
install_fedora_rhel() {
dnf install -y \
libpq \
openssl-libs \
xz-libs \
zlib \
ca-certificates \
curl \
wget \
abseil-cpp \
clang-libs \
pkgconf-pkg-config \
lxc \
lxc-templates
}
install_arch() {
pacman -Sy --noconfirm \
postgresql-libs \
openssl \
xz \
zlib \
ca-certificates \
curl \
wget \
abseil-cpp \
clang \
pkgconf \
lxc
}
install_alpine() {
apk add --no-cache \
libpq \
openssl \
xz-libs \
zlib \
ca-certificates \
curl \
wget \
abseil-cpp \
clang \
pkgconf \
lxc
}
case $OS in
ubuntu|debian|linuxmint|pop)
install_debian_ubuntu
;;
fedora|rhel|centos|rocky|almalinux)
install_fedora_rhel
;;
arch|manjaro)
install_arch
;;
alpine)
install_alpine
;;
*)
echo -e "${RED}Unsupported OS: $OS${NC}"
echo "Required libraries:"
echo " - libpq (PostgreSQL client)"
echo " - libssl (OpenSSL)"
echo " - liblzma (XZ compression)"
echo " - zlib (compression)"
echo " - abseil-cpp (Google Abseil)"
echo " - clang (LLVM runtime)"
echo " - LXC (containers)"
exit 1
;;
esac
echo -e "${GREEN}Runtime dependencies installed!${NC}"
echo ""
echo "You can now run:"
echo " ./botserver"

967
PROD.md Normal file
View file

@ -0,0 +1,967 @@
# Production Environment Guide
## CRITICAL RULES — READ FIRST
NEVER INCLUDE HERE CREDENTIALS OR COMPANY INFORMATION, THIS IS COMPANY AGNOSTIC.
Always manage services with `systemctl` inside the `system` Incus container. Never run `/opt/gbo/bin/botserver` or `/opt/gbo/bin/botui` directly — they will fail because they won't load the `.env` file containing Vault credentials and paths. The correct commands are `sudo incus exec system -- systemctl start|stop|restart|status botserver` and the same for `ui`. Systemctl handles environment loading, auto-restart, logging, and dependencies.
Never push secrets (API keys, passwords, tokens) to git. Never commit `init.json` (it contains Vault unseal keys). All secrets must come from Vault — only `VAULT_*` variables are allowed in `.env`. Never deploy manually via scp or ssh; always use CI/CD. Always push all submodules (botserver, botui, botlib) before or alongside the main repo. Always ask before pushing to ALM.
---
## Infrastructure Overview
The host machine is accessed via `ssh user@<hostname>`, running Incus (an LXD fork) as hypervisor. All services run inside named Incus containers. You enter containers with `sudo incus exec <container> -- <command>` and list them with `sudo incus list`.
### Container Architecture
| Container | Service | Technology | Binary Path | Logs Path | Data Path | Notes |
|-----------|---------|------------|-------------|-----------|-----------|-------|
| **system** | BotServer + BotUI | Rust/Axum | `/opt/gbo/bin/botserver`<br>`/opt/gbo/bin/botui` | `/opt/gbo/logs/out.log`<br>`/opt/gbo/logs/err.log` | `/opt/gbo/work/` | Main API + UI proxy |
| **tables** | PostgreSQL | PostgreSQL 15+ | `/usr/lib/postgresql/*/bin/postgres` | `/opt/gbo/logs/postgresql/` | `/opt/gbo/data/pgdata/` | Primary database |
| **vault** | HashiCorp Vault | Vault | `/opt/gbo/bin/vault` | `/opt/gbo/logs/vault/` | `/opt/gbo/data/vault/` | Secrets management |
| **cache** | Valkey | Valkey (Redis fork) | `/opt/gbo/bin/valkey-server` | `/opt/gbo/logs/valkey/` | `/opt/gbo/data/valkey/` | Distributed cache |
| **drive** | MinIO | MinIO | `/opt/gbo/bin/minio` | `/opt/gbo/logs/minio/` | `/opt/gbo/data/minio/` | Object storage (S3 API) |
| **directory** | Zitadel | Zitadel (Go) | `/opt/gbo/bin/zitadel` | `/opt/gbo/logs/zitadel.log` | `PROD-DIRECTORY` DB | Identity provider |
| **llm** | llama.cpp | C++/CUDA | `/opt/gbo/bin/llama-server` | `/opt/gbo/logs/llm/` | `/opt/gbo/models/` | Local LLM inference |
| **vectordb** | Qdrant | Qdrant (Rust) | `/opt/gbo/bin/qdrant` | `/opt/gbo/logs/qdrant/` | `/opt/gbo/data/qdrant/` | Vector database |
| **alm** | Forgejo | Forgejo (Go) | `/opt/gbo/bin/forgejo` | `/opt/gbo/logs/forgejo/` | `/opt/gbo/data/forgejo/` | Git server (port 4747) |
| **alm-ci** | Forgejo Runner | Docker/runner | `/opt/gbo/bin/forgejo-runner` | `/opt/gbo/logs/forgejo-runner.log` | `/opt/gbo/data/ci/` | CI/CD runner |
| **proxy** | Caddy | Caddy | `/opt/gbo/bin/caddy` | `/opt/gbo/logs/caddy/` | `/opt/gbo/conf/` | Reverse proxy |
| **email** | Stalwart | Stalwart (Rust) | `/opt/gbo/bin/stalwart` | `/opt/gbo/logs/email/` | `/opt/gbo/data/email/` | Mail server |
| **webmail** | Roundcube | PHP | `/usr/share/roundcube/` | `/var/log/php/` | `/var/lib/roundcube/` | Webmail frontend |
| **dns** | CoreDNS | CoreDNS (Go) | `/opt/gbo/bin/coredns` | `/opt/gbo/logs/dns/` | `/opt/gbo/conf/Corefile` | DNS resolution |
| **meet** | LiveKit | LiveKit (Go) | `/opt/gbo/bin/livekit-server` | `/opt/gbo/logs/meet/` | `/opt/gbo/data/meet/` | Video conferencing |
| **table-editor** | NocoDB | NocoDB | `/opt/gbo/bin/nocodb` | `/opt/gbo/logs/nocodb/` | `/opt/gbo/data/nocodb/` | Database UI |
### Network Access
Externally, services are exposed via reverse proxy (Caddy). Internally, containers communicate via private IPs:
| Service | External URL | Internal Address |
|---------|--------------|------------------|
| BotServer | `https://<system-domain>` | `http://<system-ip>:8080` |
| BotUI | `https://<chat-domain>` | `http://<system-ip>:3000` |
| Zitadel | `https://<login-domain>` | `http://<directory-ip>:8080` |
| Forgejo | `https://<alm-domain>` | `http://<alm-ip>:4747` |
| Webmail | `https://<webmail-domain>` | `http://<webmail-ip>:80` |
| Roundcube | `https://<roundcube-domain>` | `http://<webmail-ip>:80` |
**Note:** BotUI's `BOTSERVER_URL` must be `http://<system-ip>:8080` internally, NOT the external HTTPS URL.
---
## Daily Operations
### Daily Health Check (5 minutes)
Run this every morning or after any deploy:
```bash
# 1. Container status
sudo incus list
# 2. Service health - all should show "active (running)"
sudo incus exec system -- systemctl is-active botserver
sudo incus exec system -- systemctl is-active ui
sudo incus exec directory -- systemctl is-active directory 2>/dev/null || echo "Directory check failed"
sudo incus exec drive -- pgrep -f minio > /dev/null && echo "MinIO OK" || echo "MinIO DOWN"
sudo incus exec tables -- pgrep -f postgres > /dev/null && echo "PostgreSQL OK" || echo "PostgreSQL DOWN"
# 3. IPv4 connectivity check - all containers should have IPv4
sudo incus list -c n4 | grep -E "(system|tables|vault|directory|drive|cache|llm|vector_db)" | grep -v "10\." && echo "WARNING: Missing IPv4" || echo "IPv4 OK"
# 4. Application health endpoint
curl -sf https://<system-domain>/api/health && echo "Health OK" || echo "Health FAILED"
# 5. Recent errors (last 10 lines)
sudo incus exec system -- tail -10 /opt/gbo/logs/err.log | grep -i "error\|panic\|failed" | head -5
```
**Expected Result:** All services "active", all containers have IPv4, health endpoint returns 200, no critical errors.
### Weekly Deep Check (15 minutes)
Run every Monday morning:
```bash
# 1. Disk space on all containers
for c in system tables vault directory drive cache llm vector_db; do
echo "=== $c ==="
sudo incus exec $c -- df -h / 2>/dev/null | tail -1
done
# 2. Database connection pool status
sudo incus exec tables -- psql -h localhost -U postgres -d botserver -c "SELECT count(*), state FROM pg_stat_activity GROUP BY state;"
# 3. Vault status (should be unsealed)
sudo incus exec vault -- curl -ksf https://localhost:8200/v1/sys/health | grep -q '"sealed":false' && echo "Vault unsealed" || echo "Vault SEALED - CRITICAL"
# 4. CI runner status
sudo incus exec alm-ci -- pgrep -f forgejo > /dev/null && echo "CI runner OK" || echo "CI runner DOWN"
# 5. MinIO buckets health
sudo incus exec drive -- bash -c 'export PATH=/opt/gbo/bin:$PATH && mc admin info local' 2>&1 | head -10
# 6. Backup verification - check latest snapshot exists
sudo incus snapshot list system | head -5
```
### Quick Status Dashboard
One-line status of everything:
```bash
echo "=== GBO Status Dashboard $(date) ==="
echo "Containers:"
sudo incus list -c n4,s | grep -E "(system|tables|vault|directory|drive|cache|llm|vector_db|alm-ci)" | awk '{print $1 ": " $3 " " $4}'
echo ""
echo "Services:"
for svc in botserver ui; do
sudo incus exec system -- systemctl is-active $svc 2>/dev/null && echo " $svc: ACTIVE" || echo " $svc: DOWN"
done
echo ""
echo "Health:"
curl -s -o /dev/null -w "%{http_code}" https://<system-domain>/api/health 2>/dev/null | grep -q "200" && echo " API: OK" || echo " API: FAIL"
```
---
## Alert Response Playbook
### Alert: "No IPv4 on container"
**Symptoms:** Container shows empty IPV4 column in `incus list`
**Quick Fix:**
```bash
# Identify container
CONTAINER=<name>
IP=<unused-ip-in-range> # e.g., 10.x.x.x
GATEWAY=<gateway-ip>
# Set static IP
sudo incus config device set $CONTAINER eth0 ipv4.address $IP
# Configure network inside
sudo incus exec $CONTAINER -- bash -c "cat > /etc/network/interfaces << 'EOF'
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet static
address $IP
netmask 255.255.255.0
gateway $GATEWAY
dns-nameservers 8.8.8.8 8.8.4.4
EOF"
# Restart
sudo incus restart $CONTAINER
# Verify
sudo incus exec $CONTAINER -- ip addr show eth0
```
**Prevention:** Always configure static IP when creating new containers.
---
### Alert: "ALM botserver problem" / CI Build Failed
**Symptoms:** Deploy not working, CI status shows failure
**Quick Diagnostics:**
```bash
# Check CI database for recent runs
sudo incus exec tables -- bash -c 'export PGPASSWORD=<password>; psql -h localhost -U postgres -d PROD-ALM -c "SELECT id, status, created FROM action_run ORDER BY id DESC LIMIT 5;"'
# Status codes: 0=pending, 1=success, 2=failure, 3=cancelled, 6=running
```
**Quick Fixes:**
1. **If stuck at status 6 (running):**
```bash
RUN_ID=<stuck-run-id>
sudo incus exec tables -- bash -c "export PGPASSWORD=<password>; psql -h localhost -U postgres -d PROD-ALM -c \"UPDATE action_task SET status = 0 WHERE id = $RUN_ID; UPDATE action_run_job SET status = 0 WHERE run_id = $RUN_ID; UPDATE action_run SET status = 0 WHERE id = $RUN_ID;\""
```
2. **If /tmp permission denied:**
```bash
sudo incus exec alm-ci -- chmod 1777 /tmp
sudo incus exec alm-ci -- touch /tmp/build.log && chmod 666 /tmp/build.log
```
3. **If CI runner down:**
```bash
sudo incus exec alm-ci -- pkill -9 forgejo
sleep 2
sudo incus exec alm-ci -- bash -c 'cd /opt/gbo/bin && nohup ./forgejo-runner daemon --config config.yaml >> /opt/gbo/logs/forgejo-runner.log 2>&1 &'
```
**After fix:** Push a trivial change to re-trigger CI.
---
### Alert: "Email container stopping reach Internet"
**Symptoms:** Email notifications failing, container cannot resolve external domains
**Quick Diagnostics:**
```bash
# Test DNS from email container
sudo incus exec email -- nslookup google.com
# Check network config
sudo incus exec email -- cat /etc/resolv.conf
sudo incus exec email -- ip route
```
**Quick Fixes:**
1. **If IPv6-only (no IPv4):** Follow "No IPv4 on container" playbook above.
2. **If DNS not working:**
```bash
# Force Google DNS
sudo incus exec email -- bash -c 'echo "nameserver 8.8.8.8" > /etc/resolv.conf'
# Or configure via interfaces file
sudo incus exec email -- bash -c "cat > /etc/network/interfaces << 'EOF'
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet static
address <email-container-ip>
netmask 255.255.255.0
gateway <gateway>
dns-nameservers 8.8.8.8 8.8.4.4
EOF"
sudo incus restart email
```
3. **If firewall blocking:** Check iptables rules on host for email container IP.
---
### Alert: "Vault sealed"
**Symptoms:** All services failing, Vault health shows "sealed": true
**Quick Fix:**
```bash
# Get unseal keys from secure location (not in git!)
KEY1=<key-from-secure-location>
KEY2=<key-from-secure-location>
KEY3=<key-from-secure-location>
sudo incus exec vault -- vault operator unseal $KEY1
sudo incus exec vault -- vault operator unseal $KEY2
sudo incus exec vault -- vault operator unseal $KEY3
# Verify
sudo incus exec vault -- vault status
```
---
### Alert: "Botserver not responding"
**Quick Diagnostics:**
```bash
# Check process
sudo incus exec system -- pgrep -f botserver || echo "NOT RUNNING"
# Check systemd status
sudo incus exec system -- systemctl status botserver --no-pager
# Check recent logs
sudo incus exec system -- tail -20 /opt/gbo/logs/err.log
# Check for GLIBC errors
sudo incus exec system -- ldd /opt/gbo/bin/botserver | grep "not found"
```
**Quick Fixes:**
1. **If systemd failed:**
```bash
sudo incus exec system -- systemctl restart botserver
sudo incus exec system -- systemctl restart ui
```
2. **If GLIBC mismatch:** Binary compiled with wrong glibc. Must rebuild inside system container (Debian 12, glibc 2.36).
3. **If port conflict:**
```bash
sudo incus exec system -- lsof -i :8080
sudo incus exec system -- killall botserver
sudo incus exec system -- systemctl start botserver
```
---
## Services Detail
Botserver runs as user `gbuser`, binary at `/opt/gbo/bin/botserver`, logs at `/opt/gbo/logs/out.log` and `/opt/gbo/logs/err.log`, systemd unit at `/etc/systemd/system/botserver.service`, env loaded from `/opt/gbo/bin/.env`. Bot BASIC scripts are stored in MinIO Drive under `{bot}.gbai/{bot}.gbdialog/*.bas` and are downloaded/compiled by DriveMonitor to `/opt/gbo/work/{bot}.gbai/{bot}.gbdialog/*.ast`.
The directory service runs Zitadel as user `root`, binary at `/opt/gbo/bin/zitadel`, logs at `/opt/gbo/logs/zitadel.log`, systemd unit at `/etc/systemd/system/directory.service`, and loads environment from the service configuration. Zitadel provides identity management and OAuth2 services for the platform.
Internally, Zitadel listens on port 8080 within the directory container. For external access:
- Via public domain (HTTPS): `https://<login-domain>` (configured through proxy container)
- Via host IP (HTTP): `http://<host-ip>:9000` (direct container port forwarding)
- Via container IP (HTTP): `http://<directory-container-ip>:9000` (direct container access)
Access the Zitadel console at `https://<login-domain>/ui/console` with admin credentials. Zitadel implements v1 Management API (deprecated) and v2 Organization/User services. Always use the v2 endpoints under `/v2/organizations` and `/v2/users` for all operations.
The botserver bootstrap also manages: Vault (secrets), PostgreSQL (database), Valkey (cache, password auth), MinIO (object storage), Zitadel (identity provider), and llama.cpp (LLM).
To obtain a PAT for Zitadel API access, check /opt/gbo/conf/directory/admin-pat.txt in the directory container. Use it with curl by setting the Authorization header: `Authorization: Bearer $(cat /opt/gbo/conf/directory/admin-pat.txt)` and include `-H "Host: <directory-ip> "` for correct host resolution.
---
## Directory Management (Zitadel)
### Getting Admin PAT (Personal Access Token)
```bash
# Get the admin PAT from directory container
PAT=$(ssh administrator@<hostname> "sudo incus exec directory -- cat /opt/gbo/conf/directory/admin-pat.txt")
```
### User Management via API (v2)
**Create a Human User:**
```bash
curl -X POST "http://<directory-ip>:8080/v2/users/human" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PAT" \
-H "Host: <directory-ip>" \
-d '{
"username": "testuser",
"profile": {"givenName": "Test", "familyName": "User"},
"email": {"email": "test@example.com", "isVerified": true},
"password": {"password": "<password>", "changeRequired": false}
}'
```
**List Users:**
```bash
curl -X POST "http://<directory-ip>:8080/v2/users" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PAT" \
-H "Host: <directory-ip>" \
-d '{"query": {"offset": 0, "limit": 100}}'
```
**Update User Password:**
```bash
curl -X POST "http://<directory-ip>:8080/v2/users/<user-id>/password" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PAT" \
-H "Host: <directory-ip>" \
-d '{
"newPassword": {"password": "<password>", "changeRequired": false}
}'
```
**Delete User:**
```bash
curl -X DELETE "http://<directory-ip>:8080/v2/users/<user-id>" \
-H "Authorization: Bearer $PAT" \
-H "Host: <directory-ip>"
```
### Directory Quick Reference
| Task | Command |
|------|---------|
| Get PAT | `sudo incus exec directory -- cat /opt/gbo/conf/directory/admin-pat.txt` |
| Check health | `curl -sf http://<directory-ip>:8080/debug/healthz` |
| Console UI | `http://<host-ip>:9000/ui/console` |
| Create user | `POST /v2/users/human` |
| List users | `POST /v2/users` |
| Update password | `POST /v2/users/{id}/password` |
# /tmp permission denied for build.log
sudo incus exec alm-ci -- chmod 1777 /tmp
sudo incus exec alm-ci -- touch /tmp/build.log && chmod 666 /tmp/build.log
# Clean old CI runs (keep recent)
sudo incus exec tables -- bash -c 'export PGPASSWORD=<postgres-password>; psql -h localhost -U postgres -d PROD-ALM -c "DELETE FROM action_run WHERE id < <RECENT_ID>;"'
sudo incus exec tables -- bash -c 'export PGPASSWORD=<postgres-password>; psql -h localhost -U postgres -d PROD-ALM -c "DELETE FROM action_run_job WHERE run_id < <RECENT_ID>;"'
```
**Watch CI in real-time:**
```bash
# Tail runner logs
sudo incus exec alm-ci -- tail -f /opt/gbo/logs/forgejo-runner.log
# Check if new builds appear
watch -n 5 'sudo incus exec tables -- bash -c "export PGPASSWORD=<postgres-password>; psql -h localhost -U postgres -d PROD-ALM -c \\"SELECT id, status, created FROM action_run ORDER BY id DESC LIMIT 3;\\""'
# Verify botserver deployed correctly
sudo incus exec system -- /opt/gbo/bin/botserver --version 2>&1 | head -3
sudo incus exec system -- tail -5 /opt/gbo/logs/err.log
```
### Monitor CI/CD Build Status
**Check latest build status:**
```bash
# View latest 3 builds with status
sudo incus exec alm -- bash -c 'cd /opt/gbo/data/GeneralBots/BotServer/actions/runs && for dir in $(ls -t | head -3); do echo "=== Build $dir ==="; cat $dir/jobs/0.json 2>/dev/null | grep -E "\"status\"|\"commit\"|\"workflow\"" | head -5; done'
# Watch runner logs in real-time
sudo incus exec alm-ci -- tail -f /opt/gbo/logs/forgejo-runner.log | grep -E "Clone|Build|Deploy|Success|Failure"
```
**Understand build timing:**
- **Rust compilation**: 2-5 minutes (cold build), 30-60 seconds (incremental)
- **Dependencies**: First build downloads ~200 dependencies
- **Deploy step**: ~5 seconds
- **Total CI time**: 2-6 minutes depending on cache
**Verify binary was updated:**
```bash
# Check binary timestamp
ssh administrator@63.141.255.9 "sudo incus exec system -- stat -c '%y' /opt/gbo/bin/botserver"
# Check running version
ssh administrator@63.141.255.9 "sudo incus exec system -- /opt/gbo/bin/botserver --version"
# Check health endpoint
curl -sf https://chat.pragmatismo.com.br/api/health || echo "Health check failed"
```
```
---
## DriveMonitor & Bot Configuration
DriveMonitor is a background service inside botserver that watches MinIO buckets and syncs changes to the local filesystem and database every 10 seconds. It monitors three directory types per bot: the `.gbdialog/` folder for BASIC scripts (downloads and recompiles on change), the `.gbot/` folder for `config.csv` (syncs to the `bot_configuration` database table), and the `.gbkb/` folder for knowledge base documents (downloads and indexes for vector search).
Bot configuration is stored in two PostgreSQL tables inside the `botserver` database. The `bot_configuration` table holds key-value pairs with columns `bot_id`, `config_key`, `config_value`, `config_type`, `is_encrypted`, and `updated_at`. The `gbot_config_sync` table tracks sync state with columns `bot_id`, `config_file_path`, `last_sync_at`, `file_hash`, and `sync_count`.
The `config.csv` format is a plain CSV with no header: each line is `key,value`, for example `llm-provider,groq` or `theme-color1,#cc0000`. DriveMonitor syncs it when the file ETag changes in MinIO, on botserver startup, or after a restart.
**Check config status:** Query `bot_configuration` via `sudo incus exec tables -- psql -h localhost -U postgres -d botserver -c "SELECT config_key, config_value FROM bot_configuration WHERE bot_id = (SELECT id FROM bots WHERE name = '<botname>') ORDER BY config_key;"`. Check sync state via the `gbot_config_sync` table. Inspect the bucket directly with `sudo incus exec drive -- /opt/gbo/bin/mc cat local/<botname>.gbai/<botname>.gbot/config.csv`.
**Debug DriveMonitor:** Monitor live logs with `sudo incus exec system -- tail -f /opt/gbo/logs/out.log | grep -E "(DRIVE_MONITOR|check_gbot|config)"`. An empty `gbot_config_sync` table means DriveMonitor has not synced yet. If no new log entries appear after 30 seconds, the loop may be stuck — restart botserver with systemctl to clear the state.
**Common config issues:** If config.csv is missing from the bucket, create and upload it with `mc cp`. If the database shows stale values, restart botserver to force a fresh sync, or as a temporary fix update the database directly with `UPDATE bot_configuration SET config_value = 'groq', updated_at = NOW() WHERE ...`. To force a re-sync without restarting, copy config.csv over itself with `mc cp local/... local/...` to change the ETag.
---
## MinIO (Drive) Operations
All bot files live in MinIO buckets. Use the `mc` CLI at `/opt/gbo/bin/mc` from inside the `drive` container. The bucket structure per bot is: `{bot}.gbai/` as root, `{bot}.gbai/{bot}.gbdialog/` for BASIC scripts, `{bot}.gbai/{bot}.gbot/` for config.csv, and `{bot}.gbai/{bot}.gbkb/` for knowledge base folders.
Common mc commands: `mc ls local/` lists all buckets; `mc ls local/botname.gbai/` lists a bucket; `mc cat local/.../start.bas` prints a file; `mc cp local/.../file /tmp/file` downloads; `mc cp /tmp/file local/.../file` uploads (this triggers DriveMonitor recompile); `mc stat local/.../config.csv` shows ETag and metadata; `mc mb local/newbot.gbai` creates a bucket; `mc rb local/oldbot.gbai` removes an empty bucket.
If mc is not found, use the full path `/opt/gbo/bin/mc`. If alias `local` is not configured, check with `mc config host list`. If MinIO is not running, check with `sudo incus exec drive -- systemctl status minio`.
---
## Vault Security Architecture
HashiCorp Vault is the single source of truth for all secrets. Botserver reads `VAULT_ADDR` and `VAULT_TOKEN` from `/opt/gbo/bin/.env` at startup, initializes a TLS/mTLS client, then reads credentials from Vault paths. If Vault is unavailable, it falls back to defaults. The `.env` file must only contain `VAULT_*` variables plus `PORT`, `DATA_DIR`, `WORK_DIR`, and `LOAD_ONLY`.
**Global Vault paths:** `gbo/tables` holds PostgreSQL credentials; `gbo/drive` holds MinIO access key and secret; `gbo/cache` holds Valkey password; `gbo/llm` holds LLM URL and API keys; `gbo/directory` holds Zitadel config; `gbo/email` holds SMTP credentials; `gbo/vectordb` holds Qdrant config; `gbo/jwt` holds JWT signing secret; `gbo/encryption` holds the master encryption key. Organization-scoped secrets follow patterns like `gbo/orgs/{org_id}/bots/{bot_id}` and tenant infrastructure uses `gbo/tenants/{tenant_id}/infrastructure`.
**Credential resolution:** For any service, botserver checks the most specific Vault path first (org+bot level), falls back to a default bot path, then falls back to the global path, and only uses environment variables as a last resort in development.
**Verify Vault health:** `sudo incus exec vault -- curl -k -sf https://localhost:8200/v1/sys/health` should return JSON with `"sealed":false`. To read a secret: set `VAULT_ADDR`, `VAULT_TOKEN`, and `VAULT_CACERT` then run `vault kv get secret/gbo/tables`. To test from the system container, use curl with `--cacert /opt/gbo/conf/system/certificates/ca/ca.crt` and `-H "X-Vault-Token: <token>"`.
**init.json** is stored at `/opt/gbo/bin/botserver-stack/conf/vault/vault-conf/init.json` and contains the root token and 5 unseal keys (3 needed to unseal). Never commit this file to git. Store it encrypted in a secure location.
**Vault troubleshooting — cannot connect:** Check that the vault container's systemd unit is running, verify the token in `.env` is not expired with `vault token lookup`, confirm the CA cert path in `.env` matches the actual file location, and test network connectivity from system to vault container. To generate a new token: `vault token create -policy="botserver" -ttl="8760h" -format=json` then update `.env` and restart botserver.
**Get database credentials from Vault v2 API:**
```bash
ssh user@<hostname> "sudo incus exec system -- curl -s --cacert /opt/gbo/conf/system/certificates/ca/ca.crt -H 'X-Vault-Token: <vault-token>' https://<vault-ip>:8200/v1/secret/data/gbo/tables 2>/dev/null"
```
**Vault troubleshooting — secrets missing:** Run `vault kv get secret/gbo/tables` (and other paths) to check if secrets exist. If a path returns NOT FOUND, add secrets with `vault kv put secret/gbo/tables host=<ip> port=5432 database=botserver username=gbuser password=<pw>` and similar for other paths.
**Vault sealed after restart:** Run `vault operator unseal <key1>`, repeat with key2 and key3 (3 of 5 keys from init.json), then verify with `vault status`.
**TLS certificate errors:** Confirm `/opt/gbo/conf/system/certificates/ca/ca.crt` exists in the system container. If missing, copy it from the vault container using `incus file pull vault/opt/gbo/conf/vault/ca.crt /tmp/ca.crt` then place it at the expected path.
**Vault snapshots:** Stop vault, run `sudo incus snapshot create vault backup-$(date +%Y%m%d-%H%M)`, start vault. Restore with `sudo incus snapshot restore vault <name>` while vault is stopped.
---
## DNS Management
### Updating DNS Records
**CRITICAL:** When updating DNS zone files, you MUST:
1. **Update the serial number** in the SOA record (format: YYYYMMDDNN)
2. **Run sync-zones.sh** to propagate changes to secondary nameservers
3. **Anonymize IPs and credentials** in all documentation and logs
**Workflow:**
```bash
# 1. Edit zone file
sudo incus exec dns -- nano /opt/gbo/data/pragmatismo.com.br.zone
# 2. Update serial (YYYYMMDDNN format)
# Example: 2026041801 (April 18, 2026, change #1)
sudo incus exec dns -- sed -i 's/2026041801/2026041802/' /opt/gbo/data/pragmatismo.com.br.zone
# 3. Reload CoreDNS
sudo incus exec dns -- pkill -HUP coredns
# 4. Sync to secondary NS
sudo /opt/gbo/bin/sync-zones.sh
# 5. Verify on secondary
ssh -o StrictHostKeyChecking=no -i /home/administrator/.ssh/id_ed25519 admin@<secondary-ip> 'getent hosts <domain>'
```
**Zone File Location:** `/opt/gbo/data/<domain>.zone` in the `dns` container
**Sync Script:** `/opt/gbo/bin/sync-zones.sh` - copies zone files to secondary NS (3.218.224.38)
**⚠️ Security Rules:**
- NEVER include real IPs in documentation - use `<ip>` or `10.x.x.x`
- NEVER include credentials - use `<password>` or `<token>`
- NEVER commit zone files with secrets to git
---
### Adding New Subdomains (HTTPS with Caddy)
**CRITICAL:** When adding new subdomains that need HTTPS, follow this order:
1. **Add DNS record FIRST** (see above workflow)
2. **Wait for DNS propagation** (can take up to 1 hour)
3. **Add Caddy config** - Caddy will automatically obtain Let's Encrypt certificate
**Complete Workflow:**
```bash
# 1. Add DNS record (update serial, sync zones)
sudo incus exec dns -- nano /opt/gbo/data/pragmatismo.com.br.zone
# Add: news IN A <ip>
sudo incus exec dns -- sed -i 's/2026041801/2026041802/' /opt/gbo/data/pragmatismo.com.br.zone
sudo incus exec dns -- pkill -HUP coredns
sudo /opt/gbo/bin/sync-zones.sh
# 2. Verify DNS propagation (wait until this works)
dig @9.9.9.9 news.pragmatismo.com.br A +short
# 3. Add Caddy config (AFTER DNS is working)
sudo sh -c 'cat >> /opt/gbo/conf/config << EOF
news.pragmatismo.com.br {
import tls_config
reverse_proxy http://<container-ip>:<port> {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-Proto https
}
}
EOF'
# 4. Restart Caddy
sudo incus exec proxy -- systemctl restart proxy
# 5. Wait for certificate (Caddy will auto-obtain from Let's Encrypt)
# Check logs: sudo incus exec proxy -- tail -f /opt/gbo/logs/access.log
```
**⚠️ Important:**
- Caddy will fail to obtain certificate if DNS is not propagated
- Wait up to 1 hour for DNS propagation before adding Caddy config
- Check Caddy logs for "challenge failed" errors - indicates DNS not ready
- Certificate is automatically renewed by Caddy
---
## Troubleshooting Quick Reference
**botserver won't start:** Run `sudo incus exec system -- ldd /opt/gbo/bin/botserver | grep "not found"` to check for missing libraries. Run `sudo incus exec system -- timeout 10 /opt/gbo/bin/botserver 2>&1` to see startup errors. Confirm `/opt/gbo/work/` exists and is accessible.
**botui can't reach botserver:** Check that the `ui.service` systemd file has `BOTSERVER_URL=http://localhost:5858` — not the external HTTPS URL. Fix with `sed -i 's|BOTSERVER_URL=.*|BOTSERVER_URL=http://localhost:5858|'` on the service file, then `systemctl daemon-reload` and `systemctl restart ui`.
**Suggestions not showing:** Confirm bot `.bas` files exist in MinIO Drive under `{bot}.gbai/{bot}.gbdialog/`. Check logs for compilation errors. Clear the AST cache in `/opt/gbo/work/` and restart botserver.
**IPv6 DNS timeouts on external APIs (Groq, Cloudflare):** The container's DNS may return AAAA records without IPv6 connectivity. The container should have `IPV6=no` in its network config and `gai.conf` set appropriately. Check for `RES_OPTIONS=inet4` in `botserver.service` if issues persist.
**Logs show development paths instead of Drive:** Botserver is using hardcoded dev paths. Check `.env` has `DATA_DIR=/opt/gbo/work/` and `WORK_DIR=/opt/gbo/work/`, verify the systemd unit has `EnvironmentFile=/opt/gbo/bin/.env`, and confirm Vault is reachable so service discovery works. Expected startup log lines include `info watcher:Watching data directory /opt/gbo/work` and `info botserver:BotServer started successfully on port 5858`.
**Migrations not running after push:** If `stat /opt/gbo/bin/botserver` shows old timestamp and `__diesel_schema_migrations` table has no new entries, CI did not rebuild. Make a trivial code change (e.g., add a comment) in botserver and push again to force rebuild.
---
## Drive (MinIO) File Operations Cheatsheet
All `mc` commands run inside the `drive` container with `PATH` set: `sudo incus exec drive -- bash -c 'export PATH=/opt/gbo/bin:$PATH && mc <command>'`. If `local` alias is missing, create it with credentials from Vault path `gbo/drive`.
**List bucket contents recursively:** `mc ls local/<bot>.gbai/ --recursive`
**Read a file from Drive:** `mc cat local/<bot>.gbai/<bot>.gbdialog/start.bas`
**Download a file:** `mc cp local/<bot>.gbai/<bot>.gbdialog/start.bas /tmp/start.bas`
**Upload a file to Drive (triggers DriveMonitor recompile):** Transfer file to host via `scp`, push into drive container with `sudo incus file push /tmp/file drive/tmp/file`, then `mc put /tmp/file local/<bot>.gbai/<bot>.gbdialog/start.bas`
**Full upload workflow example — updating config.csv:**
```bash
# 1. Download current config from Drive
ssh user@host "sudo incus exec drive -- bash -c 'export PATH=/opt/gbo/bin:\$PATH && mc cat local/botname.gbai/botname.gbot/config.csv'" > /tmp/config.csv
# 2. Edit locally (change model, keys, etc.)
sed -i 's/llm-model,old-model/llm-model,new-model/' /tmp/config.csv
# 3. Push edited file back to Drive
scp /tmp/config.csv user@host:/tmp/config.csv
ssh user@host "sudo incus file push /tmp/config.csv drive/tmp/config.csv"
ssh user@host "sudo incus exec drive -- bash -c 'export PATH=/opt/gbo/bin:\$PATH && mc put /tmp/config.csv local/botname.gbai/botname.gbot/config.csv'"
# 4. Wait ~15 seconds, then verify DriveMonitor picked up the change
ssh user@host "sudo incus exec system -- bash -c 'grep -i \"Model:\" /opt/gbo/logs/err.log | tail -3'"
```
**Force re-sync of config.csv** (change ETag without content change): `mc cp local/<bot>.gbai/<bot>.gbot/config.csv local/<bot>.gbai/<bot>.gbot/config.csv`
**Create a new bot bucket:** `mc mb local/newbot.gbai`
**Check MinIO health:** `sudo incus exec drive -- bash -c '/opt/gbo/bin/mc admin info local'`
---
## Logging Quick Reference
**Application logs** (searchable, timestamped, most useful): `sudo incus exec system -- tail -f /opt/gbo/logs/err.log` (errors and debug) or `/opt/gbo/logs/out.log` (stdout). The systemd journal only captures process lifecycle events, not application output.
**Search logs for specific bot activity:** `grep -i "botname\|llm\|Model:\|KB\|USE_KB\|drive_monitor" /opt/gbo/logs/err.log | tail -30`
**Check which LLM model a bot is using:** `grep "Model:" /opt/gbo/logs/err.log | tail -5`
**Check DriveMonitor config sync:** `grep "check_gbot\|config.csv\|should_sync" /opt/gbo/logs/err.log | tail -20`
**Check KB/vector operations:** `grep -i "gbkb\|qdrant\|embedding\|index" /opt/gbo/logs/err.log | tail -20`
**Live tail with filter:** `sudo incus exec system -- bash -c 'tail -f /opt/gbo/logs/err.log | grep --line-buffered -i "botname\|error\|KB"'`
---
## Program Access Cheatsheet
| Program | Container | Path | Notes |
|---------|-----------|------|-------|
| botserver | system | `/opt/gbo/bin/botserver` | Run via systemctl only |
| botui | system | `/opt/gbo/bin/botui` | Run via systemctl only |
| mc (MinIO Client) | drive | `/opt/gbo/bin/mc` | Must set `PATH=/opt/gbo/bin:$PATH` |
| psql | tables | `/usr/bin/psql` | `psql -h localhost -U postgres -d botserver` |
| vault | vault | `/opt/gbo/bin/vault` | Needs `VAULT_ADDR`, `VAULT_TOKEN`, `VAULT_CACERT` |
| zitadel | directory | `/opt/gbo/bin/zitadel` | Runs as root on port 8080 internally |
**Quick psql query — bot config:** `sudo incus exec tables -- psql -h localhost -U postgres -d botserver -c "SELECT config_key, config_value FROM bot_configuration WHERE bot_id = (SELECT id FROM bots WHERE name = 'botname') ORDER BY config_key;"`
**Quick psql query — active KBs for session:** `sudo incus exec tables -- psql -h localhost -U postgres -d botserver -c "SELECT * FROM session_kb_associations WHERE session_id = '<uuid>' AND is_active = true;"`
---
## BASIC Compilation Architecture
Compilation and runtime are now strictly separated. **Compilation** happens only in `BasicCompiler` inside DriveMonitor when it detects `.bas` file changes. The output is a fully preprocessed `.ast` file written to `work/<bot>.gbai/<bot>.gbdialog/<tool>.ast`. **Runtime** (start.bas, TOOL_EXEC, automation, schedule) loads only `.ast` files and calls `ScriptService::run()` which does `engine.compile() + eval_ast_with_scope()` on the already-preprocessed Rhai source — no preprocessing at runtime.
The `.ast` file has all transforms applied: `USE KB "cartas"` becomes `USE_KB("cartas")`, `IF/END IF``if/{ }`, `WHILE/WEND``while/{ }`, `BEGIN TALK/END TALK` → function calls, `SAVE`, `FOR EACH/NEXT`, `SELECT CASE`, `SET SCHEDULE`, `WEBHOOK`, `USE WEBSITE`, `LLM` keyword expansion, variable predeclaration, and keyword lowercasing. Runtime never calls `compile()`, `compile_tool_script()`, or `compile_preprocessed()` — those methods no longer exist.
**Tools (TOOL_EXEC) load `.ast` only** — there is no `.bas` fallback. If an `.ast` file is missing, the tool fails with "Failed to read tool .ast file". DriveMonitor must have compiled it first.
**Suggestion deduplication** uses Redis `SADD` (set) instead of `RPUSH` (list). This prevents duplicate suggestion buttons when `start.bas` runs multiple times per session. The key format is `suggestions:{bot_id}:{session_id}` and `get_suggestions` uses `SMEMBERS` to read it.
---
## Container Quick Reference
| Container | Critical | Check Command | Restart Command |
|-----------|----------|---------------|-----------------|
| system | YES | `systemctl is-active botserver` | `systemctl restart botserver` |
| tables | YES | `pgrep -f postgres` | `systemctl restart postgresql` |
| vault | YES | `curl -ksf https://localhost:8200/v1/sys/health` | `systemctl restart vault` |
| drive | YES | `pgrep -f minio` | `systemctl restart minio` |
| cache | HIGH | `pgrep -f valkey` | `systemctl restart valkey` |
| directory | HIGH | `curl -sf http://localhost:8080/debug/healthz` | `systemctl restart directory` |
| alm-ci | MED | `pgrep -f forgejo` | manual restart |
| llm | MED | `curl -sf http://localhost:8081/health` | `systemctl restart llm` |
| vector_db | LOW | `curl -sf http://localhost:6333/healthz` | `systemctl restart qdrant` |
---
## Log Tailing Commands
```bash
# Live error monitoring
sudo incus exec system -- tail -f /opt/gbo/logs/err.log | grep -i "error\|panic\|failed"
# Bot-specific activity
sudo incus exec system -- tail -f /opt/gbo/logs/err.log | grep -i "<botname>"
# DriveMonitor activity
sudo incus exec system -- tail -f /opt/gbo/logs/err.log | grep -i "drive\|config"
# LLM calls
sudo incus exec system -- tail -f /opt/gbo/logs/err.log | grep -i "model\|llm\|groq"
# CI runner
sudo incus exec alm-ci -- tail -f /opt/gbo/logs/forgejo-runner.log
```
---
## Health Endpoint Monitoring
Set up a simple cron job to alert if health fails:
```bash
# Add to host crontab (crontab -e)
*/5 * * * * curl -sf https://<system-domain>/api/health || echo "ALERT: Health check failed at $(date)" >> /var/log/gbo-health.log
```
---
## Troubleshooting Quick Reference
### Container Won't Start (No IPv4)
**Symptom:** Container shows empty IPV4 column in `sudo incus list`
**Diagnose:**
```bash
sudo incus list <container> -c n4
sudo incus exec <container> -- ip addr show eth0
```
**Fix:**
```bash
# 1. Stop container
sudo incus stop <container>
# 2. Set static IP
sudo incus config device set <container> eth0 ipv4.address <ip-address>
# 3. Configure network inside
sudo incus exec <container> -- bash -c 'cat > /etc/network/interfaces << EOF
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet static
address <ip-address>
netmask 255.255.255.0
gateway <gateway>
dns-nameservers 8.8.8.8 8.8.4.4
EOF'
# 4. Restart
sudo incus restart <container>
# 5. Verify
sudo incus exec <container> -- ip addr show eth0
```
---
### CI/ALM Permission Errors
**Symptom:** `/tmp permission denied` during CI build
**Fix:**
```bash
# On alm-ci container
sudo incus exec alm-ci -- chmod 1777 /tmp
sudo incus exec alm-ci -- touch /tmp/build.log && chmod 666 /tmp/build.log
# Check runner user
sudo incus exec alm-ci -- ls -la /opt/gbo/
# Fix ownership
sudo incus exec alm-ci -- chown -R gbuser:gbuser /opt/gbo/bin/
sudo incus exec alm-ci -- chown -R gbuser:gbuser /opt/gbo/work/
```
**CI Runner Down:**
```bash
sudo incus exec alm-ci -- pkill -9 forgejo
sleep 2
sudo incus exec alm-ci -- bash -c 'cd /opt/gbo/bin && nohup ./forgejo-runner daemon --config config.yaml >> /opt/gbo/logs/forgejo-runner.log 2>&1 &'
```
---
### MinIO (Drive) Operations with `mc`
**Setup:**
```bash
# Access drive container
sudo incus exec drive -- bash
# Set PATH
export PATH=/opt/gbo/bin:$PATH
# Verify mc works
mc --version
```
**Common Commands:**
```bash
# List all buckets
mc ls local/
# List bot bucket
mc ls local/<botname>.gbai/
# Read start.bas
mc cat local/<botname>.gbai/<botname>.gbdialog/start.bas
# Download file
mc cp local/<botname>.gbai/<botname>.gbdialog/config.csv /tmp/config.csv
# Upload file (triggers DriveMonitor)
mc cp /tmp/config.csv local/<botname>.gbai/<botname>.gbot/config.csv
# Force re-sync (change ETag)
mc cp local/<bot>.gbai/<bot>.gbot/config.csv local/<bot>.gbai/<bot>.gbot/config.csv
# Create new bucket
mc mb local/newbot.gbai
# Check MinIO health
mc admin info local
```
**If `local` alias missing:**
```bash
# Create alias
mc alias set local http://localhost:9000 <access-key> <secret-key>
```
---
### Forgejo ALM Database Operations
**Access ALM database (PROD-ALM):**
```bash
# On tables container
sudo incus exec tables -- psql -h localhost -U postgres -d PROD-ALM
```
**Common Queries:**
```sql
-- Check CI runs
SELECT id, status, commit_sha, created FROM action_run ORDER BY id DESC LIMIT 10;
-- Status codes: 0=pending, 1=success, 2=failure, 3=cancelled, 6=running
-- Check specific run jobs
SELECT id, status, name FROM action_run_job WHERE run_id = <ID>;
-- Reset stuck run
UPDATE action_task SET status = 0 WHERE id = <ID>;
UPDATE action_run_job SET status = 0 WHERE run_id = <RUN_ID>;
UPDATE action_run SET status = 0 WHERE id = <RUN_ID>;
-- Check runner token
SELECT * FROM action_runner_token;
-- List runners
SELECT * FROM action_runner;
```
**Check CI from host:**
```bash
export PGPASSWORD=<password>
sudo incus exec tables -- psql -h localhost -U postgres -d PROD-ALM -c "SELECT id, status, created FROM action_run ORDER BY id DESC LIMIT 5;"
```
---
### Zitadel API v2 Operations
**Important:** Always use **v2 API** - v1 is deprecated and non-functional.
**Get PAT:**
```bash
PAT=$(sudo incus exec directory -- cat /opt/gbo/conf/directory/admin-pat.txt)
```
**Common Operations:**
**Create User (v2):**
```bash
curl -X POST "http://<directory-ip>:8080/v2/users/human" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PAT" \
-H "Host: <directory-ip>" \
-d '{
"username": "newuser",
"profile": {"givenName": "New", "familyName": "User"},
"email": {"email": "user@example.com", "isVerified": true},
"password": {"password": "<password>", "changeRequired": false}
}'
```
**List Users (v2):**
```bash
curl -X POST "http://<directory-ip>:8080/v2/users" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PAT" \
-H "Host: <directory-ip>" \
-d '{"query": {"offset": 0, "limit": 100}}'
```
**Create Organization (v2):**
```bash
curl -X POST "http://<directory-ip>:8080/v2/organizations" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PAT" \
-H "Host: <directory-ip>" \
-d '{"name": "organization-name"}'
```
**Add Domain to Org (v2):**
```bash
curl -X POST "http://<directory-ip>:8080/v2/organizations/<org-id>/domains" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PAT" \
-H "Host: <directory-ip>" \
-d '{"domainName": "example.com"}'
```
**⚠️ Critical:** Always include `-H "Host: <directory-ip>"` header or API returns 404.
---
### Common Errors & Quick Fixes
| Error | Cause | Fix |
|-------|-------|-----|
| `No IPv4 on container` | DHCP failed | Set static IP (see above) |
| `/tmp permission denied` | Wrong permissions | `chmod 1777 /tmp` |
| `Errors.Token.Invalid (AUTH-7fs1e)` | Zitadel PAT expired | Regenerate via console |
| `failed SASL auth` | Wrong DB password | Check Vault credentials |
| `GLIBC_2.39 not found` | Wrong build environment | Rebuild in system container |
| `connection refused` | Service down | `systemctl restart <service>` |
| `exec format error` | Architecture mismatch | Recompile for target arch |
| `address already in use` | Port conflict | `lsof -i :<port>` |
| `certificate verify failed` | Wrong CA cert | Copy from vault container |
| `DNS lookup failed` | No IPv4 connectivity | Check network config |
---
## Contact Escalation
If quick fixes don't work:
1. Capture logs: `sudo incus exec system -- tar czf /tmp/debug-$(date +%Y%m%d).tar.gz /opt/gbo/logs/`
2. Check AGENTS.md for development troubleshooting
3. Review recent commits for breaking changes
4. Consider snapshot rollback (last resort)

1067
README.md

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,74 @@
name: GBCI
on:
workflow_dispatch:
# Disabled auto-trigger - enable when needed
# push:
# branches: ["main"]
# pull_request:
# branches: ["main"]
jobs:
build:
runs-on: gbo
steps:
- name: Disable SSL verification (temporary)
run: git config --global http.sslVerify false
- uses: actions/checkout@v4
- name: Checkout botlib dependency
uses: actions/checkout@v4
with:
repository: GeneralBots/botlib
path: ../botlib
- name: Checkout botui dependency
uses: actions/checkout@v4
with:
repository: GeneralBots/botui
path: ../botui
- name: Cache Cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-android-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-android-
- name: Install Rust
uses: msrd0/rust-toolchain@v1
with:
toolchain: stable
targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
- name: Setup Android SDK
run: |
export ANDROID_HOME=/opt/android-sdk
export NDK_HOME=$ANDROID_HOME/ndk/26.1.10909125
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools
- name: Install Tauri CLI
run: |
if ! command -v cargo-tauri &> /dev/null; then
cargo install tauri-cli
fi
- name: Build Android APK
run: |
export ANDROID_HOME=/opt/android-sdk
export NDK_HOME=$ANDROID_HOME/ndk/26.1.10909125
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk
cargo tauri android build
- name: Deploy APK
run: |
sudo mkdir -p /opt/gbo/releases/botapp
sudo cp ./gen/android/app/build/outputs/apk/universal/release/*.apk /opt/gbo/releases/botapp/ || true
sudo cp ./gen/android/app/build/outputs/apk/release/*.apk /opt/gbo/releases/botapp/ || true
sudo chmod 644 /opt/gbo/releases/botapp/*.apk

40
botapp/.gitignore vendored Normal file
View file

@ -0,0 +1,40 @@
# Build artifacts
/target
/dist
# Tauri generated
/src-tauri/target
# Dependencies
/node_modules
# IDE
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
# Environment
.env
.env.local
.env.*.local
# Lock files (keep Cargo.lock for apps)
package-lock.json
yarn.lock
pnpm-lock.yaml
# Debug
debug/
*.pdb
# Test coverage
coverage/

6727
botapp/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

46
botapp/Cargo.toml Normal file
View file

@ -0,0 +1,46 @@
[package]
name = "botapp"
version = "6.3.0"
edition = "2021"
description = "General Bots App - Tauri wrapper for desktop/mobile"
license = "AGPL-3.0"
repository = "https://github.com/GeneralBots/BotServer"
keywords = ["bot", "ai", "chatbot", "tauri", "desktop"]
categories = ["gui", "network-programming"]
[dependencies]
# Core from botlib
botlib = { workspace = true, features = ["http-client"] }
# Tauri
tauri = { workspace = true, features = ["tray-icon", "image"] }
tauri-plugin-dialog = { workspace = true }
tauri-plugin-opener = { workspace = true }
# Common
anyhow = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
dirs = { workspace = true }
log = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["full"] }
reqwest = { workspace = true, features = ["json", "rustls-tls"] }
# Unix process control
[target.'cfg(unix)'.dependencies]
libc = { workspace = true }
[target.'cfg(windows)'.dependencies]
image = { workspace = true }
thiserror = { workspace = true }
[features]
default = ["desktop"]
desktop = ["desktop-tray"]
desktop-tray = []
[build-dependencies]
tauri-build = { workspace = true }
[lints]
workspace = true

3
botapp/LICENSE Normal file
View file

@ -0,0 +1,3 @@
AGPL-3.0
See https://www.gnu.org/licenses/agpl-3.0.html

427
botapp/README.md Normal file
View file

@ -0,0 +1,427 @@
# BotApp - General Bots Desktop Application
**Version:** 6.2.0
**Purpose:** Desktop application wrapper (Tauri 2)
---
## Overview
BotApp is the Tauri-based desktop wrapper for General Bots, providing native desktop and mobile capabilities on top of the pure web UI from [botui](https://github.com/GeneralBots/botui). It extends the web interface with native file system access, system tray functionality, and desktop-specific features while maintaining a clean separation from the pure web UI.
For comprehensive documentation, see **[docs.pragmatismo.com.br](https://docs.pragmatismo.com.br)** or the **[BotBook](../botbook)** for detailed guides, API references, and tutorials.
---
## Architecture
```
botui (pure web) botapp (Tauri wrapper)
┌─────────────────┐ ┌─────────────────────────┐
│ suite/ │◄─────│ Loads botui's UI │
│ minimal/ │ │ + injects app-only │
│ shared/ │ │ features via JS │
│ │ │ │
│ No Tauri deps │ │ Tauri + native APIs │
└─────────────────┘ └─────────────────────────┘
```
### Why Two Projects?
- **botui**: Pure web UI with zero native dependencies. Works in any browser.
- **botapp**: Wraps botui with Tauri for desktop/mobile native features.
This separation allows:
- Same UI code for web, desktop, and mobile
- Clean dependency management (web users don't need Tauri)
- App-specific features only in the native app
### Communication Flow
```
Native UI (HTML/CSS/JS)
↓ Tauri IPC (invoke)
Rust #[tauri::command]
↓ HTTP (reqwest)
botserver API
Business Logic + Database
```
---
## Features
BotApp adds these native capabilities to botui:
- **Local File Access**: Browse and manage files on your device
- **System Tray**: Minimize to tray, background operation
- **Native Dialogs**: File open/save dialogs
- **Desktop Notifications**: Native OS notifications
- **App Settings**: Desktop-specific configuration
---
## Project Structure
```
botapp/
├── src/
│ ├── main.rs # Rust backend, Tauri commands
│ ├── lib.rs # Library exports
│ └── desktop/
│ ├── mod.rs # Desktop module organization
│ ├── drive.rs # File system commands
│ └── tray.rs # System tray functionality
├── ui/
│ └── app-guides/ # App-specific HTML
├── js/
│ └── app-extensions.js # JavaScript extensions
├── icons/ # App icons (all sizes)
├── tauri.conf.json # Tauri configuration
└── Cargo.toml
```
---
## Development
### Prerequisites
- Rust 1.70+
- Node.js 18+ (for Tauri CLI)
- Tauri CLI: `cargo install tauri-cli`
#### Platform-specific
**Linux:**
```bash
sudo apt install libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev
```
**macOS:**
```bash
xcode-select --install
```
**Windows:**
- Visual Studio Build Tools with C++ workload
### Getting Started
1. Clone both repositories:
```bash
git clone https://github.com/GeneralBots/botui.git
git clone https://github.com/GeneralBots/botapp.git
```
2. Start botui's web server (required for dev):
```bash
cd botui
cargo run
```
3. Run botapp in development mode:
```bash
cd botapp
cargo tauri dev
```
---
## Building
### Debug Build
```bash
cargo tauri build --debug
```
### Release Build
```bash
cargo tauri build
```
Binaries will be in `target/release/bundle/`.
---
## 🖥️ Tauri Command Pattern
```rust
use tauri::command;
#[command]
pub async fn my_command(
window: tauri::Window,
param: String,
) -> Result<MyResponse, String> {
if param.is_empty() || param.len() > 1000 {
return Err("Invalid parameter".into());
}
Ok(MyResponse { /* ... */ })
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
my_command,
])
.run(tauri::generate_context!())
.map_err(|e| format!("error running app: {e}"))?;
}
```
### JavaScript Invocation
```javascript
const result = await window.__TAURI__.invoke('my_command', {
param: 'value'
});
```
### Available Tauri Commands
| Command | Description |
|---------|-------------|
| `list_files` | List directory contents |
| `upload_file` | Copy file with progress |
| `create_folder` | Create new directory |
| `delete_path` | Delete file or folder |
| `get_home_dir` | Get user's home directory |
---
## 🔐 Security Directives
### Path Validation
```rust
// ❌ WRONG - trusting user path
#[tauri::command]
async fn read_file(path: String) -> Result<String, String> {
std::fs::read_to_string(path).map_err(|e| e.to_string())
}
// ✅ CORRECT - validate and sandbox paths
#[tauri::command]
async fn read_file(app: tauri::AppHandle, filename: String) -> Result<String, String> {
let safe_name = filename
.chars()
.filter(|c| c.is_alphanumeric() || *c == '.' || *c == '-')
.collect::<String>();
if safe_name.contains("..") {
return Err("Invalid filename".into());
}
let base_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
let full_path = base_dir.join(&safe_name);
std::fs::read_to_string(full_path).map_err(|e| e.to_string())
}
```
### Security Prohibitions
```
❌ NEVER trust user input from IPC commands
❌ NEVER expose filesystem paths to frontend without validation
❌ NEVER store secrets in plain text or localStorage
❌ NEVER disable CSP in tauri.conf.json for production
❌ NEVER use allowlist: all in Tauri configuration
```
---
## 🎨 Icons - MANDATORY
**NEVER generate icons with LLM. Use official SVG icons from `botui/ui/suite/assets/icons/`**
Required icon sizes in `icons/`:
```
icon.ico # Windows (256x256)
icon.icns # macOS
icon.png # Linux (512x512)
32x32.png
128x128.png
128x128@2x.png
```
All icons use `stroke="currentColor"` for CSS theming.
---
## ⚙️ Configuration (tauri.conf.json)
```json
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "General Bots",
"version": "6.2.0",
"identifier": "br.com.pragmatismo.botapp",
"build": {
"devUrl": "http://localhost:3000",
"frontendDist": "../botui/ui/suite"
},
"app": {
"security": {
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
}
}
}
```
---
## 🎯 How App Extensions Work
BotApp injects `js/app-extensions.js` into botui's suite at runtime. This script:
1. Detects Tauri environment (`window.__TAURI__`)
2. Injects app-only navigation items into the suite's `.app-grid`
3. Exposes `window.BotApp` API for native features
Example usage in suite:
```javascript
if (window.BotApp?.isApp) {
// Running in desktop app
const files = await BotApp.fs.listFiles('/home/user');
await BotApp.notify('Title', 'Native notification!');
}
```
---
## ✅ ZERO TOLERANCE POLICY
**EVERY SINGLE WARNING MUST BE FIXED. NO EXCEPTIONS.**
### Absolute Prohibitions
```
❌ NEVER use #![allow()] or #[allow()] in source code
❌ NEVER use _ prefix for unused variables - DELETE or USE them
❌ NEVER use .unwrap() - use ? or proper error handling
❌ NEVER use .expect() - use ? or proper error handling
❌ NEVER use panic!() or unreachable!()
❌ NEVER use todo!() or unimplemented!()
❌ NEVER leave unused imports or dead code
❌ NEVER add comments - code must be self-documenting
```
### Code Patterns
```rust
// ❌ WRONG
let value = something.unwrap();
// ✅ CORRECT
let value = something?;
let value = something.ok_or_else(|| Error::NotFound)?;
// Use Self in Impl Blocks
impl MyStruct {
fn new() -> Self { Self { } } // ✅ Not MyStruct
}
// Derive Eq with PartialEq
#[derive(PartialEq, Eq)] // ✅ Always both
struct MyStruct { }
```
---
## 📦 Key Dependencies
| Library | Version | Purpose |
|---------|---------|---------|
| tauri | 2 | Desktop framework |
| tauri-plugin-dialog | 2 | File dialogs |
| tauri-plugin-opener | 2 | URL/file opener |
| botlib | workspace | Shared types |
| reqwest | 0.12 | HTTP client |
| tokio | 1.41 | Async runtime |
---
## 🧪 Testing and Safety Tooling
BotApp follows General Bots' commitment to code quality and safety.
### Standard Testing
```bash
cargo test
```
### Miri (Undefined Behavior Detection)
Miri detects undefined behavior in unsafe code. Useful for testing data structures and parsing logic.
```bash
cargo +nightly miri test
```
**Limitations:** Cannot test I/O, FFI, or full integration tests.
### AddressSanitizer
Detects memory errors at runtime:
```bash
RUSTFLAGS="-Z sanitizer=address" cargo +nightly test
```
### Kani (Formal Verification)
For mathematically proving critical code properties:
```bash
cargo kani --function critical_function
```
### Ferrocene
Ferrocene is a qualified Rust compiler for safety-critical systems (ISO 26262, IEC 61508).
**Should BotApp use Ferrocene?**
- **For typical desktop deployment:** No - standard Rust + testing is sufficient
- **Consider Ferrocene if:** Deploying in regulated industries (medical, automotive, aerospace)
For most use cases, comprehensive testing with the tools above provides adequate confidence.
---
## 📚 Documentation
For complete documentation, guides, and API references:
- **[docs.pragmatismo.com.br](https://docs.pragmatismo.com.br)** - Full online documentation
- **[BotBook](../botbook)** - Local comprehensive guide with tutorials and examples
- **[Testing & Safety Tooling](../botbook/src/07-gbapp/testing-safety.md)** - Complete testing documentation
---
## 🔑 Remember
- **ZERO WARNINGS** - Every clippy warning must be fixed
- **NO ALLOW IN CODE** - Never use #[allow()] in source files
- **NO DEAD CODE** - Delete unused code
- **NO UNWRAP/EXPECT** - Use ? operator
- **Security** - Minimal allowlist, validate ALL inputs
- **Desktop-only features** - Shared logic in botserver
- **Tauri APIs** - No direct fs access from JS
- **Official icons** - Use icons from botui/ui/suite/assets/icons/
- **Version 6.2.0** - Do not change without approval
---
## 🔗 Related Projects
- [botui](https://github.com/GeneralBots/botui) - Pure web UI
- [botserver](https://github.com/GeneralBots/botserver) - Backend server
- [botlib](https://github.com/GeneralBots/botlib) - Shared Rust library
---
## License
AGPL-3.0 - See [LICENSE](LICENSE) for details.

5
botapp/build.rs Normal file
View file

@ -0,0 +1,5 @@
#![allow(clippy::cargo_common_metadata)]
fn main() {
tauri_build::build();
}

File diff suppressed because it is too large Load diff

BIN
botapp/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
botapp/icons/temp.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

184
botapp/js/app-extensions.js Normal file
View file

@ -0,0 +1,184 @@
(function () {
"use strict";
const APP_GUIDES = [
{
id: "local-files",
label: "Local Files",
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
<path d="M12 11v6M9 14h6"/>
</svg>`,
hxGet: "/app/guides/local-files.html",
description: "Access and manage files on your device",
},
{
id: "native-settings",
label: "App Settings",
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>`,
hxGet: "/app/guides/native-settings.html",
description: "Configure desktop app settings",
},
];
const APP_STYLES = `
.app-grid-separator {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0;
margin-top: 0.5rem;
border-top: 1px solid var(--border-color, #e0e0e0);
}
.app-grid-separator span {
font-size: 0.75rem;
color: var(--text-secondary, #666);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.app-item.app-only {
position: relative;
}
.app-item.app-only::after {
content: '';
position: absolute;
top: 4px;
right: 4px;
width: 6px;
height: 6px;
background: var(--accent-color, #4a90d9);
border-radius: 50%;
}
`;
function injectStyles() {
const styleEl = document.createElement("style");
styleEl.id = "botapp-styles";
styleEl.textContent = APP_STYLES;
document.head.appendChild(styleEl);
}
function injectAppGuides() {
const grid = document.querySelector(".app-grid");
if (!grid) {
setTimeout(injectAppGuides, 100);
return;
}
if (document.querySelector(".app-grid-separator")) {
return;
}
const separator = document.createElement("div");
separator.className = "app-grid-separator";
separator.innerHTML = "<span>Desktop Features</span>";
grid.appendChild(separator);
APP_GUIDES.forEach((guide) => {
const item = document.createElement("a");
item.className = "app-item app-only";
item.href = `#${guide.id}`;
item.dataset.section = guide.id;
item.setAttribute("role", "menuitem");
item.setAttribute("aria-label", guide.description || guide.label);
item.setAttribute("hx-get", guide.hxGet);
item.setAttribute("hx-target", "#main-content");
item.setAttribute("hx-push-url", "true");
item.innerHTML = `
<div class="app-icon" aria-hidden="true">${guide.icon}</div>
<span>${guide.label}</span>
`;
grid.appendChild(item);
});
if (window.htmx) {
htmx.process(grid);
}
console.log("[BotApp] App guides injected successfully");
}
function setupTauriEvents() {
if (!window.__TAURI__) {
console.warn("[BotApp] Tauri API not available");
return;
}
const { listen } = window.__TAURI__.event;
listen("upload_progress", (event) => {
const progress = event.payload;
const progressEl = document.getElementById("upload-progress");
if (progressEl) {
progressEl.style.width = `${progress}%`;
progressEl.textContent = `${Math.round(progress)}%`;
}
});
console.log("[BotApp] Tauri event listeners registered");
}
function init() {
console.log("[BotApp] Initializing app extensions...");
injectStyles();
injectAppGuides();
setupTauriEvents();
console.log("[BotApp] App extensions initialized");
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
window.BotApp = {
isApp: true,
version: "6.1.0",
guides: APP_GUIDES,
invoke: async function (cmd, args) {
if (!window.__TAURI__) {
throw new Error("Tauri API not available");
}
return window.__TAURI__.core.invoke(cmd, args);
},
fs: {
listFiles: (path) => window.BotApp.invoke("list_files", { path }),
uploadFile: (srcPath, destPath) =>
window.BotApp.invoke("upload_file", { srcPath, destPath }),
createFolder: (path, name) =>
window.BotApp.invoke("create_folder", { path, name }),
deletePath: (path) => window.BotApp.invoke("delete_path", { path }),
getHomeDir: () => window.BotApp.invoke("get_home_dir"),
},
notify: async function (title, body) {
if (window.__TAURI__?.notification) {
await window.__TAURI__.notification.sendNotification({ title, body });
}
},
openFileDialog: async function (options = {}) {
if (!window.__TAURI__?.dialog) {
throw new Error("Dialog API not available");
}
return window.__TAURI__.dialog.open(options);
},
saveFileDialog: async function (options = {}) {
if (!window.__TAURI__?.dialog) {
throw new Error("Dialog API not available");
}
return window.__TAURI__.dialog.save(options);
},
};
})();

156
botapp/src/desktop/drive.rs Normal file
View file

@ -0,0 +1,156 @@
use serde::{Deserialize, Serialize};
use std::fs;
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use tauri::{Emitter, Window};
#[derive(Debug, Serialize, Deserialize)]
pub struct FileItem {
pub name: String,
pub path: String,
pub is_dir: bool,
pub size: Option<u64>,
}
/// List files in a directory.
///
/// # Errors
/// Returns an error if the path does not exist or cannot be read.
#[tauri::command]
pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
let base_path = Path::new(path);
let mut files = Vec::new();
if !base_path.exists() {
return Err("Path does not exist".into());
}
for entry in fs::read_dir(base_path).map_err(|e| e.to_string())? {
let entry = entry.map_err(|e| e.to_string())?;
let path = entry.path();
let metadata = entry.metadata().ok();
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
let size = metadata.as_ref().map(std::fs::Metadata::len);
let is_dir = metadata.is_some_and(|m| m.is_dir());
files.push(FileItem {
name,
path: path.to_str().unwrap_or("").to_string(),
is_dir,
size,
});
}
files.sort_by(|a, b| {
if a.is_dir && !b.is_dir {
std::cmp::Ordering::Less
} else if !a.is_dir && b.is_dir {
std::cmp::Ordering::Greater
} else {
a.name.to_lowercase().cmp(&b.name.to_lowercase())
}
});
Ok(files)
}
/// Upload a file to the specified destination.
///
/// # Errors
/// Returns an error if the source file is invalid or the copy operation fails.
#[tauri::command]
pub fn upload_file(window: Window, src_path: &str, dest_path: &str) -> Result<(), String> {
let src = PathBuf::from(src_path);
let dest_dir = PathBuf::from(dest_path);
let dest = dest_dir.join(src.file_name().ok_or("Invalid source file")?);
if !dest_dir.exists() {
fs::create_dir_all(&dest_dir).map_err(|e| e.to_string())?;
}
let mut source_file = File::open(&src).map_err(|e| e.to_string())?;
let mut dest_file = File::create(&dest).map_err(|e| e.to_string())?;
let file_size = source_file.metadata().map_err(|e| e.to_string())?.len();
let mut buffer = [0; 8192];
let mut total_read: u64 = 0;
loop {
let bytes_read = source_file.read(&mut buffer).map_err(|e| e.to_string())?;
if bytes_read == 0 {
break;
}
dest_file
.write_all(&buffer[..bytes_read])
.map_err(|e| e.to_string())?;
total_read += bytes_read as u64;
let progress = if file_size > 0 {
(total_read * 100) / file_size
} else {
100
};
window
.emit("upload_progress", progress)
.map_err(|e| e.to_string())?;
}
Ok(())
}
/// Create a new folder at the specified path.
///
/// # Errors
/// Returns an error if the folder already exists or cannot be created.
#[tauri::command]
pub fn create_folder(path: &str, name: &str) -> Result<(), String> {
let full_path = Path::new(path).join(name);
if full_path.exists() {
return Err("Folder already exists".into());
}
fs::create_dir(&full_path).map_err(|e| e.to_string())?;
Ok(())
}
/// Delete a file or folder at the specified path.
///
/// # Errors
/// Returns an error if the path does not exist or the item cannot be deleted.
#[tauri::command]
pub fn delete_path(path: &str) -> Result<(), String> {
let target = Path::new(path);
if !target.exists() {
return Err("Path does not exist".into());
}
if target.is_dir() {
fs::remove_dir_all(target).map_err(|e| e.to_string())?;
} else {
fs::remove_file(target).map_err(|e| e.to_string())?;
}
Ok(())
}
/// Get the user's home directory path.
///
/// # Errors
/// Returns an error if the home directory cannot be determined.
#[tauri::command]
pub fn get_home_dir() -> Result<String, String> {
dirs::home_dir()
.and_then(|p| p.to_str().map(String::from))
.ok_or_else(|| "Could not determine home directory".into())
}

View file

@ -0,0 +1,4 @@
pub mod drive;
pub mod safe_command;
pub mod sync;
pub mod tray;

View file

@ -0,0 +1,209 @@
use std::collections::HashSet;
use std::path::PathBuf;
use std::process::{Child, Command, Output, Stdio};
use std::sync::LazyLock;
static ALLOWED_COMMANDS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
HashSet::from([
"rclone",
"notify-send",
"osascript",
])
});
static FORBIDDEN_SHELL_CHARS: LazyLock<HashSet<char>> = LazyLock::new(|| {
HashSet::from([
';', '|', '&', '$', '`', '(', ')', '{', '}', '<', '>', '\n', '\r', '\0',
])
});
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SafeCommandError {
CommandNotAllowed(String),
InvalidArgument(String),
ExecutionFailed(String),
ShellInjectionAttempt(String),
}
impl std::fmt::Display for SafeCommandError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CommandNotAllowed(cmd) => write!(f, "Command not in allowlist: {cmd}"),
Self::InvalidArgument(arg) => write!(f, "Invalid argument: {arg}"),
Self::ExecutionFailed(msg) => write!(f, "Command execution failed: {msg}"),
Self::ShellInjectionAttempt(input) => {
write!(f, "Shell injection attempt detected: {input}")
}
}
}
}
impl std::error::Error for SafeCommandError {}
pub struct SafeCommand {
command: String,
args: Vec<String>,
working_dir: Option<PathBuf>,
stdout: Option<Stdio>,
stderr: Option<Stdio>,
}
impl SafeCommand {
pub fn new(command: &str) -> Result<Self, SafeCommandError> {
let cmd_name = std::path::Path::new(command)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(command);
if !ALLOWED_COMMANDS.contains(cmd_name) {
return Err(SafeCommandError::CommandNotAllowed(command.to_string()));
}
Ok(Self {
command: command.to_string(),
args: Vec::new(),
working_dir: None,
stdout: None,
stderr: None,
})
}
pub fn arg(mut self, arg: &str) -> Result<Self, SafeCommandError> {
validate_argument(arg)?;
self.args.push(arg.to_string());
Ok(self)
}
#[must_use]
pub fn stdout(mut self, stdout: Stdio) -> Self {
self.stdout = Some(stdout);
self
}
#[must_use]
pub fn stderr(mut self, stderr: Stdio) -> Self {
self.stderr = Some(stderr);
self
}
pub fn output(self) -> Result<Output, SafeCommandError> {
let mut cmd = Command::new(&self.command);
cmd.args(&self.args);
if let Some(ref dir) = self.working_dir {
cmd.current_dir(dir);
}
if let Some(stdout) = self.stdout {
cmd.stdout(stdout);
}
if let Some(stderr) = self.stderr {
cmd.stderr(stderr);
}
cmd.output()
.map_err(|e| SafeCommandError::ExecutionFailed(e.to_string()))
}
pub fn spawn(self) -> Result<Child, SafeCommandError> {
let mut cmd = Command::new(&self.command);
cmd.args(&self.args);
if let Some(ref dir) = self.working_dir {
cmd.current_dir(dir);
}
if let Some(stdout) = self.stdout {
cmd.stdout(stdout);
}
if let Some(stderr) = self.stderr {
cmd.stderr(stderr);
}
cmd.spawn()
.map_err(|e| SafeCommandError::ExecutionFailed(e.to_string()))
}
}
fn validate_argument(arg: &str) -> Result<(), SafeCommandError> {
if arg.is_empty() {
return Err(SafeCommandError::InvalidArgument(
"Empty argument".to_string(),
));
}
if arg.len() > 4096 {
return Err(SafeCommandError::InvalidArgument(
"Argument too long".to_string(),
));
}
for c in arg.chars() {
if FORBIDDEN_SHELL_CHARS.contains(&c) {
return Err(SafeCommandError::ShellInjectionAttempt(format!(
"Forbidden character '{}' in argument",
c.escape_default()
)));
}
}
let dangerous_patterns = ["$(", "`", "&&", "||", ">>", "<<"];
for pattern in dangerous_patterns {
if arg.contains(pattern) {
return Err(SafeCommandError::ShellInjectionAttempt(format!(
"Dangerous pattern '{pattern}' detected"
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_allowed_command() {
assert!(SafeCommand::new("rclone").is_ok());
assert!(SafeCommand::new("notify-send").is_ok());
assert!(SafeCommand::new("osascript").is_ok());
}
#[test]
fn test_disallowed_command() {
assert!(SafeCommand::new("rm").is_err());
assert!(SafeCommand::new("bash").is_err());
assert!(SafeCommand::new("sh").is_err());
}
#[test]
fn test_valid_arguments() {
let cmd = SafeCommand::new("rclone")
.unwrap()
.arg("sync")
.unwrap()
.arg("/home/user/data")
.unwrap()
.arg("remote:bucket");
assert!(cmd.is_ok());
}
#[test]
fn test_injection_attempts() {
let cmd = SafeCommand::new("rclone").unwrap();
assert!(cmd.arg("; rm -rf /").is_err());
let cmd = SafeCommand::new("rclone").unwrap();
assert!(cmd.arg("$(whoami)").is_err());
let cmd = SafeCommand::new("rclone").unwrap();
assert!(cmd.arg("test`id`").is_err());
let cmd = SafeCommand::new("rclone").unwrap();
assert!(cmd.arg("a && b").is_err());
}
}

384
botapp/src/desktop/sync.rs Normal file
View file

@ -0,0 +1,384 @@
use super::safe_command::SafeCommand;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::process::{Child, Stdio};
use std::sync::Mutex;
use tauri::{Emitter, Window};
static RCLONE_PROCESS: Mutex<Option<Child>> = Mutex::new(None);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncStatus {
pub status: String,
pub is_running: bool,
pub last_sync: Option<String>,
pub files_synced: u64,
pub bytes_transferred: u64,
pub current_file: Option<String>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncConfig {
pub local_path: String,
pub remote_name: String,
pub remote_path: String,
pub sync_mode: SyncMode,
pub exclude_patterns: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SyncMode {
Push,
Pull,
Bisync,
}
impl Default for SyncConfig {
fn default() -> Self {
let local_path = dirs::home_dir().map_or_else(
|| "~/GeneralBots".to_string(),
|p| p.join("GeneralBots").to_string_lossy().to_string(),
);
Self {
local_path,
remote_name: "gbdrive".to_string(),
remote_path: "/".to_string(),
sync_mode: SyncMode::Bisync,
exclude_patterns: vec![
".DS_Store".to_string(),
"Thumbs.db".to_string(),
"*.tmp".to_string(),
".git/**".to_string(),
],
}
}
}
#[tauri::command]
#[must_use]
pub fn get_sync_status() -> SyncStatus {
let process_guard = RCLONE_PROCESS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let is_running = process_guard.is_some();
drop(process_guard);
SyncStatus {
status: if is_running {
"syncing".to_string()
} else {
"idle".to_string()
},
is_running,
last_sync: None,
files_synced: 0,
bytes_transferred: 0,
current_file: None,
error: None,
}
}
#[tauri::command]
pub fn start_sync(window: Window, config: Option<SyncConfig>) -> Result<SyncStatus, String> {
let config = config.unwrap_or_default();
{
let process_guard = RCLONE_PROCESS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if process_guard.is_some() {
return Err("Sync already running".to_string());
}
}
let local_path = PathBuf::from(&config.local_path);
if !local_path.exists() {
std::fs::create_dir_all(&local_path)
.map_err(|e| format!("Failed to create local directory: {e}"))?;
}
let remote_spec = format!("{}:{}", config.remote_name, config.remote_path);
let cmd_result = match config.sync_mode {
SyncMode::Push => SafeCommand::new("rclone")
.and_then(|c| c.arg("sync"))
.and_then(|c| c.arg(&config.local_path))
.and_then(|c| c.arg(&remote_spec)),
SyncMode::Pull => SafeCommand::new("rclone")
.and_then(|c| c.arg("sync"))
.and_then(|c| c.arg(&remote_spec))
.and_then(|c| c.arg(&config.local_path)),
SyncMode::Bisync => SafeCommand::new("rclone")
.and_then(|c| c.arg("bisync"))
.and_then(|c| c.arg(&config.local_path))
.and_then(|c| c.arg(&remote_spec))
.and_then(|c| c.arg("--resync")),
};
let mut cmd_builder = cmd_result
.and_then(|c| c.arg("--progress"))
.and_then(|c| c.arg("--verbose"))
.and_then(|c| c.arg("--checksum"))
.map_err(|e| format!("Failed to build rclone command: {e}"))?;
for pattern in &config.exclude_patterns {
cmd_builder = cmd_builder
.arg("--exclude")
.and_then(|c| c.arg(pattern))
.map_err(|e| format!("Invalid exclude pattern: {e}"))?;
}
let child = cmd_builder
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| {
let err_str = e.to_string();
if err_str.contains("NotFound") || err_str.contains("not found") {
"rclone not found. Please install rclone: https://rclone.org/install/".to_string()
} else {
format!("Failed to start rclone: {e}")
}
})?;
{
let mut process_guard = RCLONE_PROCESS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*process_guard = Some(child);
}
let _ = window.emit("sync_started", ());
std::thread::spawn(move || {
monitor_sync_process(&window);
});
Ok(SyncStatus {
status: "syncing".to_string(),
is_running: true,
last_sync: None,
files_synced: 0,
bytes_transferred: 0,
current_file: None,
error: None,
})
}
#[tauri::command]
pub fn stop_sync() -> Result<SyncStatus, String> {
let mut process_guard = RCLONE_PROCESS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
process_guard
.take()
.ok_or_else(|| "No sync process running".to_string())
.map(|mut child| {
let _ = child.kill();
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = child.wait();
SyncStatus {
status: "stopped".to_string(),
is_running: false,
last_sync: Some(chrono::Utc::now().to_rfc3339()),
files_synced: 0,
bytes_transferred: 0,
current_file: None,
error: None,
}
})
}
#[tauri::command]
pub fn configure_remote(
remote_name: &str,
endpoint: &str,
access_key: &str,
secret_key: &str,
bucket: &str,
) -> Result<(), String> {
let output = SafeCommand::new("rclone")
.and_then(|c| c.arg("config"))
.and_then(|c| c.arg("create"))
.and_then(|c| c.arg(remote_name))
.and_then(|c| c.arg("s3"))
.and_then(|c| c.arg("provider"))
.and_then(|c| c.arg("Minio"))
.and_then(|c| c.arg("endpoint"))
.and_then(|c| c.arg(endpoint))
.and_then(|c| c.arg("access_key_id"))
.and_then(|c| c.arg(access_key))
.and_then(|c| c.arg("secret_access_key"))
.and_then(|c| c.arg(secret_key))
.and_then(|c| c.arg("acl"))
.and_then(|c| c.arg("private"))
.map_err(|e| format!("Failed to build rclone command: {e}"))?
.output()
.map_err(|e| format!("Failed to configure rclone: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("rclone config failed: {stderr}"));
}
let _ = SafeCommand::new("rclone")
.and_then(|c| c.arg("config"))
.and_then(|c| c.arg("update"))
.and_then(|c| c.arg(remote_name))
.and_then(|c| c.arg("bucket"))
.and_then(|c| c.arg(bucket))
.and_then(|c| c.output());
Ok(())
}
#[tauri::command]
pub fn check_rclone_installed() -> Result<String, String> {
let output = SafeCommand::new("rclone")
.and_then(|c| c.arg("version"))
.map_err(|e| format!("Failed to build rclone command: {e}"))?
.output()
.map_err(|e| {
let err_str = e.to_string();
if err_str.contains("NotFound") || err_str.contains("not found") {
"rclone not installed".to_string()
} else {
format!("Error checking rclone: {e}")
}
})?;
if output.status.success() {
let version = String::from_utf8_lossy(&output.stdout);
let first_line = version.lines().next().unwrap_or("unknown");
Ok(first_line.to_string())
} else {
Err("rclone check failed".to_string())
}
}
#[tauri::command]
pub fn list_remotes() -> Result<Vec<String>, String> {
let output = SafeCommand::new("rclone")
.and_then(|c| c.arg("listremotes"))
.map_err(|e| format!("Failed to build rclone command: {e}"))?
.output()
.map_err(|e| format!("Failed to list remotes: {e}"))?;
if output.status.success() {
let remotes = String::from_utf8_lossy(&output.stdout);
Ok(remotes
.lines()
.map(|s| s.trim_end_matches(':').to_string())
.filter(|s| !s.is_empty())
.collect())
} else {
Err("Failed to list rclone remotes".to_string())
}
}
#[tauri::command]
#[must_use]
pub fn get_sync_folder() -> String {
dirs::home_dir().map_or_else(
|| "~/GeneralBots".to_string(),
|p| p.join("GeneralBots").to_string_lossy().to_string(),
)
}
#[tauri::command]
pub fn set_sync_folder(path: &str) -> Result<(), String> {
let path = PathBuf::from(path);
if !path.exists() {
std::fs::create_dir_all(&path).map_err(|e| format!("Failed to create directory: {e}"))?;
}
if !path.is_dir() {
return Err("Path is not a directory".to_string());
}
Ok(())
}
fn monitor_sync_process(window: &Window) {
loop {
std::thread::sleep(std::time::Duration::from_secs(1));
let mut process_guard = RCLONE_PROCESS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let status_opt = if let Some(ref mut child) = *process_guard {
match child.try_wait() {
Ok(Some(exit_status)) => {
let success = exit_status.success();
*process_guard = None;
drop(process_guard);
let status = SyncStatus {
status: if success {
"completed".to_string()
} else {
"error".to_string()
},
is_running: false,
last_sync: Some(chrono::Utc::now().to_rfc3339()),
files_synced: 0,
bytes_transferred: 0,
current_file: None,
error: if success {
None
} else {
Some(format!("Exit code: {:?}", exit_status.code()))
},
};
if success {
let _ = window.emit("sync_completed", &status);
} else {
let _ = window.emit("sync_error", &status);
}
return;
}
Ok(None) => {
drop(process_guard);
Some(SyncStatus {
status: "syncing".to_string(),
is_running: true,
last_sync: None,
files_synced: 0,
bytes_transferred: 0,
current_file: None,
error: None,
})
}
Err(e) => {
*process_guard = None;
drop(process_guard);
let status = SyncStatus {
status: "error".to_string(),
is_running: false,
last_sync: Some(chrono::Utc::now().to_rfc3339()),
files_synced: 0,
bytes_transferred: 0,
current_file: None,
error: Some(format!("Process error: {e}")),
};
let _ = window.emit("sync_error", &status);
return;
}
}
} else {
drop(process_guard);
return;
};
if let Some(status) = status_opt {
let _ = window.emit("sync_progress", &status);
}
}
}

346
botapp/src/desktop/tray.rs Normal file
View file

@ -0,0 +1,346 @@
use anyhow::Result;
use serde::Serialize;
use std::sync::Arc;
use tokio::sync::RwLock;
use tauri::AppHandle;
use tauri::tray::{TrayIcon, TrayIconBuilder};
use tauri::menu::{Menu, MenuItem};
use crate::desktop::safe_command::SafeCommand;
#[derive(Clone)]
pub struct TrayManager {
hostname: Arc<RwLock<Option<String>>>,
running_mode: RunningMode,
tray_active: Arc<RwLock<bool>>,
#[cfg(feature = "desktop-tray")]
tray_handle: Arc<std::sync::Mutex<Option<TrayIcon>>>,
}
impl std::fmt::Debug for TrayManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TrayManager")
.field("hostname", &self.hostname)
.field("running_mode", &self.running_mode)
.field("tray_active", &self.tray_active)
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunningMode {
Server,
Desktop,
Client,
}
#[derive(Debug, Clone, Copy)]
pub enum TrayEvent {
Open,
Settings,
About,
Quit,
}
impl TrayManager {
#[must_use]
pub fn new() -> Self {
Self {
hostname: Arc::new(RwLock::new(None)),
running_mode: RunningMode::Desktop,
tray_active: Arc::new(RwLock::new(false)),
#[cfg(feature = "desktop-tray")]
tray_handle: Arc::new(std::sync::Mutex::new(None)),
}
}
#[must_use]
pub fn with_mode(mode: RunningMode) -> Self {
Self {
hostname: Arc::new(RwLock::new(None)),
running_mode: mode,
tray_active: Arc::new(RwLock::new(false)),
#[cfg(feature = "desktop-tray")]
tray_handle: Arc::new(std::sync::Mutex::new(None)),
}
}
pub async fn start(&self, app: &AppHandle) -> Result<()> {
match self.running_mode {
RunningMode::Desktop => {
self.start_desktop_mode(app).await?;
}
RunningMode::Server => {
log::info!("Running in server mode - tray icon disabled");
}
RunningMode::Client => {
self.start_client_mode(app).await;
}
}
Ok(())
}
pub async fn start_desktop_mode(&self, app: &AppHandle) -> Result<()> {
log::info!("Starting desktop mode tray icon");
let mut active = self.tray_active.write().await;
*active = true;
drop(active);
self.setup_tray(app);
Ok(())
}
fn setup_tray(&self, app: &AppHandle) {
#[cfg(feature = "desktop-tray")]
{
log::info!(
"Initializing unified system tray via tauri::tray for mode: {:?}",
self.running_mode
);
let tray_menu = Menu::new(app).unwrap();
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap();
let _ = tray_menu.append(&quit_i);
// Create a simple red icon
let w = 32;
let h = 32;
let mut rgba = Vec::with_capacity((w * h * 4) as usize);
for _ in 0..(w * h) {
rgba.extend_from_slice(&[255, 0, 0, 255]); // Red
}
let icon = tauri::image::Image::new_owned(rgba, w, h);
let tray_builder = TrayIconBuilder::with_id("main")
.menu(&tray_menu)
.tooltip("General Bots")
.icon(icon);
match tray_builder.build(app) {
Ok(tray) => {
if let Ok(mut handle) = self.tray_handle.lock() {
*handle = Some(tray);
log::info!("Tray icon created successfully");
}
}
Err(e) => {
log::error!("Failed to build tray icon: {}", e);
}
}
}
}
async fn start_client_mode(&self, app: &AppHandle) {
log::info!("Starting client mode with minimal tray");
let mut active = self.tray_active.write().await;
*active = true;
drop(active);
self.setup_tray(app);
}
#[must_use]
pub fn get_mode_string(&self) -> String {
match self.running_mode {
RunningMode::Desktop => "Desktop".to_string(),
RunningMode::Server => "Server".to_string(),
RunningMode::Client => "Client".to_string(),
}
}
pub async fn update_status(&self, status: &str) -> Result<()> {
let active = self.tray_active.read().await;
let is_active = *active;
drop(active);
if is_active {
log::info!("Tray status: {status}");
}
Ok(())
}
pub async fn set_tooltip(&self, tooltip: &str) -> Result<()> {
let active = self.tray_active.read().await;
let is_active = *active;
drop(active);
if is_active {
log::debug!("Tray tooltip: {tooltip}");
}
Ok(())
}
pub async fn show_notification(&self, title: &str, body: &str) -> Result<()> {
let active = self.tray_active.read().await;
let is_active = *active;
drop(active);
if is_active {
log::info!("Notification: {title} - {body}");
#[cfg(target_os = "linux")]
{
if let Ok(cmd) = SafeCommand::new("notify-send")
.and_then(|c| c.arg(title))
.and_then(|c| c.arg(body))
{
let _ = cmd.spawn();
}
}
#[cfg(target_os = "macos")]
{
let script = format!("display notification \"{body}\" with title \"{title}\"");
if let Ok(cmd) = SafeCommand::new("osascript")
.and_then(|c| c.arg("-e"))
.and_then(|c| c.arg(&script))
{
let _ = cmd.spawn();
}
}
}
Ok(())
}
pub async fn get_hostname(&self) -> Option<String> {
let hostname = self.hostname.read().await;
hostname.clone()
}
pub async fn set_hostname(&self, new_hostname: String) {
let mut hostname = self.hostname.write().await;
*hostname = Some(new_hostname);
}
pub async fn stop(&self) {
let mut active = self.tray_active.write().await;
*active = false;
drop(active);
log::info!("Tray manager stopped");
}
pub async fn is_active(&self) -> bool {
let active = self.tray_active.read().await;
let result = *active;
drop(active);
result
}
pub fn handle_event(&self, event: TrayEvent) {
let mode = self.get_mode_string();
match event {
TrayEvent::Open => {
log::info!("Tray event: Open main window (mode: {mode})");
}
TrayEvent::Settings => {
log::info!("Tray event: Open settings (mode: {mode})");
}
TrayEvent::About => {
log::info!("Tray event: Show about dialog (mode: {mode})");
}
TrayEvent::Quit => {
log::info!("Tray event: Quit application (mode: {mode})");
}
}
}
}
impl Default for TrayManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct ServiceMonitor {
services: Vec<ServiceStatus>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ServiceStatus {
pub name: String,
pub running: bool,
pub port: u16,
pub url: String,
}
impl ServiceMonitor {
#[must_use]
pub fn new() -> Self {
Self {
services: vec![
ServiceStatus {
name: "API".to_string(),
running: false,
port: 8080,
url: "http://localhost:9000".to_string(),
},
ServiceStatus {
name: "UI".to_string(),
running: false,
port: 3000,
url: "http://localhost:3000".to_string(),
},
],
}
}
pub fn add_service(&mut self, name: &str, port: u16) {
self.services.push(ServiceStatus {
name: name.to_string(),
running: false,
port,
url: format!("http://localhost:{port}"),
});
}
pub async fn check_services(&mut self) -> Vec<ServiceStatus> {
for service in &mut self.services {
service.running = Self::check_service(&service.url).await;
}
self.services.clone()
}
pub async fn check_service(url: &str) -> bool {
if !url.starts_with("http://") && !url.starts_with("https://") {
return false;
}
let Ok(client) = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(2))
.build()
else {
return false;
};
let health_url = format!("{}/health", url.trim_end_matches('/'));
client
.get(&health_url)
.send()
.await
.is_ok_and(|response| response.status().is_success())
}
#[must_use]
pub fn get_service(&self, name: &str) -> Option<&ServiceStatus> {
self.services.iter().find(|s| s.name == name)
}
#[must_use]
pub fn all_running(&self) -> bool {
self.services.iter().all(|s| s.running)
}
#[must_use]
pub fn any_running(&self) -> bool {
self.services.iter().any(|s| s.running)
}
}
impl Default for ServiceMonitor {
fn default() -> Self {
Self::new()
}
}

2
botapp/src/lib.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod desktop;

236
botapp/src/main.rs Normal file
View file

@ -0,0 +1,236 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use log::info;
use tauri::Manager;
mod desktop;
use desktop::tray::{RunningMode, ServiceMonitor, TrayEvent, TrayManager};
#[tauri::command]
async fn get_tray_status(tray: tauri::State<'_, TrayManager>) -> Result<bool, String> {
Ok(tray.is_active().await)
}
#[tauri::command]
async fn start_tray(
tray: tauri::State<'_, TrayManager>,
app: tauri::AppHandle,
) -> Result<(), String> {
tray.start(&app).await.map_err(|e| e.to_string())
}
#[tauri::command]
async fn stop_tray(tray: tauri::State<'_, TrayManager>) -> Result<(), String> {
tray.stop().await;
Ok(())
}
#[tauri::command]
async fn show_notification(
tray: tauri::State<'_, TrayManager>,
title: String,
body: String,
) -> Result<(), String> {
tray.show_notification(&title, &body)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn update_tray_status(
tray: tauri::State<'_, TrayManager>,
status: String,
) -> Result<(), String> {
tray.update_status(&status).await.map_err(|e| e.to_string())
}
#[tauri::command]
async fn set_tray_tooltip(
tray: tauri::State<'_, TrayManager>,
tooltip: String,
) -> Result<(), String> {
tray.set_tooltip(&tooltip).await.map_err(|e| e.to_string())
}
#[tauri::command]
async fn get_tray_hostname(tray: tauri::State<'_, TrayManager>) -> Result<Option<String>, String> {
Ok(tray.get_hostname().await)
}
#[tauri::command]
async fn set_tray_hostname(
tray: tauri::State<'_, TrayManager>,
hostname: String,
) -> Result<(), String> {
tray.set_hostname(hostname).await;
Ok(())
}
#[tauri::command]
fn handle_tray_event(tray: tauri::State<'_, TrayManager>, event: String) -> Result<(), String> {
let tray_event = match event.as_str() {
"open" => TrayEvent::Open,
"settings" => TrayEvent::Settings,
"about" => TrayEvent::About,
"quit" => TrayEvent::Quit,
_ => return Err(format!("Unknown event: {event}")),
};
tray.handle_event(tray_event);
Ok(())
}
#[tauri::command]
async fn check_services(
monitor: tauri::State<'_, tokio::sync::Mutex<ServiceMonitor>>,
) -> Result<Vec<desktop::tray::ServiceStatus>, String> {
let mut guard = monitor.lock().await;
let result = guard.check_services().await;
drop(guard);
Ok(result)
}
#[tauri::command]
async fn add_service(
monitor: tauri::State<'_, tokio::sync::Mutex<ServiceMonitor>>,
name: String,
port: u16,
) -> Result<(), String> {
let mut guard = monitor.lock().await;
guard.add_service(&name, port);
drop(guard);
Ok(())
}
#[tauri::command]
async fn get_service(
monitor: tauri::State<'_, tokio::sync::Mutex<ServiceMonitor>>,
name: String,
) -> Result<Option<desktop::tray::ServiceStatus>, String> {
let guard = monitor.lock().await;
let result = guard.get_service(&name).cloned();
drop(guard);
Ok(result)
}
#[tauri::command]
async fn all_services_running(
monitor: tauri::State<'_, tokio::sync::Mutex<ServiceMonitor>>,
) -> Result<bool, String> {
let guard = monitor.lock().await;
let result = guard.all_running();
drop(guard);
Ok(result)
}
#[tauri::command]
async fn any_service_running(
monitor: tauri::State<'_, tokio::sync::Mutex<ServiceMonitor>>,
) -> Result<bool, String> {
let guard = monitor.lock().await;
let result = guard.any_running();
drop(guard);
Ok(result)
}
#[tauri::command]
fn get_tray_mode(tray: tauri::State<'_, TrayManager>) -> String {
tray.get_mode_string()
}
#[tauri::command]
fn get_running_modes() -> Vec<&'static str> {
vec!["Server", "Desktop", "Client"]
}
#[tauri::command]
fn create_tray_with_mode(mode: String) -> Result<String, String> {
let running_mode = match mode.to_lowercase().as_str() {
"server" => RunningMode::Server,
"desktop" => RunningMode::Desktop,
"client" => RunningMode::Client,
_ => {
return Err(format!(
"Invalid mode: {mode}. Use Server, Desktop, or Client"
))
}
};
let manager = TrayManager::with_mode(running_mode);
Ok(manager.get_mode_string())
}
fn main() {
botlib::logging::init_compact_logger("info");
let version = env!("CARGO_PKG_VERSION");
info!("BotApp {version} starting...");
let tray_manager = TrayManager::with_mode(RunningMode::Desktop);
let service_monitor = tokio::sync::Mutex::new(ServiceMonitor::new());
let builder_result = tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_opener::init())
.manage(tray_manager)
.manage(service_monitor)
.invoke_handler(tauri::generate_handler![
desktop::drive::list_files,
desktop::drive::upload_file,
desktop::drive::create_folder,
desktop::drive::delete_path,
desktop::drive::get_home_dir,
desktop::sync::get_sync_status,
desktop::sync::start_sync,
desktop::sync::stop_sync,
desktop::sync::configure_remote,
desktop::sync::check_rclone_installed,
desktop::sync::list_remotes,
desktop::sync::get_sync_folder,
desktop::sync::set_sync_folder,
get_tray_status,
start_tray,
stop_tray,
show_notification,
update_tray_status,
set_tray_tooltip,
get_tray_hostname,
set_tray_hostname,
handle_tray_event,
check_services,
add_service,
get_service,
all_services_running,
any_service_running,
get_tray_mode,
get_running_modes,
create_tray_with_mode,
])
.setup(|app| {
let tray = app.state::<TrayManager>();
let mode = tray.get_mode_string();
info!("BotApp setup complete in {mode} mode");
let tray_clone = tray.inner().clone();
let app_handle = app.handle().clone();
std::thread::spawn(move || {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(e) => {
log::error!("Failed to create runtime: {e}");
return;
}
};
rt.block_on(async {
if let Err(e) = tray_clone.start(&app_handle).await {
log::error!("Failed to start tray: {e}");
}
});
});
Ok(())
})
.run(tauri::generate_context!());
if let Err(e) = builder_result {
log::error!("Failed to run BotApp: {e}");
}
}

31
botapp/tauri.conf.json Normal file
View file

@ -0,0 +1,31 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "General Bots",
"version": "6.1.0",
"identifier": "br.com.pragmatismo.botapp",
"build": {
"beforeDevCommand": "",
"beforeBuildCommand": "",
"devUrl": "http://localhost:3000",
"frontendDist": "../botui/ui/suite"
},
"app": {
"security": {
"csp": null
},
"windows": [
{
"title": "General Bots",
"width": 1200,
"height": 800,
"resizable": true,
"fullscreen": false
}
]
},
"bundle": {
"active": true,
"targets": "all",
"icon": []
}
}

View file

@ -0,0 +1,232 @@
<div class="app-guide local-files">
<header class="guide-header">
<h1>📁 Local Files</h1>
<p>
Access and manage files on your device using native file system
access.
</p>
</header>
<section class="file-browser">
<div class="path-bar">
<button
hx-get="/app/api/files/parent"
hx-target="#file-list"
hx-include="#currentPath"
title="Go up one level"
>
⬆️
</button>
<input
type="text"
id="currentPath"
name="path"
placeholder="Enter path..."
hx-get="/app/api/files/list"
hx-target="#file-list"
hx-trigger="keyup[key=='Enter']"
hx-indicator="#loading"
/>
<button
hx-get="/app/api/files/list"
hx-target="#file-list"
hx-include="#currentPath"
hx-indicator="#loading"
>
Browse
</button>
<button
hx-get="/app/api/files/home"
hx-target="#currentPath"
hx-swap="outerHTML"
hx-on::after-request="htmx.trigger('#currentPath', 'keyup', {key: 'Enter'})"
>
🏠 Home
</button>
</div>
<div id="loading" class="htmx-indicator loading">Loading...</div>
<div class="file-list" id="file-list">
<div class="empty-state">
<span class="icon">📂</span>
<p>Click "Home" or enter a path to browse files</p>
</div>
</div>
<div class="file-actions">
<button
hx-get="/app/api/files/new-folder-dialog"
hx-target="#modal-container"
hx-swap="innerHTML"
>
📁 New Folder
</button>
<button
hx-get="/app/api/files/list"
hx-target="#file-list"
hx-include="#currentPath"
hx-indicator="#loading"
>
🔄 Refresh
</button>
</div>
</section>
<div id="modal-container"></div>
<style>
.local-files {
padding: 1.5rem;
max-width: 1200px;
margin: 0 auto;
}
.guide-header {
margin-bottom: 2rem;
}
.guide-header h1 {
font-size: 1.75rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--fg, #333);
}
.guide-header p {
color: var(--fg-muted, #666);
}
.path-bar {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--bg-secondary, #f5f5f5);
border-radius: 8px;
}
.path-bar input {
flex: 1;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border, #ddd);
border-radius: 4px;
font-family: monospace;
background: var(--bg, #fff);
color: var(--fg, #333);
}
.path-bar button {
padding: 0.5rem 1rem;
background: var(--primary, #0066cc);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
}
.path-bar button:hover {
background: var(--primary-hover, #0055aa);
}
.file-list {
border: 1px solid var(--border, #ddd);
border-radius: 8px;
min-height: 300px;
max-height: 500px;
overflow-y: auto;
background: var(--bg, #fff);
}
.file-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border-light, #eee);
cursor: pointer;
transition: background 0.15s;
}
.file-item:last-child {
border-bottom: none;
}
.file-item:hover {
background: var(--bg-hover, #f8f8f8);
}
.file-item.folder {
font-weight: 500;
}
.file-item .icon {
font-size: 1.25rem;
width: 24px;
text-align: center;
}
.file-item .name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-item .size {
color: var(--fg-muted, #888);
font-size: 0.85rem;
}
.file-actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.file-actions button {
padding: 0.5rem 1rem;
background: var(--bg-secondary, #f0f0f0);
color: var(--fg, #333);
border: 1px solid var(--border, #ddd);
border-radius: 4px;
cursor: pointer;
}
.file-actions button:hover {
background: var(--bg-hover, #e8e8e8);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
color: var(--fg-muted, #888);
}
.empty-state .icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
color: var(--fg-muted, #888);
}
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
display: flex;
}
</style>
</div>

View file

@ -0,0 +1,386 @@
<div class="app-guide native-settings">
<header class="guide-header">
<h1></h1>⚙️ App Settings</h1>
<p>Configure native desktop application preferences</p>
</header>
<form
id="settings-form"
hx-post="/app/api/settings/save"
hx-target="#save-status"
hx-swap="innerHTML"
hx-indicator="#saving"
>
<section class="settings-section">
<h2>General</h2>
<div class="setting-item">
<div class="setting-info">
<label for="startOnLogin" class="setting-label"
>Start on login</label
>
<div class="setting-description">
Automatically start General Bots when you log in
</div>
</div>
<div class="setting-control">
<input
type="checkbox"
id="startOnLogin"
name="startOnLogin"
class="toggle-input"
/>
<label for="startOnLogin" class="toggle-switch"></label>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<label for="minimizeToTray" class="setting-label"
>Minimize to tray</label
>
<div class="setting-description">
Keep running in system tray when window is closed
</div>
</div>
<div class="setting-control">
<input
type="checkbox"
id="minimizeToTray"
name="minimizeToTray"
class="toggle-input"
checked
/>
<label for="minimizeToTray" class="toggle-switch"></label>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<label class="setting-label">Default download location</label>
<div class="setting-description">
Where to save downloaded files
</div>
</div>
<div class="setting-control">
<input
type="text"
id="downloadPath"
name="downloadPath"
readonly
placeholder="Select folder..."
/>
<button
type="button"
class="btn btn-secondary"
hx-get="/app/api/settings/select-download-path"
hx-target="#downloadPath"
hx-swap="outerHTML"
>
Browse...
</button>
</div>
</div>
</section>
<section class="settings-section">
<h2>Server Connection</h2>
<div class="setting-item">
<div class="setting-info">
<label for="serverUrl" class="setting-label">Server URL</label>
<div class="setting-description">BotServer API endpoint</div>
</div>
<div class="setting-control">
<input
type="url"
id="serverUrl"
name="serverUrl"
value="http://localhost:8080"
hx-post="/app/api/settings/test-connection"
hx-target="#connection-status"
hx-trigger="blur changed"
hx-indicator="#testing-connection"
/>
<span id="testing-connection" class="htmx-indicator"></span>
<span id="connection-status"></span>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<label for="autoReconnect" class="setting-label"
>Auto-reconnect</label
>
<div class="setting-description">
Automatically reconnect when connection is lost
</div>
</div>
<div class="setting-control">
<input
type="checkbox"
id="autoReconnect"
name="autoReconnect"
class="toggle-input"
checked
/>
<label for="autoReconnect" class="toggle-switch"></label>
</div>
</div>
</section>
<section class="settings-section">
<h2>Notifications</h2>
<div class="setting-item">
<div class="setting-info">
<label for="desktopNotifications" class="setting-label"
>Desktop notifications</label
>
<div class="setting-description">
Show native desktop notifications
</div>
</div>
<div class="setting-control">
<input
type="checkbox"
id="desktopNotifications"
name="desktopNotifications"
class="toggle-input"
checked
/>
<label
for="desktopNotifications"
class="toggle-switch"
></label>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<label for="soundAlerts" class="setting-label"
>Sound alerts</label
>
<div class="setting-description">
Play sound when receiving messages
</div>
</div>
<div class="setting-control">
<input
type="checkbox"
id="soundAlerts"
name="soundAlerts"
class="toggle-input"
/>
<label for="soundAlerts" class="toggle-switch"></label>
</div>
</div>
</section>
<div class="actions">
<button type="submit" class="btn btn-primary">
<span class="btn-text">Save Settings</span>
<span id="saving" class="htmx-indicator">Saving...</span>
</button>
<button
type="button"
class="btn btn-secondary"
hx-get="/app/api/settings/defaults"
hx-target="#settings-form"
hx-swap="outerHTML"
>
Reset to Defaults
</button>
<span id="save-status"></span>
</div>
</form>
<div
class="version-info"
hx-get="/app/api/settings/version"
hx-trigger="load"
hx-swap="innerHTML"
>
<strong>General Bots App</strong><br />
Loading version info...
</div>
<style>
.native-settings {
padding: 2rem;
max-width: 800px;
margin: 0 auto;
}
.guide-header {
margin-bottom: 2rem;
border-bottom: 1px solid var(--border-color, #e0e0e0);
padding-bottom: 1rem;
}
.guide-header h1 {
margin: 0 0 0.5rem 0;
font-size: 1.75rem;
font-weight: 600;
}
.guide-header p {
margin: 0;
color: var(--text-secondary, #666);
}
.settings-section {
margin-bottom: 2rem;
}
.settings-section h2 {
font-size: 1.25rem;
margin-bottom: 1rem;
color: var(--text-primary, #333);
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: var(--bg-secondary, #f5f5f5);
border-radius: 8px;
margin-bottom: 0.75rem;
}
.setting-info {
flex: 1;
}
.setting-label {
font-weight: 500;
margin-bottom: 0.25rem;
display: block;
cursor: pointer;
}
.setting-description {
font-size: 0.875rem;
color: var(--text-secondary, #666);
}
.setting-control {
margin-left: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Toggle switch using CSS only */
.toggle-input {
display: none;
}
.toggle-switch {
position: relative;
width: 48px;
height: 24px;
background: var(--bg-tertiary, #ccc);
border-radius: 12px;
cursor: pointer;
transition: background 0.2s;
}
.toggle-switch::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
transition: transform 0.2s;
}
.toggle-input:checked + .toggle-switch {
background: var(--accent-color, #4caf50);
}
.toggle-input:checked + .toggle-switch::after {
transform: translateX(24px);
}
input[type="text"],
input[type="url"] {
padding: 0.5rem;
border: 1px solid var(--border-color, #ddd);
border-radius: 4px;
font-size: 0.875rem;
min-width: 200px;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
transition: opacity 0.2s;
}
.btn:hover {
opacity: 0.8;
}
.btn-primary {
background: var(--accent-color, #2196f3);
color: white;
}
.btn-secondary {
background: var(--bg-tertiary, #e0e0e0);
color: var(--text-primary, #333);
}
.actions {
display: flex;
gap: 0.5rem;
align-items: center;
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color, #e0e0e0);
}
#save-status {
margin-left: 1rem;
font-size: 0.875rem;
}
.version-info {
margin-top: 2rem;
padding: 1rem;
background: var(--bg-secondary, #f5f5f5);
border-radius: 8px;
font-size: 0.875rem;
color: var(--text-secondary, #666);
}
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
display: inline;
}
.htmx-request .btn-text {
display: none;
}
#connection-status.success {
color: #4caf50;
}
#connection-status.error {
color: #f44336;
}
</style>
</div>

View file

@ -0,0 +1,38 @@
name: GBCI
on:
workflow_dispatch:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
build:
runs-on: gbo
steps:
- name: Disable SSL verification (temporary)
run: git config --global http.sslVerify false
- uses: actions/checkout@v4
- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Install mdBook
run: |
if ! command -v mdbook &> /dev/null; then
cargo install mdbook
fi
- name: Build documentation
run: mdbook build
- name: Deploy documentation
run: |
sudo rm -rf /opt/gbo/bin/proxy/docs.pragmatismo.com.br/*
sudo cp -r ./book/* /opt/gbo/bin/proxy/docs.pragmatismo.com.br/
sudo chmod -R 777 /opt/gbo/bin/proxy/docs.pragmatismo.com.br/

2
botbook/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
book
.env

661
botbook/LICENSE Normal file
View file

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
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.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

279
botbook/PROMPT.md Normal file
View file

@ -0,0 +1,279 @@
# botbook Development Guide
**Version:** 6.2.0
**Purpose:** Documentation for General Bots (mdBook)
---
## CRITICAL: Keyword Naming Rules
**Keywords NEVER use underscores. Always use spaces.**
### Correct Syntax
```basic
SEND MAIL to, subject, body, attachments
GENERATE PDF template, data, output
MERGE PDF files, output
DELETE "url"
ON ERROR RESUME NEXT
SET BOT MEMORY key, value
KB STATISTICS
```
### WRONG - Never Use Underscores
```basic
SEND_MAIL ' WRONG!
GENERATE_PDF ' WRONG!
DELETE_HTTP ' WRONG!
```
### Keyword Mappings
| Write This | NOT This |
|------------|----------|
| `SEND MAIL` | `SEND_MAIL` |
| `GENERATE PDF` | `GENERATE_PDF` |
| `MERGE PDF` | `MERGE_PDF` |
| `DELETE` | `DELETE_HTTP` |
| `SET HEADER` | `SET_HEADER` |
| `FOR EACH` | `FOR_EACH` |
---
## OFFICIAL ICONS - MANDATORY
**NEVER generate icons with LLM. Use official SVG icons from `botui/ui/suite/assets/icons/`**
### Usage in Documentation
```markdown
<!-- Reference icons in docs -->
![Chat](../assets/icons/gb-chat.svg)
<!-- With HTML for sizing -->
<img src="../assets/icons/gb-analytics.svg" alt="Analytics" width="24">
```
---
## STRUCTURE
```
botbook/
├── book.toml # mdBook configuration
├── src/
│ ├── SUMMARY.md # Table of contents
│ ├── README.md # Introduction
│ ├── 01-introduction/ # Quick Start
│ ├── 02-templates/ # Package System
│ ├── 03-knowledge-base/ # Knowledge Base
│ ├── 06-gbdialog/ # BASIC Dialogs
│ ├── 08-config/ # Configuration
│ ├── 10-rest/ # REST API
│ └── assets/ # Images, diagrams
├── i18n/ # Translations
└── book/ # Generated output
```
---
## DOCUMENTATION RULES
```
- All documentation MUST match actual source code
- Extract real keywords from botserver/src/basic/keywords/
- Use actual examples from botserver/templates/
- Version numbers must be 6.2.0
- No placeholder content - only verified features
```
---
## NO ASCII DIAGRAMS - MANDATORY
**NEVER use ASCII art diagrams. ALL diagrams must be SVG.**
### Prohibited ASCII Patterns
```
+-------+
| Box |
+-------+
```
### What to Use Instead
| Instead of... | Use... |
|---------------|--------|
| ASCII box diagrams | SVG diagrams in `assets/` |
| ASCII flow charts | SVG with arrows and boxes |
| ASCII directory trees | Markdown tables |
---
## SVG DIAGRAM GUIDELINES
All SVGs must support light/dark modes:
```xml
<style>
.title-text { fill: #1E1B4B; }
.main-text { fill: #334155; }
@media (prefers-color-scheme: dark) {
.title-text { fill: #F1F5F9; }
.main-text { fill: #E2E8F0; }
}
</style>
```
---
## CONVERSATION EXAMPLES
Use WhatsApp-style HTML format for bot interactions:
```html
<div class="wa-chat">
<div class="wa-message bot">
<div class="wa-bubble">
<p>Hello! How can I help?</p>
<div class="wa-time">10:30</div>
</div>
</div>
<div class="wa-message user">
<div class="wa-bubble">
<p>I want to enroll</p>
<div class="wa-time">10:31</div>
</div>
</div>
</div>
```
---
## SOURCE CODE REFERENCES
| Topic | Source Location |
|-------|-----------------|
| BASIC Keywords | `botserver/src/basic/keywords/` |
| Database Models | `botserver/src/core/shared/models.rs` |
| API Routes | `botserver/src/core/urls.rs` |
| Configuration | `botserver/src/core/config/` |
| Templates | `botserver/templates/` |
---
## BUILDING BOTSERVER
**CRITICAL: ALWAYS USE DEBUG BUILD DURING DEVELOPMENT**
```bash
# CORRECT - Use debug build (FAST)
cargo build
# WRONG - NEVER use --release during development (SLOW)
# cargo build --release # DO NOT USE!
# Run debug server
cargo run
```
**Why Debug Build:**
- Debug builds compile in ~30 seconds
- Release builds take 5-10 minutes with LTO
- Debug builds are sufficient for development and testing
- Only use `--release` for production deployment
---
## BUILDING DOCUMENTATION
```bash
# Install mdBook
cargo install mdbook
# Build documentation
cd botbook && mdbook build
# Serve locally with hot reload
mdbook serve --open
```
---
## TESTING PROCEDURES
### Tool Testing Workflow
**CRITICAL: NO STOP UNTIL NO MORE ERRORS IN TOOLS**
When testing bot tools, follow this sequential process WITHOUT STOPPING:
```
1. Test Tool #1
├─ Fill form one field at a time (if multi-step form)
├─ Verify NO ERRORS in output
├─ Check Result types are NOT visible (no "core::result::Result<..." strings)
├─ Verify database save (if applicable)
├─ IF ERRORS FOUND: FIX THEM IMMEDIATELY, RE-TEST SAME TOOL
├─ ONLY move to next tool when CURRENT tool has ZERO errors
2. Test Tool #2
└─ (repeat process - DO NOT STOP if errors found)
3. Continue until ALL tools tested with ZERO errors
```
**IMPORTANT:**
- Do NOT stop testing to summarize or ask questions
- Do NOT move to next tool if current tool has errors
- Fix errors immediately, rebuild, re-test same tool
- Only proceed when current tool is completely error-free
### Error Patterns to Watch For
**CRITICAL ERRORS (Must Fix Before Proceeding):**
- `core::result::Result<alloc::string::String, alloc::string::String>` in output
- `invalid input syntax for type timestamp`
- `Some("Desculpe, houve um erro...")`
- Empty balloon messages
- Rust debug info visible to users
### Playwright Testing Tricks
```javascript
// Click tool button
await page.getByRole('button', { name: 'Evento/Iluminação' }).click();
// Wait for response
await page.waitForTimeout(3000);
// Take snapshot
await page.snapshot();
// Fill form field by field
await page.getByRole('textbox').fill('field value');
await page.getByRole('textbox').press('Enter');
```
### Test Documentation
After testing each tool, document:
1. Tool name and ID
2. All required parameters
3. Expected behavior
4. Any issues found and fixes applied
5. Final test result (PASS/FAIL)
---
## REMEMBER
- **Accuracy** - Must match botserver source code
- **Completeness** - No placeholder sections
- **Clarity** - Accessible to BASIC enthusiasts
- **Keywords** - NEVER use underscores - always spaces
- **NO ASCII art** - Use SVG diagrams only
- **Version 6.2.0** - Always reference 6.2.0
- **GIT WORKFLOW** - ALWAYS push to ALL repositories (github, pragmatismo)
- **TESTING** - Test tools ONE BY ONE, fix ALL errors before moving to next tool
- **NO STOP** - DO NOT STOP testing until ALL tools have ZERO errors - fix immediately and re-test

406
botbook/README.md Normal file
View file

@ -0,0 +1,406 @@
# General Bots Documentation (BotBook)
**Version:** 6.2.0
**Purpose:** Comprehensive documentation for General Bots (mdBook format)
![General Bots Logo](https://github.com/GeneralBots/botserver/blob/main/logo.png?raw=true)
---
## Overview
BotBook is the official documentation repository for General Bots, built using [mdBook](https://rust-lang.github.io/mdBook/). It provides comprehensive guides, API references, tutorials, and architectural documentation for the entire General Bots platform - an enterprise-grade LLM orchestrator and AI automation platform.
For the latest live documentation, visit **[docs.pragmatismo.com.br](https://docs.pragmatismo.com.br)**.
---
## 🏗️ Architecture
<img src="src/assets/platform-architecture.svg" alt="General Bots Platform Architecture" style="max-width: 100%; height: auto;">
---
## 📦 General Bots Repositories
| Repository | Description | Status |
|------------|-------------|--------|
| [**botserver**](https://github.com/GeneralBots/botserver) | Core API server - LLM orchestration, automation, integrations | ✅ Production |
| [**botui**](https://github.com/GeneralBots/botui) | Pure web UI - HTMX-based interface (suite & minimal) | ✅ Production |
| [**botapp**](https://github.com/GeneralBots/botapp) | Tauri desktop wrapper - native file access, system tray | ✅ Production |
| [**botlib**](https://github.com/GeneralBots/botlib) | Shared Rust library - common types, HTTP client, utilities | ✅ Production |
| [**bottemplates**](https://github.com/GeneralBots/bottemplates) | Templates - bots, apps, prompts, UI components | ✅ Production |
| [**botbook**](https://github.com/GeneralBots/botbook) | Documentation - mdBook format, multi-language | ✅ Production |
---
## 🚀 Quick Start
### Prerequisites
- **Rust** (latest stable) - [Install from rustup.rs](https://rustup.rs/)
- **Git** - [Download from git-scm.com](https://git-scm.com/downloads)
- **mdBook** - `cargo install mdbook`
### Run the Server
```bash
# Clone and run
git clone https://github.com/GeneralBots/botserver
cd botserver
cargo run
```
On first run, botserver automatically:
- Installs required components (PostgreSQL, S3 storage, Cache, LLM)
- Sets up database with migrations
- Downloads AI models
- Starts HTTP server at `http://127.0.0.1:8080`
### Run the Desktop App
```bash
# Clone botui (pure web)
git clone https://github.com/GeneralBots/botui
cd botui
cargo run # Starts web server at :3000
# In another terminal, clone and run botapp (Tauri desktop)
git clone https://github.com/GeneralBots/botapp
cd botapp
cargo tauri dev
```
### Build Documentation
```bash
# Clone botbook
git clone https://github.com/GeneralBots/botbook
cd botbook
# Build documentation
mdbook build
# Serve locally with hot reload
mdbook serve --open
```
---
## ✨ Key Features
### 🤖 Multi-Vendor LLM API
Unified interface for OpenAI, Groq, Claude, Anthropic, and local models.
### 🔧 MCP + LLM Tools Generation
Instant tool creation from code and functions - no complex configurations.
### 💾 Semantic Caching
Intelligent response caching achieving **70% cost reduction** on LLM calls.
### 🌐 Web Automation Engine
Browser automation combined with AI intelligence for complex workflows.
### 📊 Enterprise Data Connectors
Native integrations with CRM, ERP, databases, and external services.
### 🔄 Git-like Version Control
Full history with rollback capabilities for all configurations and data.
---
## 🎯 4 Essential Keywords
General Bots provides a minimal, focused system:
```basic
USE KB "knowledge-base" ' Load knowledge base into vector database
CLEAR KB "knowledge-base" ' Remove KB from session
USE TOOL "tool-name" ' Make tool available to LLM
CLEAR TOOLS ' Remove all tools from session
```
---
## 📁 Documentation Structure
```
botbook/
├── book.toml # mdBook configuration
├── src/
│ ├── SUMMARY.md # Table of contents
│ ├── README.md # Introduction
│ ├── 01-introduction/ # Quick Start
│ ├── 02-templates/ # Package System
│ ├── 03-knowledge-base/ # Knowledge Base
│ ├── 04-gbui/ # UI Interface
│ ├── 06-gbdialog/ # BASIC Dialogs
│ ├── 08-config/ # Configuration
│ ├── 10-rest/ # REST API
│ ├── 12-auth/ # Authentication
│ └── assets/ # Images, diagrams
├── i18n/ # Translations
└── book/ # Generated output
```
---
## 📚 Documentation Writing Guidelines
### ✅ Keyword Naming Rules - MANDATORY
**Keywords NEVER use underscores. Always use spaces.**
| Write This | NOT This |
|------------|----------|
| `SEND MAIL` | `SEND_MAIL` |
| `GENERATE PDF` | `GENERATE_PDF` |
| `MERGE PDF` | `MERGE_PDF` |
| `DELETE` | `DELETE_HTTP` |
| `SET HEADER` | `SET_HEADER` |
| `FOR EACH` | `FOR_EACH` |
#### Correct Syntax Examples
```basic
SEND MAIL to, subject, body, attachments
GENERATE PDF template, data, output
MERGE PDF files, output
DELETE "url"
ON ERROR RESUME NEXT
SET BOT MEMORY key, value
KB STATISTICS
```
#### ❌ NEVER Use Underscores
```basic
SEND_MAIL ' WRONG!
GENERATE_PDF ' WRONG!
DELETE_HTTP ' WRONG!
```
---
### 🎨 Official Icons - MANDATORY
**NEVER generate icons with LLM. Use official SVG icons from `botui/ui/suite/assets/icons/`**
#### Usage in Documentation
```markdown
<!-- Reference icons in docs -->
![Chat](../assets/icons/gb-chat.svg)
<!-- With HTML for sizing -->
<img src="../assets/icons/gb-analytics.svg" alt="Analytics" width="24">
```
#### Required Icons
```
ui/suite/assets/icons/
├── gb-logo.svg # Main GB logo
├── gb-bot.svg # Bot/assistant
├── gb-analytics.svg # Analytics
├── gb-calendar.svg # Calendar
├── gb-chat.svg # Chat
├── gb-drive.svg # File storage
├── gb-mail.svg # Email
├── gb-meet.svg # Video meetings
├── gb-tasks.svg # Task management
└── ...
```
All icons use `stroke="currentColor"` for CSS theming.
---
### 🚫 NO ASCII Diagramrams - MANDATORY
**NEVER use ASCII art diagrams. ALL diagrams must be SVG.**
#### ❌ Prohibited ASCII Patterns
```
┌─────────┐ ╔═══════╗ +-------+
│ Box │ ║ Box ║ | Box |
└─────────┘ ╚═══════╝ +-------+
```
#### ✅ What to Use Instead
| Instead of... | Use... |
|---------------|--------|
| ASCII box diagrams | SVG diagrams in `assets/` |
| ASCII flow charts | SVG with arrows and boxes |
| ASCII directory trees | Markdown tables |
---
### 🎨 SVG Diagram Guidelines
All SVGs must support light/dark modes:
```xml
<style>
.title-text { fill: #1E1B4B; }
.main-text { fill: #334155; }
@media (prefers-color-scheme: dark) {
.title-text { fill: #F1F5F9; }
.main-text { fill: #E2E8F0; }
}
</style>
```
---
### 💬 Conversation Examples
Use WhatsApp-style HTML format for bot interactions:
```html
<div class="wa-chat">
<div class="wa-message bot">
<div class="wa-bubble">
<p>Hello! How can I help?</p>
<div class="wa-time">10:30</div>
</div>
</div>
<div class="wa-message user">
<div class="wa-bubble">
<p>I want to enroll</p>
<div class="wa-time">10:31</div>
</div>
</div>
</div>
```
---
### 📋 Source Code References
| Topic | Source Location |
|-------|-----------------|
| BASIC Keywords | `botserver/src/basic/keywords/` |
| Database Models | `botserver/src/shared/models.rs` |
| API Routes | `botserver/src/core/urls.rs` |
| Configuration | `botserver/src/core/config/` |
| Templates | `botserver/templates/` |
---
### 📖 Documentation Accuracy Rules
```
- All documentation MUST match actual source code
- Extract real keywords from botserver/src/basic/keywords/
- Use actual examples from botserver/templates/
- Version numbers must be 6.2.0
- No placeholder content - only verified features
```
---
## 🏛️ Architecture Details
### botserver (Core)
The main API server handling:
- LLM orchestration and prompt management
- Multi-channel communication (WhatsApp, Teams, Email, Web)
- File storage and drive management
- Task scheduling and automation
- Authentication and authorization
### botui (Web Interface)
Pure web UI with zero native dependencies:
- **Suite**: Full-featured multi-app interface
- **Minimal**: Lightweight single-page chat
- HTMX-powered for minimal JavaScript
- Works in any browser
### botapp (Desktop)
Tauri wrapper adding native capabilities:
- Local file system access
- System tray integration
- Native dialogs and notifications
- Desktop-specific features
### botlib (Shared Library)
Common Rust code shared across projects:
- HTTP client for botserver communication
- Shared types and models
- Branding and configuration utilities
- Error handling
---
## 🛡️ Security
- **AGPL-3.0 License** - True open source with contribution requirements
- **Self-hosted** - Your data stays on your infrastructure
- **Enterprise-grade** - 5+ years of stability
- **No vendor lock-in** - Open protocols and standards
Report security issues to: **security@pragmatismo.com.br**
---
## 🆚 Why General Bots?
| vs. Alternative | General Bots Advantage |
|-----------------|----------------------|
| **ChatGPT/Claude** | Automates entire business processes, not just chat |
| **n8n/Make** | Simpler approach with minimal programming |
| **Microsoft 365** | User control, not locked ecosystems |
| **Salesforce** | Open-source AI orchestration connecting all systems |
---
## 🔗 Links
- **Website:** [pragmatismo.com.br](https://pragmatismo.com.br)
- **Documentation:** [docs.pragmatismo.com.br](https://docs.pragmatismo.com.br)
- **BotBook:** [Complete Documentation](https://github.com/GeneralBots/botbook)
- **Quick Start:** [Get Started in Minutes](https://github.com/GeneralBots/botserver/blob/main/docs/QUICK_START.md)
- **API Reference:** [REST API Documentation](https://github.com/GeneralBots/botserver/blob/main/docs/src/chapter-10-api/README.md)
- **Architecture:** [System Architecture Guide](https://github.com/GeneralBots/botserver/blob/main/docs/src/chapter-07-gbapp/README.md)
- **Stack Overflow:** Tag questions with `generalbots`
- **Video Tutorial:** [7 AI General Bots LLM Templates](https://www.youtube.com/watch?v=KJgvUPXi3Fw)
---
## 🤝 Contributing
We welcome contributions! See our [Contributing Guidelines](https://github.com/GeneralBots/botserver/blob/main/docs/src/chapter-13-community/README.md).
### Contributors
<a href="https://github.com/generalbots/botserver/graphs/contributors">
<img src="https://contrib.rocks/image?repo=generalbots/botserver" />
</a>
---
## 🔑 Remember
- **Accuracy** - Must match botserver source code
- **Completeness** - No placeholder sections
- **Clarity** - Accessible to BASIC enthusiasts
- **Keywords** - NEVER use underscores - always spaces
- **NO ASCII art** - Use SVG diagrams only
- **Official icons** - Use icons from botui/ui/suite/assets/icons/
- **Version 6.2.0** - Always reference 6.2.0
- **GIT WORKFLOW** - ALWAYS push to ALL repositories (github, pragmatismo)
---
## 📄 License
General Bots is licensed under **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.
Copyright (c) pragmatismo.com.br. All rights reserved.
---
> **Code Name:** [Guaribas](https://en.wikipedia.org/wiki/Guaribas) (a city in Brazil, state of Piauí)
>
> *"No one should have to do work that can be done by a machine."* - Roberto Mangabeira Unger

18
botbook/book.toml Normal file
View file

@ -0,0 +1,18 @@
[book]
title = "General Bots Documentation"
authors = ["General Bots Team"]
language = "en"
src = "src"
[build]
build-dir = "book"
[output.html]
default-theme = "light"
preferred-dark-theme = "navy"
smart-punctuation = true
additional-css = ["src/custom.css", "src/whatsapp-chat.css", "src/designer-panel.css"]
additional-js = ["src/theme-sync.js"]

View file

@ -0,0 +1,133 @@
# Chapter 01: Run and Talk
> **⚡ In a hurry?** Jump straight to [Quick Start](./quick-start.md) — you'll be chatting with your bot in 5 minutes.
Get General Bots running and have your first conversation.
---
## What You'll Achieve
By the end of this chapter, you will:
- Have General Bots running on your machine
- Understand what happens during bootstrap
- Complete your first conversation with a bot
- Know how sessions and channels work
---
## Choose Your Path
| If You Want To... | Go To |
|-------------------|-------|
| Get running immediately | [Quick Start](./quick-start.md) |
| Understand the architecture first | [Overview](./overview.md) |
| See all installation options | [Installation](./installation.md) |
| Learn about conversations | [First Conversation](./first-conversation.md) |
| Understand session management | [Sessions and Channels](./sessions.md) |
---
## The 30-Second Version
```bash
./botserver
```
Open `http://localhost:9000`. Start chatting. That's it.
Everything installs automatically on first run—PostgreSQL, storage, cache, and your first bot.
---
## How It Works
<div class="wa-chat">
<div class="wa-message user">
<div class="wa-bubble">
<p>How do I get started with General Bots?</p>
<div class="wa-time">09:00</div>
</div>
</div>
<div class="wa-message bot">
<div class="wa-bubble">
<p>Just three steps:</p>
<p>1⃣ Download <code>botserver</code></p>
<p>2⃣ Run <code>./botserver</code></p>
<p>3⃣ Open your browser to localhost:9000</p>
<p>The bootstrap process handles everything else automatically!</p>
<div class="wa-time">09:00</div>
</div>
</div>
<div class="wa-message user">
<div class="wa-bubble">
<p>What gets installed?</p>
<div class="wa-time">09:01</div>
</div>
</div>
<div class="wa-message bot">
<div class="wa-bubble">
<p>PostgreSQL (database)</p>
<p>MinIO (file storage)</p>
<p>Cache (Redis-compatible)</p>
<p>Default bot templates</p>
<p>All in <code>botserver-stack/</code> — no system-wide installation!</p>
<div class="wa-time">09:01</div>
</div>
</div>
</div>
---
## Topics in This Chapter
### [Overview](./overview.md)
What General Bots does and how it fits together.
### [Quick Start](./quick-start.md)
The fastest path from zero to running bot.
### [Installation](./installation.md)
Detailed setup options including LXC containers and production deployment.
### [First Conversation](./first-conversation.md)
Understanding how the bot responds and learns.
### [Sessions and Channels](./sessions.md)
How conversations are managed across WhatsApp, Web, Telegram, and more.
---
## Coming From Executive Vision?
If you just read the [Executive Vision](../executive-vision.md), here's what to know:
1. **Everything in that feature table?** It's all included in the single `botserver` binary
2. **No configuration needed** — Bootstrap detects your system and sets everything up
3. **Start simple** — Run it, chat with it, then customize
The philosophy is: **get running first, understand later**.
---
## Prerequisites
- **Operating System**: Linux, macOS, or Windows (WSL2 recommended)
- **Disk Space**: ~2GB for botserver-stack
- **RAM**: 4GB minimum, 8GB recommended
- **Ports**: 8080 (web), 5432 (database), 9000 (storage)
No Docker required. No cloud accounts. No API keys to start.
---
## Next Step
**[Quick Start →](./quick-start.md)**
---
<div align="center">
<img src="https://pragmatismo.com.br/icons/general-bots-text.svg" alt="General Bots" width="200">
</div>

View file

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 880 980" xmlns="http://www.w3.org/2000/svg" style="max-width: 100%; height: auto;">
<defs>
<!-- Arrow markers -->
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#2563EB">
</marker>
<!-- Drop shadow for depth -->
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="2">
<feOffset dx="1" dy="1" result="offsetblur">
<feComponentTransfer>
<feFuncA type="linear" slope="0.2">
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic">
</feMerge>
</filter>
</defs>
<!-- Beautiful gradient definitions for depth -->
<!-- White background with subtle border -->
<rect x="0" y="0" width="880" height="980" fill="#F9FAFB" stroke="#9CA3AF" stroke-width="2" rx="8">
<!-- Content container with proper margins -->
<g transform="translate(40, 40)">
<!-- Title -->
<text x="400" y="25" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Auto-Bootstrap Process</text>
<!-- Start: ./botserver (First Run) -->
<rect x="325" y="50" width="150" height="40" fill="none" stroke="#2563EB" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="400" y="75" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#4B5563" filter="url(#textShadow)">./botserver</text>
<!-- Arrow down -->
<line x1="400" y1="90" x2="400" y2="120" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- Detect System -->
<rect x="300" y="120" width="200" height="60" fill="none" stroke="#EA580C" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="400" y="145" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Detect System</text>
<text x="400" y="165" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">(Linux/Mac)</text>
<!-- Arrow down -->
<line x1="400" y1="180" x2="400" y2="210" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- Main Process Container -->
<rect x="50" y="210" width="700" height="280" fill="none" stroke="#2563EB" stroke-width="2" rx="10" stroke-dasharray="5,5" opacity="0.6" filter="url(#shadow)"/>
<text x="400" y="235" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Auto-Bootstrap Process</text>
<!-- Component Grid -->
<g id="components">
<!-- PostgreSQL -->
<rect x="100" y="260" width="140" height="100" fill="none" stroke="#2563EB" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="170" y="285" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">PostgreSQL</text>
<text x="170" y="305" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">16.2</text>
<text x="170" y="325" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">✓ Install</text>
<text x="170" y="340" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">✓ Configure</text>
<text x="170" y="355" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">✓ Migrate</text>
<!-- Valkey -->
<rect x="260" y="260" width="140" height="100" fill="none" stroke="#DC2626" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="330" y="285" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Valkey</text>
<text x="330" y="305" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Cache</text>
<text x="330" y="325" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">✓ Install</text>
<text x="330" y="340" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">✓ Configure</text>
<text x="330" y="355" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">✓ Start</text>
<!-- SeaweedFS -->
<rect x="420" y="260" width="140" height="100" fill="none" stroke="#2563EB" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="490" y="285" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">SeaweedFS</text>
<text x="490" y="305" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Storage</text>
<text x="490" y="325" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">✓ Install</text>
<text x="490" y="340" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">✓ Configure</text>
<text x="490" y="355" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">✓ Start</text>
<!-- Qdrant -->
<rect x="580" y="260" width="140" height="100" fill="none" stroke="#2563EB" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="650" y="285" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Qdrant</text>
<text x="650" y="305" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Vectors</text>
<text x="650" y="325" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">✓ Install</text>
<text x="650" y="340" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">✓ Configure</text>
<text x="650" y="355" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">✓ Start</text>
</g>
<!-- LLM Models -->
<rect x="150" y="380" width="500" height="80" fill="none" stroke="#7C3AED" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="400" y="405" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">LLM Models (Optional)</text>
<text x="400" y="430" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">✓ Download BGE embeddings</text>
<text x="400" y="445" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">✓ Download Llama model (if local)</text>
<!-- Arrow down -->
<line x1="400" y1="490" x2="400" y2="520" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- Generate .env file -->
<rect x="275" y="520" width="250" height="80" fill="none" stroke="#EA580C" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="400" y="545" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Generate .env file</text>
<text x="400" y="565" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">• Secure passwords</text>
<text x="400" y="580" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">• Connection URLs</text>
<text x="400" y="595" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">• Port assignments</text>
<!-- Arrow down -->
<line x1="400" y1="600" x2="400" y2="630" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- Load Templates -->
<rect x="275" y="630" width="250" height="80" fill="none" stroke="#7C3AED" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="400" y="655" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Load Templates</text>
<text x="400" y="675" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">• Scan .gbai dirs</text>
<text x="400" y="690" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">• Create bots</text>
<text x="400" y="705" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">• Index documents</text>
<!-- Arrow down -->
<line x1="400" y1="710" x2="400" y2="740" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- Start Web Server -->
<rect x="275" y="740" width="250" height="60" fill="none" stroke="#059669" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="400" y="765" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Start Web Server</text>
<text x="400" y="785" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">localhost:8080</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 980 980" xmlns="http://www.w3.org/2000/svg" style="max-width: 100%; height: auto;">
<defs>
<!-- Arrow markers -->
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#2563EB">
</marker>
<!-- Drop shadow for depth -->
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="2">
<feOffset dx="1" dy="1" result="offsetblur">
<feComponentTransfer>
<feFuncA type="linear" slope="0.2">
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic">
</feMerge>
</filter>
</defs>
<!-- Beautiful gradient definitions for depth -->
<!-- White background with subtle border -->
<rect x="0" y="0" width="980" height="980" fill="#F9FAFB" stroke="#9CA3AF" stroke-width="2" rx="8">
<!-- Content container with proper margins -->
<g transform="translate(40, 40)">
<!-- Title -->
<text x="450" y="30" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Bootstrap Flow</text>
<!-- Start -->
<rect x="350" y="60" width="200" height="50" fill="none" stroke="#2563EB" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="450" y="85" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#4B5563" filter="url(#textShadow)">./botserver</text>
<text x="450" y="100" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">(first run)</text>
<!-- Arrow -->
<line x1="450" y1="110" x2="450" y2="140" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- OS Detection -->
<rect x="325" y="140" width="250" height="70" fill="none" stroke="#EA580C" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="450" y="165" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">OS Detection</text>
<text x="450" y="185" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Linux/Mac/Windows</text>
<!-- Arrow -->
<line x1="450" y1="210" x2="450" y2="240" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- Component Installation Box -->
<rect x="100" y="240" width="700" height="180" fill="none" stroke="#2563EB" stroke-width="2" rx="10" stroke-dasharray="5,5" opacity="0.6" filter="url(#shadow)"/>
<text x="450" y="265" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Component Installation</text>
<!-- Component Grid -->
<g id="components">
<!-- PostgreSQL -->
<rect x="150" y="290" width="150" height="80" fill="none" stroke="#2563EB" stroke-width="2" rx="6" filter="url(#shadow)"/>
<text x="225" y="315" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">PostgreSQL</text>
<text x="225" y="335" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">16.2</text>
<!-- Valkey -->
<rect x="325" y="290" width="150" height="80" fill="none" stroke="#DC2626" stroke-width="2" rx="6" filter="url(#shadow)"/>
<text x="400" y="315" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Valkey</text>
<text x="400" y="335" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Cache</text>
<!-- SeaweedFS -->
<rect x="500" y="290" width="150" height="80" fill="none" stroke="#2563EB" stroke-width="2" rx="6" filter="url(#shadow)"/>
<text x="575" y="315" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">SeaweedFS</text>
<text x="575" y="335" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Storage</text>
<!-- Qdrant -->
<rect x="675" y="290" width="125" height="80" fill="none" stroke="#2563EB" stroke-width="2" rx="6" filter="url(#shadow)"/>
<text x="737" y="315" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Qdrant</text>
<text x="737" y="335" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Vectors</text>
</g>
<!-- Connection lines -->
<g stroke="#6B7280" stroke-width="2" fill="none" opacity="0.5">
<line x1="225" y1="370" x2="225" y2="390">
<line x1="400" y1="370" x2="400" y2="390">
<line x1="575" y1="370" x2="575" y2="390">
<line x1="737" y1="370" x2="737" y2="390">
<line x1="225" y1="390" x2="737" y2="390">
<line x1="450" y1="390" x2="450" y2="450">
</g>
<!-- Arrow -->
<line x1="450" y1="420" x2="450" y2="450" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- Configuration & Setup -->
<rect x="200" y="450" width="500" height="120" fill="none" stroke="#7C3AED" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="450" y="475" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Configuration &amp; Setup</text>
<g font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563">
<text x="250" y="505" filter="url(#textShadow)">• Generate credentials</text>
<text x="250" y="525" filter="url(#textShadow)">• Create .env file</text>
<text x="480" y="505" filter="url(#textShadow)">• Initialize databases</text>
<text x="480" y="525" filter="url(#textShadow)">• Create storage buckets</text>
</g>
<!-- Arrow -->
<line x1="450" y1="570" x2="450" y2="600" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- Bot Deployment Box -->
<rect x="150" y="600" width="600" height="200" fill="none" stroke="#2563EB" stroke-width="2" rx="10" stroke-dasharray="5,5" opacity="0.6" filter="url(#shadow)"/>
<text x="450" y="625" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Bot Deployment</text>
<!-- Deployment Steps -->
<rect x="200" y="650" width="500" height="40" fill="none" stroke="#2563EB" stroke-width="2" rx="6" filter="url(#shadow)"/>
<text x="450" y="675" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Scan templates/ directory</text>
<!-- Arrow down -->
<line x1="450" y1="690" x2="450" y2="710" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)" opacity="0.7">
<rect x="200" y="710" width="500" height="60" fill="none" stroke="#059669" stroke-width="2" rx="6" filter="url(#shadow)"/>
<text x="450" y="735" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Load .gbai packages</text>
<text x="350" y="755" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">• default.gbai</text>
<text x="500" y="755" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">• announcements.gbai</text>
<!-- Arrow down -->
<line x1="450" y1="770" x2="450" y2="790" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)" opacity="0.7">
<rect x="200" y="790" width="500" height="60" fill="none" stroke="#2563EB" stroke-width="2" rx="6" filter="url(#shadow)"/>
<text x="450" y="815" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Upload to object storage</text>
<text x="450" y="835" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Index documents • Register in database</text>
<!-- Arrow -->
<line x1="450" y1="850" x2="450" y2="880" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- Start Web Server -->
<rect x="275" y="880" width="350" height="70" fill="none" stroke="#059669" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="450" y="905" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Start Web Server</text>
<text x="450" y="925" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">http://localhost:8080</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9 KiB

View file

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 880 680" xmlns="http://www.w3.org/2000/svg" style="max-width: 100%; height: auto;">
<defs>
<!-- Arrow markers -->
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#2563EB">
</marker>
<!-- Drop shadow for depth -->
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="2">
<feOffset dx="1" dy="1" result="offsetblur">
<feComponentTransfer>
<feFuncA type="linear" slope="0.2">
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic">
</feMerge>
</filter>
</defs>
<!-- Beautiful gradient definitions for depth -->
<!-- White background with subtle border -->
<rect x="0" y="0" width="880" height="680" fill="#F9FAFB" stroke="#9CA3AF" stroke-width="2" rx="8">
<!-- Content container with proper margins -->
<g transform="translate(40, 40)">
<!-- Title -->
<text x="400" y="30" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Session Manager Architecture</text>
<!-- User Input -->
<rect x="50" y="60" width="150" height="50" fill="none" stroke="#2563EB" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="125" y="85" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">User Input</text>
<!-- Bot Response -->
<rect x="600" y="60" width="150" height="50" fill="none" stroke="#059669" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="675" y="85" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Bot Response</text>
<!-- Arrows down from User Input and up to Bot Response -->
<line x1="125" y1="110" x2="125" y2="150" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<line x1="675" y1="150" x2="675" y2="110" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- WebSocket/HTTP boxes -->
<rect x="50" y="150" width="150" height="60" fill="none" stroke="#EA580C" stroke-width="2" rx="6" filter="url(#shadow)"/>
<text x="125" y="175" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">WebSocket</text>
<text x="125" y="195" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">/HTTP</text>
<rect x="600" y="150" width="150" height="60" fill="none" stroke="#EA580C" stroke-width="2" rx="6" filter="url(#shadow)"/>
<text x="675" y="175" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">WebSocket</text>
<text x="675" y="195" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">/HTTP</text>
<!-- Arrows to/from Session Manager -->
<line x1="125" y1="210" x2="125" y2="250" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<line x1="675" y1="250" x2="675" y2="210" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- Session Manager Box -->
<rect x="100" y="250" width="600" height="130" fill="none" stroke="#7C3AED" stroke-width="2" rx="10" filter="url(#shadow)"/>
<text x="400" y="280" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Session Manager</text>
<!-- Session Manager Steps -->
<g font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563">
<text x="150" y="310" filter="url(#textShadow)">1. Validate Token</text>
<text x="150" y="330" filter="url(#textShadow)">2. Load Session</text>
<text x="150" y="350" filter="url(#textShadow)">3. Update State</text>
<text x="450" y="310" filter="url(#textShadow)">4. Execute BASIC</text>
<text x="450" y="330" filter="url(#textShadow)">5. Generate Response</text>
<text x="450" y="350" filter="url(#textShadow)">6. Save History</text>
</g>
<!-- Arrows down to databases -->
<line x1="250" y1="380" x2="250" y2="420" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<line x1="550" y1="380" x2="550" y2="420" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- Database boxes -->
<rect x="150" y="420" width="200" height="80" fill="none" stroke="#DC2626" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="250" y="450" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Valkey</text>
<text x="250" y="470" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">(Cache)</text>
<rect x="450" y="420" width="200" height="80" fill="none" stroke="#2563EB" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="550" y="450" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">PostgreSQL</text>
<text x="550" y="470" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">(Persist)</text>
<!-- Sync arrow between databases -->
<path d="M 350 460 L 450 460" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow-double)" stroke-dasharray="5,5" opacity="0.7">
<text x="400" y="455" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Sync Every</text>
<text x="400" y="475" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Message</text>
<!-- Process Flow Numbers -->
<g font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563">
<circle cx="125" cy="130" r="12" fill="none" stroke="#2563EB" stroke-width="2">
<text x="125" y="134" text-anchor="middle" filter="url(#textShadow)">1</text>
<circle cx="125" cy="230" r="12" fill="none" stroke="#2563EB" stroke-width="2">
<text x="125" y="234" text-anchor="middle" filter="url(#textShadow)">2</text>
<circle cx="400" cy="315" r="12" fill="none" stroke="#2563EB" stroke-width="2">
<text x="400" y="319" text-anchor="middle" filter="url(#textShadow)">3</text>
<circle cx="675" cy="230" r="12" fill="none" stroke="#2563EB" stroke-width="2">
<text x="675" y="234" text-anchor="middle" filter="url(#textShadow)">4</text>
<circle cx="675" cy="130" r="12" fill="none" stroke="#2563EB" stroke-width="2">
<text x="675" y="134" text-anchor="middle" filter="url(#textShadow)">5</text>
</g>
<!-- Features box -->
<g id="features" transform="translate(50, 520)">
<rect x="0" y="0" width="700" height="60" fill="none" stroke="#2563EB" stroke-width="2" rx="5" stroke-dasharray="2,2" opacity="0.5" filter="url(#shadow)"/>
<text x="350" y="20" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Key Features</text>
<g font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563">
<text x="50" y="45" filter="url(#textShadow)">• Real-time WebSocket support</text>
<text x="250" y="45" filter="url(#textShadow)">• Automatic session persistence</text>
<text x="450" y="45" filter="url(#textShadow)">• Redis-compatible caching</text>
<text x="600" y="45" filter="url(#textShadow)">• ACID compliance</text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

View file

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 880 680" xmlns="http://www.w3.org/2000/svg" style="max-width: 100%; height: auto;">
<defs>
<!-- Arrow markers -->
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#2563EB">
</marker>
<!-- Drop shadow for depth -->
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="2">
<feOffset dx="1" dy="1" result="offsetblur">
<feComponentTransfer>
<feFuncA type="linear" slope="0.2">
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic">
</feMerge>
</filter>
</defs>
<!-- Beautiful gradient definitions for depth -->
<!-- White background with subtle border -->
<rect x="0" y="0" width="880" height="680" fill="#F9FAFB" stroke="#9CA3AF" stroke-width="2" rx="8">
<!-- Content container with proper margins -->
<g transform="translate(40, 40)">
<!-- Title -->
<text x="300" y="30" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Session State Flow</text>
<!-- Browser Opens -->
<rect x="200" y="60" width="200" height="50" fill="none" stroke="#2563EB" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="300" y="85" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">Browser</text>
<text x="300" y="100" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Opens</text>
<!-- Arrow down -->
<line x1="300" y1="110" x2="300" y2="150" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- CREATE State -->
<rect x="200" y="150" width="200" height="80" fill="none" stroke="#059669" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="300" y="175" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">CREATE</text>
<text x="300" y="195" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">New UUID</text>
<text x="300" y="210" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Token Gen</text>
<!-- Arrow down -->
<line x1="300" y1="230" x2="300" y2="270" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- ACTIVE State -->
<rect x="200" y="270" width="200" height="80" fill="none" stroke="#2563EB" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="300" y="295" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">ACTIVE</text>
<text x="300" y="315" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Chatting</text>
<text x="300" y="330" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Messages</text>
<!-- Arrow down -->
<line x1="300" y1="350" x2="300" y2="390" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- IDLE State -->
<rect x="200" y="390" width="200" height="80" fill="none" stroke="#EA580C" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="300" y="415" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">IDLE</text>
<text x="300" y="435" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">No Input</text>
<text x="300" y="450" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">30min Timer</text>
<!-- User Returns Loop -->
<path d="M 400 430 Q 480 360 480 310 Q 480 260 400 310" stroke="#6B7280" stroke-width="2" fill="none" marker-end="url(#arrow)" stroke-dasharray="5,5" opacity="0.7">
<text x="490" y="360" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">User Returns</text>
<!-- Arrow down -->
<line x1="300" y1="470" x2="300" y2="510" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- EXPIRE State -->
<rect x="200" y="510" width="200" height="80" fill="none" stroke="#DC2626" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="300" y="535" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">EXPIRE</text>
<text x="300" y="555" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">7d Anon</text>
<text x="300" y="570" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Never Auth</text>
<!-- Retry Loop -->
<path d="M 200 550 Q 120 400 120 200 Q 120 150 200 190" stroke="#6B7280" stroke-width="2" fill="none" marker-end="url(#arrow)" stroke-dasharray="5,5" opacity="0.7">
<text x="90" y="360" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Retry</text>
<!-- Arrow down -->
<line x1="300" y1="590" x2="300" y2="630" stroke="#6B7280" stroke-width="2" marker-end="url(#arrow)">
<!-- CLEANUP State -->
<rect x="200" y="630" width="200" height="80" fill="none" stroke="#2563EB" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="300" y="655" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#4B5563" filter="url(#textShadow)">CLEANUP</text>
<text x="300" y="675" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Archive</text>
<text x="300" y="690" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563" filter="url(#textShadow)">Delete</text>
<!-- State Duration Labels -->
<g id="durations" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#4B5563">
<text x="420" y="195" text-anchor="start" filter="url(#textShadow)">Instant</text>
<text x="420" y="315" text-anchor="start" filter="url(#textShadow)">Active use</text>
<text x="420" y="435" text-anchor="start" filter="url(#textShadow)">30 minutes</text>
<text x="420" y="555" text-anchor="start" filter="url(#textShadow)">7 days / Never</text>
<text x="420" y="675" text-anchor="start" filter="url(#textShadow)">Permanent</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

View file

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 900 1000" xmlns="http://www.w3.org/2000/svg" style="max-width: 100%; height: auto;">
<defs>
<!-- Arrow markers -->
<marker id="arrow" markerWidth="12" markerHeight="12" refX="11" refY="6" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,12 L12,6 z" fill="#2563EB"/>
</marker>
<!-- Gradient definitions -->
<linearGradient id="blueGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:0.1" />
<stop offset="100%" style="stop-color:#2563EB;stop-opacity:0.05" />
</linearGradient>
<linearGradient id="greenGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#10B981;stop-opacity:0.1" />
<stop offset="100%" style="stop-color:#059669;stop-opacity:0.05" />
</linearGradient>
<linearGradient id="purpleGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#8B5CF6;stop-opacity:0.1" />
<stop offset="100%" style="stop-color:#7C3AED;stop-opacity:0.05" />
</linearGradient>
<linearGradient id="orangeGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#F97316;stop-opacity:0.1" />
<stop offset="100%" style="stop-color:#EA580C;stop-opacity:0.05" />
</linearGradient>
<linearGradient id="tealGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#06B6D4;stop-opacity:0.1" />
<stop offset="100%" style="stop-color:#0891B2;stop-opacity:0.05" />
</linearGradient>
<!-- Drop shadow -->
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="2" dy="2" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.15"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Background -->
<rect x="0" y="0" width="900" height="1000" fill="#FAFAFA"/>
<!-- Title -->
<text x="450" y="40" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="24" font-weight="700" fill="#1F2937">Tool Execution Flow</text>
<text x="450" y="65" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#6B7280">How the bot understands and executes user commands</text>
<!-- Step 1: User Input -->
<rect x="200" y="100" width="500" height="80" fill="url(#blueGradient)" stroke="#2563EB" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="450" y="130" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="600" fill="#2563EB">1. User Input</text>
<text x="450" y="155" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#1F2937">"I want to enroll in Computer Science"</text>
<!-- Arrow -->
<line x1="450" y1="180" x2="450" y2="220" stroke="#2563EB" stroke-width="3" marker-end="url(#arrow)"/>
<!-- Step 2: LLM Analysis -->
<rect x="200" y="220" width="500" height="100" fill="url(#purpleGradient)" stroke="#7C3AED" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="450" y="250" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="600" fill="#7C3AED">2. LLM Analyzes Intent</text>
<text x="450" y="275" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#1F2937">Detects: "enrollment request"</text>
<text x="450" y="300" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#6B7280">Natural language understanding</text>
<!-- Time indicator -->
<rect x="720" y="255" width="80" height="30" fill="#F3F4F6" rx="15"/>
<text x="760" y="275" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#6B7280">~100ms</text>
<!-- Arrow -->
<line x1="450" y1="320" x2="450" y2="360" stroke="#7C3AED" stroke-width="3" marker-end="url(#arrow)"/>
<!-- Step 3: Scan Available Tools -->
<rect x="150" y="360" width="600" height="120" fill="url(#orangeGradient)" stroke="#EA580C" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="450" y="390" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="600" fill="#EA580C">3. Scan Available Tools</text>
<g transform="translate(250, 415)">
<rect x="0" y="0" width="180" height="40" fill="#059669" fill-opacity="0.1" stroke="#059669" stroke-width="2" rx="6"/>
<text x="90" y="26" text-anchor="middle" font-family="monospace" font-size="16" font-weight="600" fill="#059669">✓ enrollment.bas</text>
</g>
<g transform="translate(470, 415)">
<rect x="0" y="0" width="180" height="40" fill="#6B7280" fill-opacity="0.1" stroke="#6B7280" stroke-width="1" rx="6" stroke-dasharray="4,2"/>
<text x="90" y="26" text-anchor="middle" font-family="monospace" font-size="16" fill="#6B7280">other-tools.bas</text>
</g>
<!-- Time indicator -->
<rect x="720" y="395" width="80" height="30" fill="#F3F4F6" rx="15"/>
<text x="760" y="415" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#6B7280">~50ms</text>
<!-- Arrow -->
<line x1="450" y1="480" x2="450" y2="520" stroke="#EA580C" stroke-width="3" marker-end="url(#arrow)"/>
<!-- Step 4: Collect Parameters -->
<rect x="150" y="520" width="600" height="140" fill="url(#tealGradient)" stroke="#0891B2" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="450" y="550" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="600" fill="#0891B2">4. Collect Parameters</text>
<g transform="translate(200, 570)">
<rect x="0" y="0" width="500" height="70" fill="white" stroke="#E5E7EB" stroke-width="1" rx="6"/>
<text x="20" y="30" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#1F2937">• Name: </text>
<text x="100" y="30" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#6B7280">(ask user)</text>
<text x="20" y="50" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#1F2937">• Email: </text>
<text x="100" y="50" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#6B7280">(ask user)</text>
<text x="250" y="30" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#1F2937">• Course: </text>
<text x="340" y="30" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#059669">"Computer Science"</text>
</g>
<!-- Time indicator -->
<rect x="720" y="575" width="100" height="30" fill="#F3F4F6" rx="15"/>
<text x="770" y="595" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#6B7280">Interactive</text>
<!-- Arrow -->
<line x1="450" y1="660" x2="450" y2="700" stroke="#0891B2" stroke-width="3" marker-end="url(#arrow)"/>
<!-- Step 5: Execute Script -->
<rect x="150" y="700" width="600" height="120" fill="url(#greenGradient)" stroke="#059669" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="450" y="730" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="600" fill="#059669">5. Execute enrollment.bas</text>
<g transform="translate(250, 750)">
<circle cx="15" cy="15" r="8" fill="#059669"/>
<path d="M10,15 L13,18 L20,11" stroke="white" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<text x="35" y="20" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#1F2937">Save to enrollments.csv</text>
</g>
<g transform="translate(250, 780)">
<circle cx="15" cy="15" r="8" fill="#059669"/>
<path d="M10,15 L13,18 L20,11" stroke="white" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<text x="35" y="20" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#1F2937">Generate confirmation</text>
</g>
<!-- Time indicator -->
<rect x="720" y="745" width="80" height="30" fill="#F3F4F6" rx="15"/>
<text x="760" y="765" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#6B7280">~200ms</text>
<!-- Arrow -->
<line x1="450" y1="820" x2="450" y2="860" stroke="#059669" stroke-width="3" marker-end="url(#arrow)"/>
<!-- Step 6: Bot Response -->
<rect x="200" y="860" width="500" height="80" fill="#059669" fill-opacity="0.1" stroke="#059669" stroke-width="2" rx="8" filter="url(#shadow)"/>
<text x="450" y="890" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="600" fill="#059669">6. Bot Response</text>
<text x="450" y="915" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#1F2937">"Welcome to Computer Science, John!"</text>
<!-- Time indicator -->
<rect x="720" y="885" width="80" height="30" fill="#F3F4F6" rx="15"/>
<text x="760" y="905" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#6B7280">Instant</text>
</svg>

After

Width:  |  Height:  |  Size: 9.1 KiB

View file

@ -0,0 +1,193 @@
# First Conversation
After botserver starts, you can immediately start chatting with your bot. No programming required!
## Just Start Talking
Open your browser to `http://localhost:9000` and start chatting:
```
You: Hi!
Bot: Hello! How can I help you today?
You: I want to enroll in a course
Bot: I'll help you with enrollment. Let me collect your information...
[Bot automatically calls enrollment.bas tool]
You: What documents do you have?
Bot: [Searches .gbkb/ folders and answers from your documents]
```
That's it! The LLM handles everything automatically.
## How It Works
### Drop Documents in `.gbkb/`
```
mybot.gbai/
mybot.gbkb/
policies/
enrollment-policy.pdf
course-catalog.pdf
faqs/
student-faq.docx
payment-guide.txt
```
The bot automatically indexes all documents, creates vector embeddings, searches when users ask questions, and provides accurate answers from your content.
### Create Tools as `.bas` Files
Create `enrollment.bas`:
```basic
PARAM name AS string LIKE "John Smith" DESCRIPTION "Student full name"
PARAM email AS string LIKE "john@example.com" DESCRIPTION "Contact email"
PARAM course AS string LIKE "Computer Science" DESCRIPTION "Course to enroll in"
DESCRIPTION "Enrollment tool - collects student information and processes enrollment"
SAVE "enrollments.csv", name, email, course, NOW()
TALK "Enrollment complete! Welcome to " + course + ", " + name
```
The LLM automatically discovers this tool, knows when to call it based on user intent, collects required parameters through natural conversation, and executes the tool when all information is gathered.
### The LLM Does Everything Else
You don't need to write IF/THEN logic, intent detection, dialog flows, state machines, or complex routing. The LLM automatically understands user intent, calls appropriate tools, searches knowledge bases, maintains conversation context, and handles follow-up questions naturally.
## Real Example Conversation
With `enrollment.bas` tool and course documents in `.gbkb/`:
```
User: Hello, I'd like to know about your computer science program
Bot: I'd be happy to help you learn about our Computer Science program!
Based on our course catalog, the program offers a comprehensive
4-year curriculum covering software development, algorithms,
databases, and AI. Would you like to know about specific courses
or the enrollment process?
User: Yes, I want to enroll
Bot: Great! I'll help you enroll in the Computer Science program.
Let me collect your information. What's your full name?
User: John Smith
Bot: Thank you, John. What email address should we use for contact?
User: john.smith@gmail.com
Bot: Perfect! I'm enrolling you in Computer Science now...
[Executes enrollment.bas tool]
Enrollment complete! Welcome to Computer Science, John Smith!
You'll receive a confirmation at john.smith@gmail.com.
User: What are the payment options?
Bot: [Searches payment-guide.txt from .gbkb/]
According to our payment guide, we offer three options:
1. Full payment with 10% discount
2. Monthly installments over 12 months
3. Semester-based payments
Which option would you prefer?
```
## The Magic Formula
Documents in `.gbkb/` combined with tools in `.bas` files and the LLM create an intelligent bot. No complex programming needed!
## Creating Your First Bot
Start by creating the directory structure for your bot with folders for dialog scripts, knowledge base documents, and bot configuration. Add your documents to the `.gbkb/` directory including PDFs, Word documents, text files, and Markdown files. Optionally create tools as `.bas` files to handle specific actions like processing forms or calling APIs. Then restart botserver and start chatting. The LLM will answer questions from your documents, call your tools when appropriate, and handle the entire conversation naturally.
```bash
mkdir -p mybot.gbai/mybot.gbdialog
mkdir -p mybot.gbai/mybot.gbkb/docs
mkdir -p mybot.gbai/mybot.gbot
```
Example tool in `mybot.gbdialog/my-tool.bas`:
```basic
PARAM user_name AS string
PARAM request AS string
DESCRIPTION "Handles user requests"
result = CALL "/api/process", user_name, request
TALK "Done! " + result
```
## No Programming Required
Traditional chatbots require complex logic with IF/THEN statements, intent detection, and multi-step dialog management. With botserver, you simply create the tool with parameters and a description, and the LLM handles all the conversation logic automatically.
Traditional approach (don't do this):
```basic
' Complex multi-step dialog
IF intent = "enrollment" THEN
TALK "Let me help you enroll. What's your name?"
HEAR name
TALK "What's your email?"
HEAR email
' ... lots more code ...
ENDIF
```
botserver approach (just create the tool):
```basic
' In enrollment.bas - becomes a tool automatically
PARAM name AS string
PARAM email AS string
DESCRIPTION "Collects enrollment information"
SAVE "enrollments.csv", name, email
TALK "Successfully enrolled " + name
```
## What Can You Build?
A customer support bot uses product manuals in `.gbkb/` and a `create-ticket.bas` tool, allowing the LLM to answer questions and create support tickets automatically.
An HR assistant combines the employee handbook in `.gbkb/` with a `leave-request.bas` tool so the LLM can explain policies and process leave requests.
An education platform stores course materials in `.gbkb/` and provides `enrollment.bas` and `submit-assignment.bas` tools, enabling the LLM to teach content and manage student tasks.
A sales assistant uses product catalogs in `.gbkb/` with a `create-quote.bas` tool, allowing the LLM to answer product questions and generate quotes.
## Advanced Features
### Dynamic Tool Loading
The LLM can load tools based on context. In `start.bas`, you simply specify which knowledge bases to use, and tools in `.gbdialog/` are auto-discovered. The LLM handles the conversation naturally without explicit HEAR statements.
### Multi-Language Support
The LLM handles multiple languages automatically. Users can write in Portuguese, Chinese, or any other language, and the bot responds appropriately in the same language.
### Context Awareness
The LLM maintains conversation context throughout the interaction. If a user starts to enroll but then asks about prerequisites, the bot handles the tangent and can return to the enrollment process afterward.
## Tips for Success
Organize documents clearly by creating folders for policies, products, FAQs, and tutorials within your `.gbkb/` directory. This helps the LLM find relevant information quickly.
Name tools descriptively with names like `enrollment.bas`, `create-ticket.bas`, and `schedule-meeting.bas`. The LLM understands what each tool does from its name and description.
Always add descriptions to tools using the DESCRIPTION keyword. A good description like "This tool processes student enrollment for courses" helps the LLM know when to use the tool.
Let the LLM work without trying to control every aspect of the conversation. Allow it to rephrase responses naturally, handle unexpected questions, and maintain conversation flow on its own.
## Next Steps
The Quick Start guide walks you through building your first bot. The Packages chapter explains the package structure in detail. The Tool Definition documentation covers creating sophisticated tools. The Knowledge Base chapter describes document management and indexing.
Remember: Just add documents and tools, and the LLM does the rest!

View file

@ -0,0 +1,232 @@
# Installation
botserver installs itself automatically through the bootstrap process. Just run the binary.
## Runtime Dependencies
Before running the botserver binary, you must install required system libraries on the **host system**.
### Quick Install (Recommended)
Download and run the dependency installer:
```bash
curl -fsSL https://raw.githubusercontent.com/GeneralBots/botserver/main/scripts/install-dependencies.sh | sudo bash
```
Or if you have the script locally:
```bash
sudo ./scripts/install-dependencies.sh
```
### Manual Install (Debian/Ubuntu)
```bash
sudo apt update
sudo apt install -y \
libpq5 \
libssl3 \
liblzma5 \
zlib1g \
ca-certificates \
curl \
wget \
libabseil-dev \
libclang-dev \
pkg-config
# For container support (LXC)
sudo snap install lxd
sudo lxd init --auto
```
### Manual Install (Fedora/RHEL)
```bash
sudo dnf install -y \
libpq \
openssl-libs \
xz-libs \
zlib \
ca-certificates \
curl \
wget \
lxc
```
> ⚠️ **Common Error**: If you see `error while loading shared libraries: libpq.so.5`, install `libpq5` (Debian/Ubuntu) or `libpq` (Fedora/RHEL).
## System Requirements
| Resource | Minimum | Production |
|----------|---------|------------|
| **OS** | Linux, macOS, Windows | Linux (Ubuntu/Debian) |
| **RAM** | 4GB | 16GB+ |
| **Disk** | 10GB | 100GB SSD |
| **CPU** | 1 core | 2+ cores |
| **GPU** | None | RTX 3060+ (12GB VRAM) for local LLM |
## Quick Start
```bash
./botserver
```
The bootstrap process automatically:
1. Detects your system (OS/architecture)
2. Creates `botserver-stack/` directory structure
3. Downloads PostgreSQL, Drive, Cache, LLM server
4. Initializes database and storage
5. Deploys default bot
6. Starts all services
**First run takes 2-5 minutes.**
## Using Existing Services
If you have existing infrastructure, configure it in your bot's `config.csv`:
```csv
name,value
database-url,postgres://myuser:mypass@myhost:5432/mydb
drive-server,http://my-drive:9000
drive-accesskey,my-access-key
drive-secret,my-secret-key
```
## Default Ports
| Service | Port | Config Key |
|---------|------|------------|
| UI Server | 8080 | `server-port` |
| PostgreSQL | 5432 | `DATABASE_URL` |
| Drive API | 9000 | `DRIVE_SERVER` |
| Drive Console | 9001 | - |
| LLM Server | 8081 | `llm-server-port` |
| Embedding | 8082 | `embedding-url` |
| Cache | 6379 | Internal |
## Verify Installation
```bash
# Check services
./botserver status
# Test database
psql $DATABASE_URL -c "SELECT version();"
# Test LLM
curl http://localhost:8081/v1/models
# Open UI
open http://localhost:9000
```
## Bot Deployment
Bots deploy to object storage (not local filesystem):
```bash
mybot.gbai → creates 'mybot' bucket in drive
```
The `work/` folder is for internal use only.
### S3 Sync for Development
Use S3-compatible tools for local editing:
- **Cyberduck** (GUI)
- **rclone** (CLI)
- **WinSCP** (Windows)
```bash
# rclone sync example
rclone sync ./mybot.gbai drive:mybot --watch
```
Edits sync automatically - changes reload without restart.
## Memory Optimization
For limited RAM systems:
```csv
name,value
llm-server-ctx-size,2048
llm-server-parallel,2
```
Use quantized models (Q3_K_M, Q4_K_M) for smaller memory footprint.
## GPU Setup
For GPU acceleration:
```csv
name,value
llm-server-gpu-layers,35
```
Requires CUDA installed and 12GB+ VRAM.
## Deployment Options
| Method | Use Case | Guide |
|--------|----------|-------|
| **Local** | Development, single instance | This page |
| **Docker** | Production, microservices | [Docker Deployment](../02-architecture-packages/docker-deployment.md) |
| **LXC** | Isolated components, Linux | [Container Deployment](../02-architecture-packages/containers.md) |
| **Brother Mode** | Container managing host containers | See below |
### Container-on-Host (Brother Mode)
You can run `botserver` inside a container (Docker/LXC) while letting it manage other containers directly on the host system. This is useful for CI/CD pipelines or managing "host" deployment from a restricted environment.
**Requirements:**
- Mount host's LXD socket to container
- Run container as privileged (if accessing host devices)
**Docker Run Example:**
```bash
docker run -d \
--name botserver \
--network host \
--privileged \
-v /var/snap/lxd/common/lxd/unix.socket:/var/snap/lxd/common/lxd/unix.socket \
-e VAULT_ADDR="https://127.0.0.1:8200" \
-e VAULT_TOKEN="<your-token>" \
botserver:latest
```
> **Note**: For non-snap LXD, use `-v /var/lib/lxd/unix.socket:/var/lib/lxd/unix.socket` instead.
The installer detects if it is running in a container but needs to manage the host (brother mode) and will configure the host's LXD/LXC environment safely.
> ⚠️ **IMPORTANT**: Container create commands (`botserver install ... --container`) must be run from the **host system**, not inside a container.
### Example: Create Vault and VectorDB
```bash
# Run on HOST system
botserver install vault --container --tenant mycompany
botserver install vector_db --container --tenant mycompany
# Verify containers
lxc list | grep mycompany
```
## Troubleshooting
| Issue | Solution |
|-------|----------|
| Database connection | Check `DATABASE_URL`, verify PostgreSQL running |
| Port conflict | Change port in config or stop conflicting service |
| Memory issues | Reduce `llm-server-ctx-size`, use quantized model |
| GPU not detected | Verify CUDA, set `llm-server-gpu-layers,0` for CPU |
## Next Steps
- [Quick Start Guide](./quick-start.md) - Create your first bot
- [First Conversation](./first-conversation.md) - Test your bot
- [Configuration Reference](../10-configuration-deployment/README.md) - All settings

View file

@ -0,0 +1,342 @@
# Configuring Local Development Access
After bootstrap, botserver services are immediately accessible via **IP addresses** - no configuration required. For those who prefer friendly hostnames, optional DNS setup is also available.
## Zero Configuration: IP Access (Default)
botserver certificates include `127.0.0.1` as a Subject Alternative Name (SAN), so **mTLS works immediately** via IP address without any system changes.
### Service Ports
| Component | Description | IP:Port |
|-----------|-------------|---------|
| api | Main botserver API | `127.0.0.1:8443` (HTTPS) / `127.0.0.1:9000` (HTTP) |
| tables | PostgreSQL database | `127.0.0.1:5432` |
| drive | Object storage (S3-compatible) | `127.0.0.1:9000` |
| cache | Redis cache | `127.0.0.1:6379` |
| vectordb | Vector database | `127.0.0.1:6333` |
| vault | Secrets management | `127.0.0.1:8200` |
| llm | Local LLM server | `127.0.0.1:8081` |
| embedding | Embedding server | `127.0.0.1:8082` |
| directory | Authentication/Identity | `127.0.0.1:8085` |
| email | Email server | `127.0.0.1:25` (SMTP) / `127.0.0.1:993` (IMAP) |
| meet | Video conferencing | `127.0.0.1:7880` |
### Quick Test
```bash
# Test API (no config needed)
curl -k https://127.0.0.1:8443/health
# With mTLS client certificate
curl --cert ./botserver-stack/conf/system/certificates/api/client.crt \
--key ./botserver-stack/conf/system/certificates/api/client.key \
--cacert ./botserver-stack/conf/system/certificates/ca/ca.crt \
https://127.0.0.1:8443/api/v1/status
```
### Code Examples
#### Python
```python
import requests
response = requests.get(
"https://127.0.0.1:8443/api/v1/bots",
cert=("./botserver-stack/conf/system/certificates/api/client.crt",
"./botserver-stack/conf/system/certificates/api/client.key"),
verify="./botserver-stack/conf/system/certificates/ca/ca.crt"
)
print(response.json())
```
#### Node.js
```javascript
const https = require('https');
const fs = require('fs');
const options = {
hostname: '127.0.0.1',
port: 8443,
path: '/api/v1/bots',
method: 'GET',
cert: fs.readFileSync('./botserver-stack/conf/system/certificates/api/client.crt'),
key: fs.readFileSync('./botserver-stack/conf/system/certificates/api/client.key'),
ca: fs.readFileSync('./botserver-stack/conf/system/certificates/ca/ca.crt')
};
https.request(options, (res) => {
res.on('data', (d) => process.stdout.write(d));
}).end();
```
#### Rust
```rust
use reqwest::Certificate;
use std::fs;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cert = fs::read("./botserver-stack/conf/system/certificates/api/client.crt")?;
let key = fs::read("./botserver-stack/conf/system/certificates/api/client.key")?;
let ca = fs::read("./botserver-stack/conf/system/certificates/ca/ca.crt")?;
let identity = reqwest::Identity::from_pem(&[cert, key].concat())?;
let ca_cert = Certificate::from_pem(&ca)?;
let client = reqwest::Client::builder()
.identity(identity)
.add_root_certificate(ca_cert)
.build()?;
let response = client
.get("https://127.0.0.1:8443/api/v1/bots")
.send()
.await?;
println!("{}", response.text().await?);
Ok(())
}
```
### Remote Server Access
If botserver runs on a different machine (e.g., `192.168.1.100`), regenerate certificates with additional IP SANs:
```bash
./botserver regenerate-certs --san-ip 192.168.1.100 --san-ip 10.0.0.50
```
Or configure before bootstrap in `botserver-stack/conf/system/ca-config.toml`:
```toml
[certificates.api]
san_names = ["localhost", "api.botserver.local", "127.0.0.1", "192.168.1.100"]
```
---
## Optional: Hostname Access
For browser access with friendly URLs, configure your system to resolve `*.botserver.local` hostnames.
### Hostname Mapping
| Component | Hostname |
|-----------|----------|
| api | `api.botserver.local` |
| tables | `tables.botserver.local` |
| drive | `drive.botserver.local` |
| cache | `cache.botserver.local` |
| vectordb | `vectordb.botserver.local` |
| vault | `vault.botserver.local` |
| llm | `llm.botserver.local` |
| embedding | `embedding.botserver.local` |
| directory | `directory.botserver.local` |
| email | `email.botserver.local` |
| meet | `meet.botserver.local` |
### Option 1: Edit hosts file (Simplest)
#### Windows
Open Notepad **as Administrator**, edit `C:\Windows\System32\drivers\etc\hosts`:
```
127.0.0.1 botserver.local
127.0.0.1 api.botserver.local
127.0.0.1 tables.botserver.local
127.0.0.1 drive.botserver.local
127.0.0.1 cache.botserver.local
127.0.0.1 vectordb.botserver.local
127.0.0.1 vault.botserver.local
127.0.0.1 llm.botserver.local
127.0.0.1 embedding.botserver.local
127.0.0.1 directory.botserver.local
127.0.0.1 email.botserver.local
127.0.0.1 meet.botserver.local
```
**PowerShell one-liner (run as Administrator):**
```powershell
@"
127.0.0.1 botserver.local
127.0.0.1 api.botserver.local
127.0.0.1 tables.botserver.local
127.0.0.1 drive.botserver.local
127.0.0.1 cache.botserver.local
127.0.0.1 vectordb.botserver.local
127.0.0.1 vault.botserver.local
127.0.0.1 llm.botserver.local
127.0.0.1 embedding.botserver.local
127.0.0.1 directory.botserver.local
127.0.0.1 email.botserver.local
127.0.0.1 meet.botserver.local
"@ | Add-Content C:\Windows\System32\drivers\etc\hosts
```
#### Linux
```bash
sudo tee -a /etc/hosts << 'EOF'
127.0.0.1 botserver.local
127.0.0.1 api.botserver.local
127.0.0.1 tables.botserver.local
127.0.0.1 drive.botserver.local
127.0.0.1 cache.botserver.local
127.0.0.1 vectordb.botserver.local
127.0.0.1 vault.botserver.local
127.0.0.1 llm.botserver.local
127.0.0.1 embedding.botserver.local
127.0.0.1 directory.botserver.local
127.0.0.1 email.botserver.local
127.0.0.1 meet.botserver.local
EOF
```
#### macOS
```bash
sudo tee -a /etc/hosts << 'EOF'
127.0.0.1 botserver.local
127.0.0.1 api.botserver.local
127.0.0.1 tables.botserver.local
127.0.0.1 drive.botserver.local
127.0.0.1 cache.botserver.local
127.0.0.1 vectordb.botserver.local
127.0.0.1 vault.botserver.local
127.0.0.1 llm.botserver.local
127.0.0.1 embedding.botserver.local
127.0.0.1 directory.botserver.local
127.0.0.1 email.botserver.local
127.0.0.1 meet.botserver.local
EOF
# Flush DNS cache
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder
```
### Option 2: Use botserver's CoreDNS
botserver runs CoreDNS on port 53. Point your system to use it as DNS server.
#### Windows
```powershell
# Get active interface
$interface = (Get-NetAdapter | Where-Object {$_.Status -eq "Up"}).Name
# Set DNS servers
Set-DnsClientServerAddress -InterfaceAlias $interface -ServerAddresses ("127.0.0.1","8.8.8.8")
```
#### Linux (systemd-resolved)
```bash
sudo mkdir -p /etc/systemd/resolved.conf.d/
sudo tee /etc/systemd/resolved.conf.d/botserver.conf << 'EOF'
[Resolve]
DNS=127.0.0.1
FallbackDNS=8.8.8.8 8.8.4.4
Domains=~botserver.local
EOF
sudo systemctl restart systemd-resolved
```
#### macOS
```bash
sudo mkdir -p /etc/resolver
sudo tee /etc/resolver/botserver.local << 'EOF'
nameserver 127.0.0.1
EOF
```
This routes only `*.botserver.local` queries to botserver's DNS.
---
## Trusting Self-Signed Certificates
For browser access without warnings, trust the CA certificate:
### Windows
```powershell
Import-Certificate -FilePath ".\botserver-stack\conf\system\certificates\ca\ca.crt" -CertStoreLocation Cert:\LocalMachine\Root
```
### Linux
```bash
# Ubuntu/Debian
sudo cp ./botserver-stack/conf/system/certificates/ca/ca.crt /usr/local/share/ca-certificates/botserver-ca.crt
sudo update-ca-certificates
# Fedora/RHEL
sudo cp ./botserver-stack/conf/system/certificates/ca/ca.crt /etc/pki/ca-trust/source/anchors/botserver-ca.crt
sudo update-ca-trust
```
### macOS
```bash
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ./botserver-stack/conf/system/certificates/ca/ca.crt
```
### Firefox
Firefox uses its own certificate store:
1. **Settings****Privacy & Security** → **View Certificates**
2. **Authorities** → **Import**
3. Select `botserver-stack/conf/system/certificates/ca/ca.crt`
4. Check "Trust this CA to identify websites"
---
## Troubleshooting
### DNS Not Resolving
```bash
# Check CoreDNS is running
./botserver status dns
# Test DNS directly
dig @127.0.0.1 api.botserver.local
```
### macOS `.local` Conflicts
macOS reserves `.local` for Bonjour. Use the `/etc/resolver/` method which doesn't conflict.
### Reverting Changes
```bash
# Remove hosts entries (Linux/macOS)
sudo sed -i '/botserver\.local/d' /etc/hosts
# Remove macOS resolver
sudo rm /etc/resolver/botserver.local
# Reset DNS (Linux)
sudo rm /etc/systemd/resolved.conf.d/botserver.conf
sudo systemctl restart systemd-resolved
# Reset DNS (Windows)
Set-DnsClientServerAddress -InterfaceAlias $interface -ResetServerAddresses
```
---
## Summary
| Access Method | Configuration | Best For |
|---------------|---------------|----------|
| **IP Address** | None | Default - works immediately, scripts, APIs |
| hosts file | Minimal | Browser access with friendly URLs |
| CoreDNS | Low | Development teams, wildcard subdomains |
**Default recommendation:** Use IP addresses for development. No configuration needed.

View file

<
@ -0,0 +1,199 @@
# Overview
botserver is an open-source conversational AI platform built in Rust that enables developers to create, deploy, and manage intelligent bots with minimal configuration. This chapter provides a comprehensive introduction to the platform's architecture, capabilities, and design philosophy.
## Core Philosophy
botserver was designed around five guiding principles that shape every aspect of the platform. Zero Configuration means the system works out of the box with sensible defaults, eliminating lengthy setup processes. The Package-Based approach ensures bots are self-contained in `.gbai` folders that can be copied and deployed anywhere. BASIC Scripting provides simple, accessible programming for conversation flows that non-programmers can understand and modify. Multi-Channel support means you deploy once and run everywhere across Web, WhatsApp, Teams, and other platforms. Knowledge-First design provides built-in document management and semantic search as core capabilities rather than afterthoughts.