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:
parent
a51e3a0758
commit
51c8a53a90
9 changed files with 665 additions and 541 deletions
|
|
@ -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
315
PROMPT.md
|
|
@ -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
514
README.md
|
|
@ -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)
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
**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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
10
src/main.rs
10
src/main.rs
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue