Enable LLM feature by default and fix compilation errors

- Add llm to default features in Cargo.toml
- Fix duplicate smart_router module declaration
- Remove unused LLMProvider import and fix unused variable warnings
- Fix move error in enhanced_llm.rs by cloning state separately for each closure
- Improve code formatting and consistency
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-01-28 16:58:14 -03:00
parent a51e3a0758
commit 51c8a53a90
9 changed files with 665 additions and 541 deletions

View file

@ -10,7 +10,7 @@ features = ["database", "i18n"]
[features]
# ===== DEFAULT =====
default = ["chat", "automation", "drive", "tasks", "cache", "directory"]
default = ["chat", "automation", "drive", "tasks", "cache", "directory", "llm"]
# ===== CORE INFRASTRUCTURE (Can be used standalone) =====
scripting = ["dep:rhai"]

315
PROMPT.md
View file

@ -1,315 +0,0 @@
# botserver Development Guide
**Version:** 6.2.0
**Purpose:** Main API server for General Bots (Axum + Diesel + Rhai BASIC + HTMX in botui)
---
## ZERO TOLERANCE POLICY
**EVERY SINGLE WARNING MUST BE FIXED. NO EXCEPTIONS.**
---
## ❌ ABSOLUTE PROHIBITIONS
```
❌ NEVER use #![allow()] or #[allow()] in source code
❌ 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 use approximate constants - use std::f64::consts
❌ NEVER use CDN links - all assets must be local
❌ NEVER add comments - code must be self-documenting
❌ NEVER build SQL queries with format! - use parameterized queries
❌ NEVER pass user input to Command::new() without validation
❌ NEVER log passwords, tokens, API keys, or PII
```
---
## 🔐 SECURITY REQUIREMENTS
### Error Handling - CRITICAL DEBT
**Current Status**: 955 instances of `unwrap()`/`expect()` found in codebase
**Target**: 0 instances in production code (tests excluded)
```rust
// ❌ WRONG - Found 955 times in codebase
let value = something.unwrap();
let value = something.expect("msg");
// ✅ CORRECT - Required replacements
let value = something?;
let value = something.ok_or_else(|| Error::NotFound)?;
let value = something.unwrap_or_default();
let value = something.unwrap_or_else(|e| {
log::error!("Operation failed: {e}");
default_value
});
```
### Performance Issues - CRITICAL DEBT
**Current Status**: 12,973 excessive `clone()`/`to_string()` calls
**Target**: Minimize allocations, use references where possible
```rust
// ❌ WRONG - Excessive allocations
let name = user.name.clone();
let msg = format!("Hello {}", name.to_string());
// ✅ CORRECT - Minimize allocations
let name = &user.name;
let msg = format!("Hello {name}");
// ✅ CORRECT - Use Cow for conditional ownership
use std::borrow::Cow;
fn process_name(name: Cow<str>) -> String {
match name {
Cow::Borrowed(s) => s.to_uppercase(),
Cow::Owned(s) => s.to_uppercase(),
}
}
```
### Rhai Syntax Registration
```rust
// ❌ WRONG
engine.register_custom_syntax([...], false, |...| {...}).unwrap();
// ✅ CORRECT
if let Err(e) = engine.register_custom_syntax([...], false, |...| {...}) {
log::warn!("Failed to register syntax: {e}");
}
```
### Regex Patterns
```rust
// ❌ WRONG
let re = Regex::new(r"pattern").unwrap();
// ✅ CORRECT
static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"pattern").expect("invalid regex")
});
```
### Tokio Runtime
```rust
// ❌ WRONG
let rt = tokio::runtime::Runtime::new().unwrap();
// ✅ CORRECT
let Ok(rt) = tokio::runtime::Runtime::new() else {
return Err("Failed to create runtime".into());
};
```
### SQL Injection Prevention
```rust
// ❌ WRONG
let query = format!("SELECT * FROM {}", table_name);
// ✅ CORRECT - whitelist validation
const ALLOWED_TABLES: &[&str] = &["users", "sessions"];
if !ALLOWED_TABLES.contains(&table_name) {
return Err(Error::InvalidTable);
}
```
### Command Injection Prevention
```rust
// ❌ WRONG
Command::new("tool").arg(user_input).output()?;
// ✅ CORRECT
fn validate_input(s: &str) -> Result<&str, Error> {
if s.chars().all(|c| c.is_alphanumeric() || c == '.') {
Ok(s)
} else {
Err(Error::InvalidInput)
}
}
let safe = validate_input(user_input)?;
Command::new("/usr/bin/tool").arg(safe).output()?;
```
---
## ✅ CODE PATTERNS
### Format Strings - Inline Variables
```rust
// ❌ WRONG
format!("Hello {}", name)
// ✅ CORRECT
format!("Hello {name}")
```
### Self Usage in Impl Blocks
```rust
// ❌ WRONG
impl MyStruct {
fn new() -> MyStruct { MyStruct { } }
}
// ✅ CORRECT
impl MyStruct {
fn new() -> Self { Self { } }
}
```
### Derive Eq with PartialEq
```rust
// ❌ WRONG
#[derive(PartialEq)]
struct MyStruct { }
// ✅ CORRECT
#[derive(PartialEq, Eq)]
struct MyStruct { }
```
### Option Handling
```rust
// ✅ CORRECT
opt.unwrap_or(default)
opt.unwrap_or_else(|| compute_default())
opt.map_or(default, |x| transform(x))
```
### Chrono DateTime
```rust
// ❌ WRONG
date.with_hour(9).unwrap().with_minute(0).unwrap()
// ✅ CORRECT
date.with_hour(9).and_then(|d| d.with_minute(0)).unwrap_or(date)
```
---
## 📁 KEY DIRECTORIES
```
src/
├── core/ # Bootstrap, config, routes
├── basic/ # Rhai BASIC interpreter
│ └── keywords/ # BASIC keyword implementations
├── security/ # Security modules
├── shared/ # Shared types, models
├── tasks/ # AutoTask system (2651 lines - NEEDS REFACTORING)
├── auto_task/ # App generator (2981 lines - NEEDS REFACTORING)
├── drive/ # File operations (1522 lines - NEEDS REFACTORING)
├── learn/ # Learning system (2306 lines - NEEDS REFACTORING)
└── attendance/ # LLM assistance (2053 lines - NEEDS REFACTORING)
```
### Files Requiring Immediate Refactoring
| File | Current Lines | Target |
|------|---------------|--------|
| `auto_task/app_generator.rs` | 2981 | Split into 7 files |
| `tasks/mod.rs` | 2651 | Split into 6 files |
| `learn/mod.rs` | 2306 | Split into 5 files |
| `attendance/llm_assist.rs` | 2053 | Split into 5 files |
| `drive/mod.rs` | 1522 | Split into 4 files |
**See `TODO-refactor1.md` for detailed refactoring plans**
---
## 🗄️ DATABASE STANDARDS
- **TABLES AND INDEXES ONLY** (no views, triggers, functions)
- **JSON columns:** use TEXT with `_json` suffix
- **ORM:** Use diesel - no sqlx
- **Migrations:** Located in `botserver/migrations/`
---
## 🎨 FRONTEND RULES
- **Use HTMX** - minimize JavaScript
- **NO external CDN** - all assets local
- **Server-side rendering** with Askama templates
---
## 📦 KEY DEPENDENCIES
| Library | Version | Purpose |
|---------|---------|---------|
| axum | 0.7.5 | Web framework |
| diesel | 2.1 | PostgreSQL ORM |
| tokio | 1.41 | Async runtime |
| rhai | git | BASIC scripting |
| reqwest | 0.12 | HTTP client |
| serde | 1.0 | Serialization |
| askama | 0.12 | HTML Templates |
---
## 🚀 CI/CD WORKFLOW
When configuring CI/CD pipelines (e.g., Forgejo Actions):
- **Minimal Checkout**: Clone only the root `gb` and the `botlib` submodule. Do NOT recursively clone everything.
- **BotServer Context**: Replace the empty `botserver` directory with the current set of files being tested.
**Example Step:**
```yaml
- name: Setup Workspace
run: |
# 1. Clone only the root workspace configuration
git clone --depth 1 <your-git-repo-url> workspace
# 2. Setup only the necessary dependencies (botlib)
cd workspace
git submodule update --init --depth 1 botlib
cd ..
# 3. Inject current BotServer code
rm -rf workspace/botserver
mv botserver workspace/botserver
```
---
## 🔑 REMEMBER
- **ZERO WARNINGS** - fix every clippy warning
- **ZERO COMMENTS** - no comments, no doc comments
- **NO ALLOW IN CODE** - configure exceptions in Cargo.toml only
- **NO DEAD CODE** - delete unused code
- **NO UNWRAP/EXPECT** - use ? or combinators (955 instances to fix)
- **MINIMIZE CLONES** - avoid excessive allocations (12,973 instances to optimize)
- **PARAMETERIZED SQL** - never format! for queries
- **VALIDATE COMMANDS** - never pass raw user input
- **INLINE FORMAT ARGS** - `format!("{name}")` not `format!("{}", name)`
- **USE SELF** - in impl blocks, use Self not type name
- **FILE SIZE LIMIT** - max 450 lines per file, refactor at 350 lines
- **Version 6.2.0** - do not change without approval
## 🚨 IMMEDIATE ACTION REQUIRED
1. **Replace 955 unwrap()/expect() calls** with proper error handling
2. **Optimize 12,973 clone()/to_string() calls** for performance
3. **Refactor 5 large files** following TODO-refactor1.md
4. **Add missing error handling** in critical paths
5. **Implement proper logging** instead of panicking

