From 51c8a53a902ce70c89bae9c693ae2dddf2143238 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Wed, 28 Jan 2026 16:58:14 -0300 Subject: [PATCH] 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 --- Cargo.toml | 2 +- PROMPT.md | 315 ------------------ README.md | 514 ++++++++++++++++++++++++----- scripts/utils/set-limits.sh | 10 +- src/basic/keywords/app_server.rs | 178 ++++++---- src/basic/keywords/enhanced_llm.rs | 69 ++-- src/llm/mod.rs | 16 +- src/llm/smart_router.rs | 92 +++--- src/main.rs | 10 +- 9 files changed, 665 insertions(+), 541 deletions(-) delete mode 100644 PROMPT.md diff --git a/Cargo.toml b/Cargo.toml index 841f8a3a1..e5079057a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/PROMPT.md b/PROMPT.md deleted file mode 100644 index 7e78c7862..000000000 --- a/PROMPT.md +++ /dev/null @@ -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) -> 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 = 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 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 \ No newline at end of file diff --git a/README.md b/README.md index b19cbe12f..f02ba8554 100644 --- a/README.md +++ b/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) + +--- + ![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 # 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 # 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) -> 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 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 @@ -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 \ No newline at end of file diff --git a/scripts/utils/set-limits.sh b/scripts/utils/set-limits.sh index 395594218..0b63ac7da 100644 --- a/scripts/utils/set-limits.sh +++ b/scripts/utils/set-limits.sh @@ -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 diff --git a/src/basic/keywords/app_server.rs b/src/basic/keywords/app_server.rs index 270941dfb..a007afdd0 100644 --- a/src/basic/keywords/app_server.rs +++ b/src/basic/keywords/app_server.rs @@ -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> { @@ -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>) -> 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" + ); } } diff --git a/src/basic/keywords/enhanced_llm.rs b/src/basic/keywords/enhanced_llm.rs index 5b7d8f8b4..880efafa8 100644 --- a/src/basic/keywords/enhanced_llm.rs +++ b/src/basic/keywords/enhanced_llm.rs @@ -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, 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, 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, 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, user: UserSession, en } #[cfg(not(feature = "llm"))] -pub fn register_enhanced_llm_keyword(_state: Arc, _user: UserSession, _engine: &mut Engine) { +pub fn register_enhanced_llm_keyword( + _state: Arc, + _user: UserSession, + _engine: &mut Engine, +) { // No-op when LLM feature is disabled } diff --git a/src/llm/mod.rs b/src/llm/mod.rs index 7a46ed26f..4655bacac 100644 --- a/src/llm/mod.rs +++ b/src/llm/mod.rs @@ -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) -> std::sync::Arc { +pub fn create_llm_provider_from_url( + url: &str, + model: Option, +) -> std::sync::Arc { 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> { - 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> { - 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( diff --git a/src/llm/smart_router.rs b/src/llm/smart_router.rs index bbd701a91..acc706376 100644 --- a/src/llm/smart_router.rs +++ b/src/llm/smart_router.rs @@ -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, max_latency: Option, ) -> Result> { 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> { 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, ) -> Result> { 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) } diff --git a/src/main.rs b/src/main.rs index 990990dc5..7b1b5a7dd 100644 --- a/src/main.rs +++ b/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();