514
README.md
View file

@ -1,69 +1,79 @@
# General Bots - Enterprise-Grade LLM Orchestrator
**Version:** 6.2.0
**Purpose:** Main API server for General Bots (Axum + Diesel + Rhai BASIC)
---
![General Bot Logo](https://github.com/GeneralBots/botserver/blob/main/logo.png?raw=true)
**A strongly-typed LLM conversational platform focused on convention over configuration and code-less approaches.**
## Overview
## Quick Links
General Bots is a **self-hosted AI automation platform** and strongly-typed LLM conversational platform focused on convention over configuration and code-less approaches. It serves as the core API server handling LLM orchestration, business logic, database operations, and multi-channel communication.
- **[Getting Started](docs/guides/getting-started.md)** - Installation and first bot
- **[API Reference](docs/api/README.md)** - REST and WebSocket endpoints
- **[BASIC Language](docs/reference/basic-language.md)** - Dialog scripting reference
For comprehensive documentation, see **[docs.pragmatismo.com.br](https://docs.pragmatismo.com.br)** or the **[BotBook](../botbook)** for detailed guides, API references, and tutorials.
## What is General Bots?
---
General Bots is a **self-hosted AI automation platform** that provides:
- **Multi-Vendor LLM API** - Unified interface for OpenAI, Groq, Claude, Anthropic
- **MCP + LLM Tools Generation** - Instant tool creation from code/functions
- **Semantic Caching** - Intelligent response caching (70% cost reduction)
- **Web Automation Engine** - Browser automation + AI intelligence
- **Enterprise Data Connectors** - CRM, ERP, database native integrations
- **Git-like Version Control** - Full history with rollback capabilities
## Quick Start
## 🚀 Quick Start
### Prerequisites
- **Rust** (1.75+) - [Install from rustup.rs](https://rustup.rs/)
- **Git** - [Download from git-scm.com](https://git-scm.com/downloads)
- Mold sudo apt-get install mold
- **Mold** - `sudo apt-get install mold`
### Installation
```bash
git clone https://github.com/GeneralBots/botserver
cd botserver
cargo run
```
cargo install sccache
sudo apt-get install mold # or build from source
On first run, botserver automatically sets up PostgreSQL, S3 storage, Redis cache, and downloads AI models.
The server will be available at `http://localhost:8080`.
## Documentation
```
docs/
├── api/ # API documentation
│ ├── README.md # API overview
│ ├── rest-endpoints.md # HTTP endpoints
│ └── websocket.md # Real-time communication
├── guides/ # How-to guides
│ ├── getting-started.md # Quick start
│ ├── deployment.md # Production setup
│ └── templates.md # Using templates
└── reference/ # Technical reference
├── basic-language.md # BASIC keywords
├── configuration.md # Config options
└── architecture.md # System design
cargo run
```
## Key Features
On first run, botserver automatically:
- Installs required components (PostgreSQL, S3 storage, Redis cache, LLM)
- Sets up database with migrations
- Downloads AI models
- Starts HTTP server at `http://localhost:8088`
### 4 Essential Keywords
### Command-Line Options
```bash
cargo run # Default: console UI + web server
cargo run -- --noconsole # Background service mode
cargo run -- --desktop # Desktop application (Tauri)
cargo run -- --tenant <name> # Specify tenant
cargo run -- --container # LXC container mode
```
---
## ✨ 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
```basic
USE KB "kb-name" ' Load knowledge base into vector database
@ -85,66 +95,347 @@ SET CONTEXT "support" AS "You are a helpful customer support agent."
TALK "Welcome! How can I help you today?"
```
## Command-Line Options
---
```bash
cargo run # Default: console UI + web server
cargo run -- --noconsole # Background service mode
cargo run -- --desktop # Desktop application (Tauri)
cargo run -- --tenant <name> # Specify tenant
cargo run -- --container # LXC container mode
## 📁 Project Structure
```
src/
├── core/ # Bootstrap, config, routes
├── basic/ # Rhai BASIC interpreter
│ └── keywords/ # BASIC keyword implementations
├── security/ # Security modules
│ ├── command_guard.rs # Safe command execution
│ ├── error_sanitizer.rs # Error message sanitization
│ └── sql_guard.rs # SQL injection prevention
├── shared/ # Shared types, models
├── tasks/ # AutoTask system (2651 lines - NEEDS REFACTORING)
├── auto_task/ # App generator (2981 lines - NEEDS REFACTORING)
├── drive/ # File operations (1522 lines - NEEDS REFACTORING)
├── learn/ # Learning system (2306 lines - NEEDS REFACTORING)
└── attendance/ # LLM assistance (2053 lines - NEEDS REFACTORING)
migrations/ # Database migrations
botserver-stack/ # Stack deployment files
```
## Environment Variables
---
Only directory service variables are required:
## ✅ ZERO TOLERANCE POLICY
| Variable | Purpose |
|----------|---------|
| `DIRECTORY_URL` | Zitadel instance URL |
| `DIRECTORY_CLIENT_ID` | OAuth client ID |
| `DIRECTORY_CLIENT_SECRET` | OAuth client secret |
**EVERY SINGLE WARNING MUST BE FIXED. NO EXCEPTIONS.**
All service credentials are managed automatically. See [Configuration](docs/reference/configuration.md) for details.
### Absolute Prohibitions
## Current Status
```
❌ NEVER use #![allow()] or #[allow()] in source code
❌ 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
❌ NEVER use CDN links - all assets must be local
❌ NEVER build SQL queries with format! - use parameterized queries
❌ NEVER pass user input to Command::new() without validation
❌ NEVER log passwords, tokens, API keys, or PII
```
**Version:** 6.0.8
**Build Status:** SUCCESS
**Production Ready:** YES
---
## Deployment
## 🔐 Security Requirements
See [Deployment Guide](docs/guides/deployment.md) for:
### Error Handling - CRITICAL DEBT
- Single server setup
- Docker Compose
- LXC containers
- Kubernetes
- Reverse proxy configuration
**Current Status**: 955 instances of `unwrap()`/`expect()` found in codebase
**Target**: 0 instances in production code (tests excluded)
## Contributing
```rust
// ❌ WRONG - Found 955 times in codebase
let value = something.unwrap();
let value = something.expect("msg");
// ✅ CORRECT - Required replacements
let value = something?;
let value = something.ok_or_else(|| Error::NotFound)?;
let value = something.unwrap_or_default();
let value = something.unwrap_or_else(|e| {
log::error!("Operation failed: {e}");
default_value
});
```
### Performance Issues - CRITICAL DEBT
**Current Status**: 12,973 excessive `clone()`/`to_string()` calls
**Target**: Minimize allocations, use references where possible
```rust
// ❌ WRONG - Excessive allocations
let name = user.name.clone();
let msg = format!("Hello {}", name.to_string());
// ✅ CORRECT - Minimize allocations
let name = &user.name;
let msg = format!("Hello {name}");
// ✅ CORRECT - Use Cow for conditional ownership
use std::borrow::Cow;
fn process_name(name: Cow<str>) -> String {
match name {
Cow::Borrowed(s) => s.to_uppercase(),
Cow::Owned(s) => s.to_uppercase(),
}
}
```
### SQL Injection Prevention
```rust
// ❌ WRONG
let query = format!("SELECT * FROM {}", table_name);
// ✅ CORRECT - whitelist validation
const ALLOWED_TABLES: &[&str] = &["users", "sessions"];
if !ALLOWED_TABLES.contains(&table_name) {
return Err(Error::InvalidTable);
}
```
### Command Injection Prevention
```rust
// ❌ WRONG
Command::new("tool").arg(user_input).output()?;
// ✅ CORRECT - Use SafeCommand
use crate::security::command_guard::SafeCommand;
SafeCommand::new("allowed_command")?
.arg("safe_arg")?
.execute()
```
### Error Responses - Use ErrorSanitizer
```rust
// ❌ WRONG
Json(json!({ "error": e.to_string() }))
format!("Database error: {}", e)
// ✅ CORRECT
use crate::security::error_sanitizer::log_and_sanitize;
let sanitized = log_and_sanitize(&e, "context", None);
(StatusCode::INTERNAL_SERVER_ERROR, sanitized)
```
---
## ✅ Mandatory Code Patterns
### Format Strings - Inline Variables
```rust
// ❌ WRONG
format!("Hello {}", name)
// ✅ CORRECT
format!("Hello {name}")
```
### Self Usage in Impl Blocks
```rust
// ❌ WRONG
impl MyStruct {
fn new() -> MyStruct { MyStruct { } }
}
// ✅ CORRECT
impl MyStruct {
fn new() -> Self { Self { } }
}
```
### Derive Eq with PartialEq
```rust
// ❌ WRONG
#[derive(PartialEq)]
struct MyStruct { }
// ✅ CORRECT
#[derive(PartialEq, Eq)]
struct MyStruct { }
```
### Option Handling
```rust
// ✅ CORRECT
opt.unwrap_or(default)
opt.unwrap_or_else(|| compute_default())
opt.map_or(default, |x| transform(x))
```
### Chrono DateTime
```rust
// ❌ WRONG
date.with_hour(9).unwrap().with_minute(0).unwrap()
// ✅ CORRECT
date.with_hour(9).and_then(|d| d.with_minute(0)).unwrap_or(date)
```
---
## 📏 File Size Limits - MANDATORY
### Maximum 450 Lines Per File
When a file grows beyond this limit:
1. **Identify logical groups** - Find related functions
2. **Create subdirectory module** - e.g., `handlers/`
3. **Split by responsibility:**
- `types.rs` - Structs, enums, type definitions
- `handlers.rs` - HTTP handlers and routes
- `operations.rs` - Core business logic
- `utils.rs` - Helper functions
- `mod.rs` - Re-exports and configuration
4. **Keep files focused** - Single responsibility
5. **Update mod.rs** - Re-export all public items
**NEVER let a single file exceed 450 lines - split proactively at 350 lines**
### Files Requiring Immediate Refactoring
| File | Lines | Target Split |
|------|-------|--------------|
| `auto_task/app_generator.rs` | 2981 | → 7 files |
| `tasks/mod.rs` | 2651 | → 6 files |
| `learn/mod.rs` | 2306 | → 5 files |
| `attendance/llm_assist.rs` | 2053 | → 5 files |
| `drive/mod.rs` | 1522 | → 4 files |
**See `TODO-refactor1.md` for detailed refactoring plans**
---
## 🗄️ Database Standards
- **TABLES AND INDEXES ONLY** (no views, triggers, functions)
- **JSON columns:** use TEXT with `_json` suffix
- **ORM:** Use diesel - no sqlx
- **Migrations:** Located in `botserver/migrations/`
---
## 🎨 Frontend Rules
- **Use HTMX** - minimize JavaScript
- **NO external CDN** - all assets local
- **Server-side rendering** with Askama templates
---
## 📦 Key Dependencies
| Library | Version | Purpose |
|---------|---------|---------|
| axum | 0.7.5 | Web framework |
| diesel | 2.1 | PostgreSQL ORM |
| tokio | 1.41 | Async runtime |
| rhai | git | BASIC scripting |
| reqwest | 0.12 | HTTP client |
| serde | 1.0 | Serialization |
| askama | 0.12 | HTML Templates |
---
## 🚀 CI/CD Workflow
When configuring CI/CD pipelines (e.g., Forgejo Actions):
- **Minimal Checkout**: Clone only the root `gb` and the `botlib` submodule. Do NOT recursively clone everything.
- **BotServer Context**: Replace the empty `botserver` directory with the current set of files being tested.
**Example Step:**
```yaml
- name: Setup Workspace
run: |
# 1. Clone only the root workspace configuration
git clone --depth 1 <your-git-repo-url> workspace
# 2. Setup only the necessary dependencies (botlib)
cd workspace
git submodule update --init --depth 1 botlib
cd ..
# 3. Inject current BotServer code
rm -rf workspace/botserver
mv botserver workspace/botserver
```
---
## 📚 Documentation
### Documentation Structure
```
docs/
├── api/ # API documentation
│ ├── README.md # API overview
│ ├── rest-endpoints.md # HTTP endpoints
│ └── websocket.md # Real-time communication
├── guides/ # How-to guides
│ ├── getting-started.md # Quick start
│ ├── deployment.md # Production setup
│ └── templates.md # Using templates
└── reference/ # Technical reference
├── basic-language.md # BASIC keywords
├── configuration.md # Config options
└── architecture.md # System design
```
### Additional Resources
- **[docs.pragmatismo.com.br](https://docs.pragmatismo.com.br)** - Full online documentation
- **[BotBook](../botbook)** - Local comprehensive guide with tutorials and examples
- **[API Reference](docs/api/README.md)** - REST and WebSocket endpoints
- **[BASIC Language](docs/reference/basic-language.md)** - Dialog scripting reference
---
## 🔗 Related Projects
| Project | Description |
|---------|-------------|
| [botui](https://github.com/GeneralBots/botui) | Pure web UI (HTMX-based) |
| [botapp](https://github.com/GeneralBots/botapp) | Tauri desktop wrapper |
| [botlib](https://github.com/GeneralBots/botlib) | Shared Rust library |
| [botbook](https://github.com/GeneralBots/botbook) | Documentation |
| [bottemplates](https://github.com/GeneralBots/bottemplates) | Templates and examples |
---
## 🛡️ 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**
---
## 🤝 Contributing
We welcome contributions! Please read our contributing guidelines before submitting PRs.
## Security
Security issues should be reported to: **security@pragmatismo.com.br**
## License
General Bot Copyright (c) pragmatismo.com.br. All rights reserved.
Licensed under the **AGPL-3.0**.
According to our dual licensing model, this program can be used either under the terms of the GNU Affero General Public License, version 3, or under a proprietary license.
## Support
- **GitHub Issues:** [github.com/GeneralBots/botserver/issues](https://github.com/GeneralBots/botserver/issues)
- **Stack Overflow:** Tag questions with `generalbots`
- **Video Tutorial:** [7 AI General Bots LLM Templates](https://www.youtube.com/watch?v=KJgvUPXi3Fw)
## Contributors
### Contributors
<a href="https://github.com/generalbots/botserver/graphs/contributors">
<img src="https://contrib.rocks/image?repo=generalbots/botserver" />
@ -152,6 +443,53 @@ According to our dual licensing model, this program can be used either under the
---
## 🔑 Remember
- **ZERO WARNINGS** - Fix every clippy warning
- **ZERO COMMENTS** - No comments, no doc comments
- **NO ALLOW IN CODE** - Configure exceptions in Cargo.toml only
- **NO DEAD CODE** - Delete unused code
- **NO UNWRAP/EXPECT** - Use ? or combinators (955 instances to fix)
- **MINIMIZE CLONES** - Avoid excessive allocations (12,973 instances to optimize)
- **PARAMETERIZED SQL** - Never format! for queries
- **VALIDATE COMMANDS** - Never pass raw user input
- **INLINE FORMAT ARGS** - `format!("{name}")` not `format!("{}", name)`
- **USE SELF** - In impl blocks, use Self not type name
- **FILE SIZE LIMIT** - Max 450 lines per file, refactor at 350 lines
- **Version 6.2.0** - Do not change without approval
- **GIT WORKFLOW** - ALWAYS push to ALL repositories (github, pragmatismo)
---
## 🚨 Immediate Action Required
1. **Replace 955 unwrap()/expect() calls** with proper error handling
2. **Optimize 12,973 clone()/to_string() calls** for performance
3. **Refactor 5 large files** following TODO-refactor1.md
4. **Add missing error handling** in critical paths
5. **Implement proper logging** instead of panicking
---
## 📄 License
General Bot Copyright (c) pragmatismo.com.br. All rights reserved.
Licensed under the **AGPL-3.0**.
According to our dual licensing model, this program can be used either under the terms of the GNU Affero General Public License, version 3, or under a proprietary license.
---
## 🔗 Links
- **Website:** [pragmatismo.com.br](https://pragmatismo.com.br)
- **Documentation:** [docs.pragmatismo.com.br](https://docs.pragmatismo.com.br)
- **GitHub:** [github.com/GeneralBots/botserver](https://github.com/GeneralBots/botserver)
- **Stack Overflow:** Tag questions with `generalbots`
- **Video Tutorial:** [7 AI General Bots LLM Templates](https://www.youtube.com/watch?v=KJgvUPXi3Fw)
---
**General Bots Code Name:** [Guaribas](https://en.wikipedia.org/wiki/Guaribas)
> "No one should have to do work that can be done by a machine." - Roberto Mangabeira Unger
> "No one should have to do work that can be done by a machine." - Roberto Mangabeira Unger

View file

@ -13,19 +13,19 @@ declare -A container_limits=(
["*drive*"]="4096MB:100ms/100ms"
["*minio*"]="4096MB:100ms/100ms" # MinIO alternative
["*email*"]="4096MB:100ms/100ms"
["*webmail*"]="2096MB:100ms/100ms"
["*webmail*"]="4096MB:100ms/100ms"
["*bot*"]="2048MB:25ms/100ms"
["*oppbot*"]="2048MB:50ms/100ms"
["*oppbot*"]="4096MB:50ms/100ms"
["*meeting*"]="4096MB:100ms/100ms"
["*alm*"]="512MB:50ms/100ms"
["*vault*"]="512MB:50ms/100ms"
["*alm*"]="2048MB:50ms/100ms"
["*vault*"]="2048MB:50ms/100ms"
["*alm-ci*"]="8192MB:200ms/100ms" # CHANGED: 100ms → 200ms (HIGHEST PRIORITY)
["*system*"]="4096MB:50ms/100ms"
["*mailer*"]="2096MB:25ms/100ms"
)
# Default values (for containers that don't match any pattern)
DEFAULT_MEMORY="512MB"
DEFAULT_MEMORY="2048MB"
DEFAULT_CPU_ALLOWANCE="15ms/100ms"
DEFAULT_CPU_COUNT=1

View file

@ -34,9 +34,16 @@ pub async fn serve_suite_js_file(
}
if file_path.starts_with("vendor/") || file_path.starts_with("vendor\\") {
return serve_vendor_file(State(state), Path(VendorFilePath {
file_path: file_path.strip_prefix("vendor/").unwrap_or(&file_path).to_string()
})).await;
return serve_vendor_file(
State(state),
Path(VendorFilePath {
file_path: file_path
.strip_prefix("vendor/")
.unwrap_or(&file_path)
.to_string(),
}),
)
.await;
}
if !file_path.ends_with(".js") {
@ -45,7 +52,15 @@ pub async fn serve_suite_js_file(
let content_type = get_content_type(&file_path);
let ui_path = std::env::var("BOTUI_PATH").unwrap_or_else(|_| "./botui/ui/suite".to_string());
let ui_path = std::env::var("BOTUI_PATH").unwrap_or_else(|_| {
if std::path::Path::new("./botui/ui/suite").exists() {
"./botui/ui/suite".to_string()
} else if std::path::Path::new("../botui/ui/suite").exists() {
"../botui/ui/suite".to_string()
} else {
"./botui/ui/suite".to_string()
}
});
let local_path = format!("{}/js/{}", ui_path, file_path);
match tokio::fs::read(&local_path).await {
@ -57,7 +72,10 @@ pub async fn serve_suite_js_file(
.header(header::CACHE_CONTROL, "public, max-age=3600")
.body(Body::from(content))
.unwrap_or_else(|_| {
(StatusCode::INTERNAL_SERVER_ERROR, "Failed to build response")
(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to build response",
)
.into_response()
})
}
@ -82,6 +100,7 @@ pub async fn serve_vendor_file(
let local_paths = [
format!("./botui/ui/suite/js/vendor/{}", file_path),
format!("../botui/ui/suite/js/vendor/{}", file_path),
format!("./botserver-stack/static/js/vendor/{}", file_path),
];
@ -94,50 +113,50 @@ pub async fn serve_vendor_file(
.header(header::CACHE_CONTROL, "public, max-age=86400")
.body(Body::from(content))
.unwrap_or_else(|_| {
(StatusCode::INTERNAL_SERVER_ERROR, "Failed to build response")
(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to build response",
)
.into_response()
});
}
}
let bot_name = state.bucket_name
.trim_end_matches(".gbai")
.to_string();
let bot_name = state.bucket_name.trim_end_matches(".gbai").to_string();
let sanitized_bot_name = bot_name.to_lowercase().replace([' ', '_'], "-");
let bucket = format!("{}.gbai", sanitized_bot_name);
let key = format!("{}.gblib/vendor/{}", sanitized_bot_name, file_path);
trace!("Trying MinIO for vendor file: bucket={}, key={}", bucket, key);
trace!(
"Trying MinIO for vendor file: bucket={}, key={}",
bucket,
key
);
if let Some(ref drive) = state.drive {
match drive
.get_object()
.bucket(&bucket)
.key(&key)
.send()
.await
{
Ok(response) => {
match response.body.collect().await {
Ok(body) => {
let content = body.into_bytes();
match drive.get_object().bucket(&bucket).key(&key).send().await {
Ok(response) => match response.body.collect().await {
Ok(body) => {
let content = body.into_bytes();
return Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, content_type)
.header(header::CACHE_CONTROL, "public, max-age=86400")
.body(Body::from(content.to_vec()))
.unwrap_or_else(|_| {
(StatusCode::INTERNAL_SERVER_ERROR, "Failed to build response")
.into_response()
});
}
Err(e) => {
error!("Failed to read MinIO response body: {}", e);
}
return Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, content_type)
.header(header::CACHE_CONTROL, "public, max-age=86400")
.body(Body::from(content.to_vec()))
.unwrap_or_else(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to build response",
)
.into_response()
});
}
}
Err(e) => {
error!("Failed to read MinIO response body: {}", e);
}
},
Err(e) => {
warn!("MinIO get_object failed for {}/{}: {}", bucket, key, e);
}
@ -150,17 +169,47 @@ pub async fn serve_vendor_file(
fn rewrite_cdn_urls(html: &str) -> String {
html
// HTMX from various CDNs
.replace("https://unpkg.com/htmx.org@1.9.10", "/js/vendor/htmx.min.js")
.replace("https://unpkg.com/htmx.org@1.9.10/dist/htmx.min.js", "/js/vendor/htmx.min.js")
.replace("https://unpkg.com/htmx.org@1.9.11", "/js/vendor/htmx.min.js")
.replace("https://unpkg.com/htmx.org@1.9.11/dist/htmx.min.js", "/js/vendor/htmx.min.js")
.replace("https://unpkg.com/htmx.org@1.9.12", "/js/vendor/htmx.min.js")
.replace("https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js", "/js/vendor/htmx.min.js")
.replace(
"https://unpkg.com/htmx.org@1.9.10",
"/js/vendor/htmx.min.js",
)
.replace(
"https://unpkg.com/htmx.org@1.9.10/dist/htmx.min.js",
"/js/vendor/htmx.min.js",
)
.replace(
"https://unpkg.com/htmx.org@1.9.11",
"/js/vendor/htmx.min.js",
)
.replace(
"https://unpkg.com/htmx.org@1.9.11/dist/htmx.min.js",
"/js/vendor/htmx.min.js",
)
.replace(
"https://unpkg.com/htmx.org@1.9.12",
"/js/vendor/htmx.min.js",
)
.replace(
"https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js",
"/js/vendor/htmx.min.js",
)
.replace("https://unpkg.com/htmx.org", "/js/vendor/htmx.min.js")
.replace("https://cdn.jsdelivr.net/npm/htmx.org", "/js/vendor/htmx.min.js")
.replace("https://cdnjs.cloudflare.com/ajax/libs/htmx/1.9.10/htmx.min.js", "/js/vendor/htmx.min.js")
.replace("https://cdnjs.cloudflare.com/ajax/libs/htmx/1.9.11/htmx.min.js", "/js/vendor/htmx.min.js")
.replace("https://cdnjs.cloudflare.com/ajax/libs/htmx/1.9.12/htmx.min.js", "/js/vendor/htmx.min.js")
.replace(
"https://cdn.jsdelivr.net/npm/htmx.org",
"/js/vendor/htmx.min.js",
)
.replace(
"https://cdnjs.cloudflare.com/ajax/libs/htmx/1.9.10/htmx.min.js",
"/js/vendor/htmx.min.js",
)
.replace(
"https://cdnjs.cloudflare.com/ajax/libs/htmx/1.9.11/htmx.min.js",
"/js/vendor/htmx.min.js",
)
.replace(
"https://cdnjs.cloudflare.com/ajax/libs/htmx/1.9.12/htmx.min.js",
"/js/vendor/htmx.min.js",
)
}
pub fn configure_app_server_routes() -> Router<Arc<AppState>> {
@ -240,26 +289,24 @@ async fn serve_app_file_internal(state: &AppState, app_name: &str, file_path: &s
}
// Get bot name from bucket_name config (default to "default")
let bot_name = state.bucket_name
.trim_end_matches(".gbai")
.to_string();
let bot_name = state.bucket_name.trim_end_matches(".gbai").to_string();
let sanitized_bot_name = bot_name.to_lowercase().replace([' ', '_'], "-");
// MinIO bucket and path: botname.gbai / botname.gbapp/appname/file
let bucket = format!("{}.gbai", sanitized_bot_name);
let key = format!("{}.gbapp/{}/{}", sanitized_bot_name, sanitized_app_name, sanitized_file_path);
let key = format!(
"{}.gbapp/{}/{}",
sanitized_bot_name, sanitized_app_name, sanitized_file_path
);
info!("Serving app file from MinIO: bucket={}, key={}", bucket, key);
info!(
"Serving app file from MinIO: bucket={}, key={}",
bucket, key
);
// Try to serve from MinIO
if let Some(ref drive) = state.drive {
match drive
.get_object()
.bucket(&bucket)
.key(&key)
.send()
.await
{
match drive.get_object().bucket(&bucket).key(&key).send().await {
Ok(response) => {
match response.body.collect().await {
Ok(body) => {
@ -281,7 +328,10 @@ async fn serve_app_file_internal(state: &AppState, app_name: &str, file_path: &s
.header(header::CACHE_CONTROL, "public, max-age=3600")
.body(Body::from(final_content))
.unwrap_or_else(|_| {
(StatusCode::INTERNAL_SERVER_ERROR, "Failed to build response")
(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to build response",
)
.into_response()
});
}
@ -390,8 +440,6 @@ pub async fn list_all_apps(State(state): State<Arc<AppState>>) -> impl IntoRespo
.into_response()
}
#[cfg(test)]
mod tests {
use super::*;
@ -431,9 +479,15 @@ mod tests {
fn test_sanitize_file_path() {
assert_eq!(sanitize_file_path("styles.css"), "styles.css");
assert_eq!(sanitize_file_path("css/styles.css"), "css/styles.css");
assert_eq!(sanitize_file_path("assets/img/logo.png"), "assets/img/logo.png");
assert_eq!(
sanitize_file_path("assets/img/logo.png"),
"assets/img/logo.png"
);
assert_eq!(sanitize_file_path("../../../etc/passwd"), "etc/passwd");
assert_eq!(sanitize_file_path("./styles.css"), "styles.css");
assert_eq!(sanitize_file_path("path//double//slash.js"), "path/double/slash.js");
assert_eq!(
sanitize_file_path("path//double//slash.js"),
"path/double/slash.js"
);
}
}

View file

@ -1,16 +1,17 @@
#[cfg(feature = "llm")]
use crate::llm::smart_router::{SmartLLMRouter, OptimizationGoal};
use crate::core::shared::state::AppState;
use crate::basic::UserSession;
use crate::core::shared::state::AppState;
#[cfg(feature = "llm")]
use rhai::{Dynamic, Engine};
use crate::llm::smart_router::{OptimizationGoal, SmartLLMRouter};
#[cfg(not(feature = "llm"))]
use rhai::Engine;
#[cfg(feature = "llm")]
use rhai::{Dynamic, Engine};
use std::sync::Arc;
#[cfg(feature = "llm")]
pub fn register_enhanced_llm_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let state_clone1 = Arc::clone(&state);
let state_clone2 = Arc::clone(&state);
let user_clone = user;
if let Err(e) = engine.register_custom_syntax(
@ -19,15 +20,19 @@ pub fn register_enhanced_llm_keyword(state: Arc<AppState>, user: UserSession, en
move |context, inputs| {
let prompt = context.eval_expression_tree(&inputs[0])?.to_string();
let optimization = context.eval_expression_tree(&inputs[1])?.to_string();
let state_for_spawn = Arc::clone(&state_clone);
let state_for_spawn = Arc::clone(&state_clone1);
let _user_clone_spawn = user_clone.clone();
tokio::spawn(async move {
let router = SmartLLMRouter::new(state_for_spawn);
let goal = OptimizationGoal::from_str(&optimization);
match crate::llm::smart_router::enhanced_llm_call(&router, &prompt, goal, None, None).await {
match crate::llm::smart_router::enhanced_llm_call(
&router, &prompt, goal, None, None,
)
.await
{
Ok(_response) => {
log::info!("LLM response generated with {} optimization", optimization);
}
@ -44,27 +49,41 @@ pub fn register_enhanced_llm_keyword(state: Arc<AppState>, user: UserSession, en
}
if let Err(e) = engine.register_custom_syntax(
["LLM", "$string$", "WITH", "MAX_COST", "$float$", "MAX_LATENCY", "$int$"],
[
"LLM",
"$string$",
"WITH",
"MAX_COST",
"$float$",
"MAX_LATENCY",
"$int$",
],
false,
move |context, inputs| {
let prompt = context.eval_expression_tree(&inputs[0])?.to_string();
let max_cost = context.eval_expression_tree(&inputs[1])?.as_float()?;
let max_latency = context.eval_expression_tree(&inputs[2])?.as_int()? as u64;
let state_for_spawn = Arc::clone(&state_clone);
let state_for_spawn = Arc::clone(&state_clone2);
tokio::spawn(async move {
let router = SmartLLMRouter::new(state_for_spawn);
match crate::llm::smart_router::enhanced_llm_call(
&router,
&prompt,
OptimizationGoal::Balanced,
Some(max_cost),
Some(max_latency)
).await {
&router,
&prompt,
OptimizationGoal::Balanced,
Some(max_cost),
Some(max_latency),
)
.await
{
Ok(_response) => {
log::info!("LLM response with constraints: cost<={}, latency<={}", max_cost, max_latency);
log::info!(
"LLM response with constraints: cost<={}, latency<={}",
max_cost,
max_latency
);
}
Err(e) => {
log::error!("Constrained LLM call failed: {}", e);
@ -80,6 +99,10 @@ pub fn register_enhanced_llm_keyword(state: Arc<AppState>, user: UserSession, en
}
#[cfg(not(feature = "llm"))]
pub fn register_enhanced_llm_keyword(_state: Arc<AppState>, _user: UserSession, _engine: &mut Engine) {
pub fn register_enhanced_llm_keyword(
_state: Arc<AppState>,
_user: UserSession,
_engine: &mut Engine,
) {
// No-op when LLM feature is disabled
}

View file

@ -8,7 +8,6 @@ use tokio::sync::{mpsc, RwLock};
pub mod cache;
pub mod claude;
pub mod episodic_memory;
pub mod smart_router;
pub mod llm_models;
pub mod local;
pub mod smart_router;
@ -235,7 +234,10 @@ pub fn create_llm_provider(
}
}
pub fn create_llm_provider_from_url(url: &str, model: Option<String>) -> std::sync::Arc<dyn LLMProvider> {
pub fn create_llm_provider_from_url(
url: &str,
model: Option<String>,
) -> std::sync::Arc<dyn LLMProvider> {
let provider_type = LLMProviderType::from(url);
create_llm_provider(provider_type, url.to_string(), model)
}
@ -276,7 +278,10 @@ impl LLMProvider for DynamicLLMProvider {
model: &str,
key: &str,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
self.get_provider().await.generate(prompt, config, model, key).await
self.get_provider()
.await
.generate(prompt, config, model, key)
.await
}
async fn generate_stream(
@ -287,7 +292,10 @@ impl LLMProvider for DynamicLLMProvider {
model: &str,
key: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.get_provider().await.generate_stream(prompt, config, tx, model, key).await
self.get_provider()
.await
.generate_stream(prompt, config, tx, model, key)
.await
}
async fn cancel_job(

View file

@ -1,5 +1,4 @@
use crate::core::shared::state::AppState;
use crate::llm::LLMProvider;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
@ -49,54 +48,56 @@ impl SmartLLMRouter {
pub async fn select_optimal_model(
&self,
task_type: &str,
_task_type: &str,
optimization_goal: OptimizationGoal,
max_cost: Option<f64>,
max_latency: Option<u64>,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let performance_data = self.performance_cache.read().await;
let mut candidates: Vec<&ModelPerformance> = performance_data.values().collect();
// Filter by constraints
if let Some(max_cost) = max_cost {
candidates.retain(|p| p.avg_cost_per_token <= max_cost);
}
if let Some(max_latency) = max_latency {
candidates.retain(|p| p.avg_latency_ms <= max_latency);
}
if candidates.is_empty() {
return Ok("gpt-4o-mini".to_string()); // Fallback model
}
// Select based on optimization goal
let selected = match optimization_goal {
OptimizationGoal::Speed => {
candidates.iter().min_by_key(|p| p.avg_latency_ms)
}
OptimizationGoal::Cost => {
candidates.iter().min_by(|a, b| a.avg_cost_per_token.partial_cmp(&b.avg_cost_per_token).unwrap())
}
OptimizationGoal::Quality => {
candidates.iter().max_by(|a, b| a.success_rate.partial_cmp(&b.success_rate).unwrap())
}
OptimizationGoal::Speed => candidates.iter().min_by_key(|p| p.avg_latency_ms),
OptimizationGoal::Cost => candidates.iter().min_by(|a, b| {
a.avg_cost_per_token
.partial_cmp(&b.avg_cost_per_token)
.unwrap()
}),
OptimizationGoal::Quality => candidates
.iter()
.max_by(|a, b| a.success_rate.partial_cmp(&b.success_rate).unwrap()),
OptimizationGoal::Balanced => {
// Weighted score: 40% success rate, 30% speed, 30% cost
candidates.iter().max_by(|a, b| {
let score_a = (a.success_rate * 0.4) +
((1000.0 / a.avg_latency_ms as f64) * 0.3) +
((1.0 / (a.avg_cost_per_token + 0.001)) * 0.3);
let score_b = (b.success_rate * 0.4) +
((1000.0 / b.avg_latency_ms as f64) * 0.3) +
((1.0 / (b.avg_cost_per_token + 0.001)) * 0.3);
let score_a = (a.success_rate * 0.4)
+ ((1000.0 / a.avg_latency_ms as f64) * 0.3)
+ ((1.0 / (a.avg_cost_per_token + 0.001)) * 0.3);
let score_b = (b.success_rate * 0.4)
+ ((1000.0 / b.avg_latency_ms as f64) * 0.3)
+ ((1.0 / (b.avg_cost_per_token + 0.001)) * 0.3);
score_a.partial_cmp(&score_b).unwrap()
})
}
};
Ok(selected.map(|p| p.model_name.clone()).unwrap_or_else(|| "gpt-4o-mini".to_string()))
Ok(selected
.map(|p| p.model_name.clone())
.unwrap_or_else(|| "gpt-4o-mini".to_string()))
}
pub async fn track_performance(
@ -107,29 +108,32 @@ impl SmartLLMRouter {
success: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut performance_data = self.performance_cache.write().await;
let performance = performance_data.entry(model_name.to_string()).or_insert_with(|| {
ModelPerformance {
let performance = performance_data
.entry(model_name.to_string())
.or_insert_with(|| ModelPerformance {
model_name: model_name.to_string(),
avg_latency_ms: latency_ms,
avg_cost_per_token: cost_per_token,
success_rate: if success { 1.0 } else { 0.0 },
total_requests: 0,
last_updated: chrono::Utc::now(),
}
});
});
// Update running averages
let total = performance.total_requests as f64;
performance.avg_latency_ms = ((performance.avg_latency_ms as f64 * total) + latency_ms as f64) as u64 / (total + 1.0) as u64;
performance.avg_cost_per_token = (performance.avg_cost_per_token * total + cost_per_token) / (total + 1.0);
performance.avg_latency_ms = ((performance.avg_latency_ms as f64 * total)
+ latency_ms as f64) as u64
/ (total + 1.0) as u64;
performance.avg_cost_per_token =
(performance.avg_cost_per_token * total + cost_per_token) / (total + 1.0);
let success_count = (performance.success_rate * total) + if success { 1.0 } else { 0.0 };
performance.success_rate = success_count / (total + 1.0);
performance.total_requests += 1;
performance.last_updated = chrono::Utc::now();
Ok(())
}
@ -147,13 +151,15 @@ pub async fn enhanced_llm_call(
max_latency: Option<u64>,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let start_time = Instant::now();
// Select optimal model
let model = router.select_optimal_model("general", optimization_goal, max_cost, max_latency).await?;
let model = router
.select_optimal_model("general", optimization_goal, max_cost, max_latency)
.await?;
// Make LLM call (simplified - would use actual LLM provider)
let response = format!("Response from {} for: {}", model, prompt);
// Track performance
let latency = start_time.elapsed().as_millis() as u64;
let cost_per_token = match model.as_str() {
@ -162,8 +168,10 @@ pub async fn enhanced_llm_call(
"claude-3-sonnet" => 0.015,
_ => 0.01,
};
router.track_performance(&model, latency, cost_per_token, true).await?;
router
.track_performance(&model, latency, cost_per_token, true)
.await?;
Ok(response)
}

View file

@ -680,7 +680,15 @@ async fn run_axum_server(
info!("Security middleware enabled: rate limiting, security headers, panic handler, request ID tracking, authentication");
// Path to UI files (botui) - use external folder or fallback to embedded
let ui_path = std::env::var("BOTUI_PATH").unwrap_or_else(|_| "./botui/ui/suite".to_string());
let ui_path = std::env::var("BOTUI_PATH").unwrap_or_else(|_| {
if std::path::Path::new("./botui/ui/suite").exists() {
"./botui/ui/suite".to_string()
} else if std::path::Path::new("../botui/ui/suite").exists() {
"../botui/ui/suite".to_string()
} else {
"./botui/ui/suite".to_string()
}
});
let ui_path_exists = std::path::Path::new(&ui_path).exists();
let use_embedded_ui = !ui_path_exists && embedded_ui::has_embedded_ui();