From 90c14bcd09ac09c797f397b357ee55f756b18758 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Mon, 6 Apr 2026 13:37:23 -0300 Subject: [PATCH] Fix DETECT: use bot-specific DB pool, add anonymous auth when directory disabled --- Cargo.toml | 11 +- .../agendamento_visita.bas | 3 + .../agendamento_visita.mcp.json | 24 +-- .../agendamento_visita.tool.json | 24 +-- .../contato_secretaria.mcp.json | 10 +- .../salesianos.gbdialog/inscricao.mcp.json | 52 ++--- .../salesianos.gbdialog/inscricao.tool.json | 42 ++-- .../salesianos.gbdialog/start.ast | 21 +- .../salesianos.gbdialog/start.bas | 22 +-- src/api/mod.rs | 1 + src/api/terminal.rs | 1 + src/basic/compiler/blocks/mail.rs | 14 +- src/basic/compiler/blocks/mod.rs | 10 +- src/basic/compiler/blocks/talk.rs | 15 +- src/basic/compiler/mod.rs | 180 ++++++++++++++---- src/basic/keywords/detect.rs | 40 +++- src/basic/keywords/enhanced_llm.rs | 116 ++++------- src/basic/mod.rs | 57 ++++++ src/core/bootstrap/bootstrap_manager.rs | 6 + src/core/shared/admin_email.rs | 2 + src/core/shared/models/mod.rs | 32 ++-- src/llm/smart_router.rs | 33 +++- src/main_module/server.rs | 71 ++++++- src/settings/mod.rs | 7 +- src/settings/rbac.rs | 5 +- 25 files changed, 539 insertions(+), 260 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b5192934..caa8ca3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,12 @@ features = ["database", "i18n"] [features] # ===== DEFAULT ===== -default = ["chat", "people", "automation", "drive", "tasks", "cache", "directory", "llm", "crawler", "browser", "terminal", "editor", "mail", "whatsapp", "designer", "marketing", "goals", "analytics", "vectordb", "research"] +default = ["chat", "automation", "cache", "llm"] + +# ===== SECURITY MODES ===== +# no-security: Minimal build - chat, automation, drive, cache only (no RBAC, directory, security, compliance) +# Build with: cargo build --no-default-features --features "no-security,chat,llm" +no-security = [] browser = ["automation", "drive", "cache"] terminal = ["automation", "drive", "cache"] @@ -21,7 +26,8 @@ scripting = ["dep:rhai"] automation = ["scripting", "dep:cron"] drive = ["dep:aws-config", "dep:aws-sdk-s3", "dep:aws-smithy-async", "dep:pdf-extract", "dep:notify"] cache = ["dep:redis"] -directory = [] +directory = ["rbac"] +rbac = [] crawler = ["drive", "cache"] # ===== APPS (Each includes what it needs from core) ===== @@ -90,6 +96,7 @@ console = ["automation", "drive", "cache", "dep:crossterm", "dep:ratatui"] # ===== BUNDLES (Optional - for convenience) ===== minimal = ["chat"] +minimal-chat = ["chat", "automation", "drive", "cache"] # No security at all lightweight = ["chat", "tasks", "people"] full = ["chat", "people", "mail", "tasks", "calendar", "drive", "docs", "llm", "cache", "compliance"] embed-ui = ["dep:rust-embed"] diff --git a/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/agendamento_visita.bas b/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/agendamento_visita.bas index aef79302..30316b7c 100644 --- a/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/agendamento_visita.bas +++ b/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/agendamento_visita.bas @@ -7,6 +7,9 @@ PARAM horario AS STRING LIKE "10:00" DESCRIPTION "Horário preferencial (formato PARAM numeroVisitantes AS INTEGER LIKE "3" DESCRIPTION "Número de visitantes" + + + id = "VIS-" + FORMAT(NOW(), "yyyyMMdd") + "-" + FORMAT(RANDOM(1000, 9999), "0000") protocoloNumero = "VIS" + FORMAT(RANDOM(100000, 999999), "000000") dataCadastro = FORMAT(NOW(), "yyyy-MM-dd HH:mm:ss") diff --git a/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/agendamento_visita.mcp.json b/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/agendamento_visita.mcp.json index 88b52c68..8f1cc0af 100644 --- a/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/agendamento_visita.mcp.json +++ b/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/agendamento_visita.mcp.json @@ -4,15 +4,15 @@ "input_schema": { "type": "object", "properties": { - "telefone": { + "horario": { "type": "string", - "description": "Telefone com DDD", - "example": "(21) 99999-9999" + "description": "Horário preferencial (formato HH:MM, entre 8h e 17h)", + "example": "10:00" }, - "email": { + "nomeResponsavel": { "type": "string", - "description": "Email para contato", - "example": "joao@example.com" + "description": "Nome do responsável", + "example": "João Silva" }, "dataVisita": { "type": "string", @@ -20,20 +20,20 @@ "example": "2026-03-15", "format": "date" }, - "horario": { + "email": { "type": "string", - "description": "Horário preferencial (formato HH:MM, entre 8h e 17h)", - "example": "10:00" + "description": "Email para contato", + "example": "joao@example.com" }, "numeroVisitantes": { "type": "integer", "description": "Número de visitantes", "example": "3" }, - "nomeResponsavel": { + "telefone": { "type": "string", - "description": "Nome do responsável", - "example": "João Silva" + "description": "Telefone com DDD", + "example": "(21) 99999-9999" } }, "required": [ diff --git a/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/agendamento_visita.tool.json b/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/agendamento_visita.tool.json index e70c0b81..d8ffde53 100644 --- a/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/agendamento_visita.tool.json +++ b/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/agendamento_visita.tool.json @@ -6,11 +6,15 @@ "parameters": { "type": "object", "properties": { - "dataVisita": { + "nomeResponsavel": { "type": "string", - "description": "Data desejada para visita (formato ISO: YYYY-MM-DD)", - "example": "2026-03-15", - "format": "date" + "description": "Nome do responsável", + "example": "João Silva" + }, + "email": { + "type": "string", + "description": "Email para contato", + "example": "joao@example.com" }, "numeroVisitantes": { "type": "integer", @@ -22,15 +26,11 @@ "description": "Telefone com DDD", "example": "(21) 99999-9999" }, - "email": { + "dataVisita": { "type": "string", - "description": "Email para contato", - "example": "joao@example.com" - }, - "nomeResponsavel": { - "type": "string", - "description": "Nome do responsável", - "example": "João Silva" + "description": "Data desejada para visita (formato ISO: YYYY-MM-DD)", + "example": "2026-03-15", + "format": "date" }, "horario": { "type": "string", diff --git a/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/contato_secretaria.mcp.json b/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/contato_secretaria.mcp.json index 88074319..2562a65e 100644 --- a/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/contato_secretaria.mcp.json +++ b/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/contato_secretaria.mcp.json @@ -4,11 +4,6 @@ "input_schema": { "type": "object", "properties": { - "email": { - "type": "string", - "description": "Email para contato", - "example": "joao@example.com" - }, "nome": { "type": "string", "description": "Nome do interessado", @@ -24,6 +19,11 @@ "description": "Mensagem completa", "example": "Gostaria de saber as formas de pagamento disponíveis" }, + "email": { + "type": "string", + "description": "Email para contato", + "example": "joao@example.com" + }, "telefone": { "type": "string", "description": "Telefone para retorno", diff --git a/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/inscricao.mcp.json b/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/inscricao.mcp.json index 77e78f24..dfdf09bf 100644 --- a/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/inscricao.mcp.json +++ b/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/inscricao.mcp.json @@ -4,46 +4,46 @@ "input_schema": { "type": "object", "properties": { - "dataNascimento": { - "type": "string", - "description": "Data de nascimento (formato ISO: YYYY-MM-DD)", - "example": "2015-03-15", - "format": "date" - }, - "email": { - "type": "string", - "description": "Email para contato", - "example": "maria.santos@example.com" - }, - "endereco": { - "type": "string", - "description": "Endereço completo", - "example": "Rua das Flores, 123 - Centro" - }, - "nomeCompleto": { - "type": "string", - "description": "Nome completo do aluno", - "example": "João Silva Santos" - }, "telefone": { "type": "string", "description": "Telefone com DDD", "example": "(21) 99999-9999" }, - "turno": { - "type": "string", - "description": "Turno: MANHA ou TARDE", - "example": "MANHA" - }, "serie": { "type": "string", "description": "Série desejada", "example": "8º ano" }, + "nomeCompleto": { + "type": "string", + "description": "Nome completo do aluno", + "example": "João Silva Santos" + }, + "endereco": { + "type": "string", + "description": "Endereço completo", + "example": "Rua das Flores, 123 - Centro" + }, "nomeResponsavel": { "type": "string", "description": "Nome do responsável", "example": "Maria Silva Santos" + }, + "email": { + "type": "string", + "description": "Email para contato", + "example": "maria.santos@example.com" + }, + "turno": { + "type": "string", + "description": "Turno: MANHA ou TARDE", + "example": "MANHA" + }, + "dataNascimento": { + "type": "string", + "description": "Data de nascimento (formato ISO: YYYY-MM-DD)", + "example": "2015-03-15", + "format": "date" } }, "required": [ diff --git a/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/inscricao.tool.json b/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/inscricao.tool.json index 147dc15c..5b7ce42b 100644 --- a/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/inscricao.tool.json +++ b/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/inscricao.tool.json @@ -6,27 +6,6 @@ "parameters": { "type": "object", "properties": { - "nomeCompleto": { - "type": "string", - "description": "Nome completo do aluno", - "example": "João Silva Santos" - }, - "telefone": { - "type": "string", - "description": "Telefone com DDD", - "example": "(21) 99999-9999" - }, - "dataNascimento": { - "type": "string", - "description": "Data de nascimento (formato ISO: YYYY-MM-DD)", - "example": "2015-03-15", - "format": "date" - }, - "serie": { - "type": "string", - "description": "Série desejada", - "example": "8º ano" - }, "turno": { "type": "string", "description": "Turno: MANHA ou TARDE", @@ -36,11 +15,32 @@ "TARDE" ] }, + "nomeCompleto": { + "type": "string", + "description": "Nome completo do aluno", + "example": "João Silva Santos" + }, + "dataNascimento": { + "type": "string", + "description": "Data de nascimento (formato ISO: YYYY-MM-DD)", + "example": "2015-03-15", + "format": "date" + }, "email": { "type": "string", "description": "Email para contato", "example": "maria.santos@example.com" }, + "serie": { + "type": "string", + "description": "Série desejada", + "example": "8º ano" + }, + "telefone": { + "type": "string", + "description": "Telefone com DDD", + "example": "(21) 99999-9999" + }, "endereco": { "type": "string", "description": "Endereço completo", diff --git a/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/start.ast b/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/start.ast index 4e6b9530..cadf7dff 100644 --- a/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/start.ast +++ b/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/start.ast @@ -1,21 +1,20 @@ USE_WEBSITE("https://salesianos.br", "30d"); -USE_KB "carta"; -USE_KB "proc"; +USE KB "carta"; +USE KB "proc"; USE TOOL "inscricao"; USE TOOL "consultar_inscricao"; USE TOOL "agendamento_visita"; USE TOOL "informacoes_curso"; USE TOOL "documentos_necessarios"; USE TOOL "contato_secretaria"; -USE TOOL ""; USE TOOL "calendario_letivo"; -ADD_SUGGESTION_TOOL "inscricao" as "📚 Fazer Inscrição"; -ADD_SUGGESTION_TOOL "consultar_inscricao" as "🔍 Consultar Inscrição"; -ADD_SUGGESTION_TOOL "agendamento_visita" as "🏫 Agendar Visita"; -ADD_SUGGESTION_TOOL "informacoes_curso" as "📖 Informações de Cursos"; -ADD_SUGGESTION_TOOL "documentos_necessarios" as "📋 Documentos Necessários"; -ADD_SUGGESTION_TOOL "contato_secretaria" as "📞 Falar com Secretaria"; -ADD_SUGGESTION_TOOL "" as "Segunda Via de Boleto"; -ADD_SUGGESTION_TOOL "calendario_letivo" as "📅 Calendário Letivo"; +ADD_SUGGESTION_TOOL "inscricao" as "Fazer Inscrição"; +ADD_SUGGESTION_TOOL "consultar_inscricao" as "Consultar Inscrição"; +ADD_SUGGESTION_TOOL "agendamento_visita" as "Agendar Visita"; +ADD_SUGGESTION_TOOL "informacoes_curso" as "Informações de Cursos"; +ADD_SUGGESTION_TOOL "documentos_necessarios" as "Documentos Necessários"; +ADD_SUGGESTION_TOOL "contato_secretaria" as "Falar com Secretaria"; +ADD_SUGGESTION_TOOL "segunda_via" as "Segunda Via de Boleto"; +ADD_SUGGESTION_TOOL "calendario_letivo" as "Calendário Letivo"; ADD_SUGGESTION_TOOL "outros" as "Outros"; TALK "Olá! Sou o assistente virtual da Escola Salesiana. Como posso ajudá-lo hoje com inscrições, visitas, informações sobre cursos, documentos ou calendário letivo?"; diff --git a/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/start.bas b/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/start.bas index 15fd3c65..896264ff 100644 --- a/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/start.bas +++ b/botserver-stack/data/system/work/salesianos.gbai/salesianos.gbdialog/start.bas @@ -1,7 +1,8 @@ USE_WEBSITE("https://salesianos.br", "30d") -USE_KB "carta" -USE_KB "proc" +USE KB "carta" +USE KB "proc" + USE TOOL "inscricao" USE TOOL "consultar_inscricao" @@ -9,17 +10,16 @@ USE TOOL "agendamento_visita" USE TOOL "informacoes_curso" USE TOOL "documentos_necessarios" USE TOOL "contato_secretaria" -USE TOOL "" USE TOOL "calendario_letivo" -ADD_SUGGESTION_TOOL "inscricao" AS "📚 Fazer Inscrição" -ADD_SUGGESTION_TOOL "consultar_inscricao" AS "🔍 Consultar Inscrição" -ADD_SUGGESTION_TOOL "agendamento_visita" AS "🏫 Agendar Visita" -ADD_SUGGESTION_TOOL "informacoes_curso" AS "📖 Informações de Cursos" -ADD_SUGGESTION_TOOL "documentos_necessarios" AS "📋 Documentos Necessários" -ADD_SUGGESTION_TOOL "contato_secretaria" AS "📞 Falar com Secretaria" -ADD_SUGGESTION_TOOL "" AS "Segunda Via de Boleto" -ADD_SUGGESTION_TOOL "calendario_letivo" AS "📅 Calendário Letivo" +ADD_SUGGESTION_TOOL "inscricao" AS "Fazer Inscrição" +ADD_SUGGESTION_TOOL "consultar_inscricao" AS "Consultar Inscrição" +ADD_SUGGESTION_TOOL "agendamento_visita" AS "Agendar Visita" +ADD_SUGGESTION_TOOL "informacoes_curso" AS "Informações de Cursos" +ADD_SUGGESTION_TOOL "documentos_necessarios" AS "Documentos Necessários" +ADD_SUGGESTION_TOOL "contato_secretaria" AS "Falar com Secretaria" +ADD_SUGGESTION_TOOL "segunda_via" AS "Segunda Via de Boleto" +ADD_SUGGESTION_TOOL "calendario_letivo" AS "Calendário Letivo" ADD_SUGGESTION_TOOL "outros" AS "Outros" REM Validar região para escolha de secretaria. REM Sincronizar as bases entre o Bot e a Org. diff --git a/src/api/mod.rs b/src/api/mod.rs index 334eeee0..8c14a2df 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,4 +1,5 @@ pub mod editor; pub mod database; pub mod git; +#[cfg(feature = "terminal")] pub mod terminal; diff --git a/src/api/terminal.rs b/src/api/terminal.rs index 257611fe..27cb93f6 100644 --- a/src/api/terminal.rs +++ b/src/api/terminal.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "terminal")] use axum::{ extract::{ Query, diff --git a/src/basic/compiler/blocks/mail.rs b/src/basic/compiler/blocks/mail.rs index 5b27ebae..ba32a68d 100644 --- a/src/basic/compiler/blocks/mail.rs +++ b/src/basic/compiler/blocks/mail.rs @@ -44,7 +44,14 @@ pub fn convert_mail_line_with_substitution(line: &str) -> String { current_var.clear(); } _ if in_substitution => { - if c.is_alphanumeric() || c == '_' || c == '(' || c == ')' || c == ',' || c == ' ' || c == '\"' { + if c.is_alphanumeric() + || c == '_' + || c == '(' + || c == ')' + || c == ',' + || c == ' ' + || c == '\"' + { current_var.push(c); } } @@ -136,7 +143,10 @@ pub fn convert_mail_block(recipient: &str, lines: &[String]) -> String { } else { recipient.to_string() }; - result.push_str(&format!("send_mail({}, \"{}\", {}, []);\n", recipient_expr, subject, body_expr)); + result.push_str(&format!( + "send_mail({}, \"{}\", {}, []);\n", + recipient_expr, subject, body_expr + )); trace!("Converted MAIL block → {}", result); result diff --git a/src/basic/compiler/blocks/mod.rs b/src/basic/compiler/blocks/mod.rs index b6a12c80..d1b3f145 100644 --- a/src/basic/compiler/blocks/mod.rs +++ b/src/basic/compiler/blocks/mod.rs @@ -30,7 +30,10 @@ pub fn convert_begin_blocks(script: &str) -> String { } if upper == "END TALK" { - trace!("Converting END TALK statement, processing {} lines", talk_block_lines.len()); + trace!( + "Converting END TALK statement, processing {} lines", + talk_block_lines.len() + ); in_talk_block = false; let converted = convert_talk_block(&talk_block_lines); result.push_str(&converted); @@ -53,7 +56,10 @@ pub fn convert_begin_blocks(script: &str) -> String { } if upper == "END MAIL" { - trace!("Converting END MAIL statement, processing {} lines", mail_block_lines.len()); + trace!( + "Converting END MAIL statement, processing {} lines", + mail_block_lines.len() + ); in_mail_block = false; let converted = convert_mail_block(&mail_recipient, &mail_block_lines); result.push_str(&converted); diff --git a/src/basic/compiler/blocks/talk.rs b/src/basic/compiler/blocks/talk.rs index 20772218..59876562 100644 --- a/src/basic/compiler/blocks/talk.rs +++ b/src/basic/compiler/blocks/talk.rs @@ -53,7 +53,14 @@ pub fn convert_talk_line_with_substitution(line: &str) -> String { } } _ if in_substitution => { - if c.is_alphanumeric() || c == '_' || c == '.' || c == '[' || c == ']' || c == ',' || c == '"' { + if c.is_alphanumeric() + || c == '_' + || c == '.' + || c == '[' + || c == ']' + || c == ',' + || c == '"' + { current_var.push(c); } else if c == '(' { current_var.push(c); @@ -115,12 +122,14 @@ pub fn convert_talk_line_with_substitution(line: &str) -> String { pub fn convert_talk_block(lines: &[String]) -> String { // Convert all lines first - let converted_lines: Vec = lines.iter() + let converted_lines: Vec = lines + .iter() .map(|line| convert_talk_line_with_substitution(line)) .collect(); // Extract content after "TALK " prefix - let line_contents: Vec = converted_lines.iter() + let line_contents: Vec = converted_lines + .iter() .map(|line| { if let Some(stripped) = line.strip_prefix("TALK ") { stripped.trim().to_string() diff --git a/src/basic/compiler/mod.rs b/src/basic/compiler/mod.rs index fbf3bd61..350020d3 100644 --- a/src/basic/compiler/mod.rs +++ b/src/basic/compiler/mod.rs @@ -9,11 +9,11 @@ use diesel::QueryableByName; use diesel::ExpressionMethods; use diesel::QueryDsl; use diesel::RunQueryDsl; -use log::{trace, warn}; +use log::{info, trace, warn}; use regex::Regex; -pub mod goto_transform; pub mod blocks; +pub mod goto_transform; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::collections::HashSet; @@ -116,6 +116,11 @@ impl BasicCompiler { let source_content = fs::read_to_string(source_path) .map_err(|e| format!("Failed to read source file: {e}"))?; + // Also process tables.bas to ensure tables are created + if let Err(e) = Self::process_tables_bas(&self.state, self.bot_id) { + log::warn!("Failed to process tables.bas: {}", e); + } + if let Err(e) = process_table_definitions(Arc::clone(&self.state), self.bot_id, &source_content) { @@ -132,7 +137,8 @@ impl BasicCompiler { let source_with_suggestions = self.generate_enum_suggestions(&source_content, &tool_def)?; let ast_path = format!("{output_dir}/{file_name}.ast"); - let ast_content = self.preprocess_basic(&source_with_suggestions, source_path, self.bot_id)?; + let ast_content = + self.preprocess_basic(&source_with_suggestions, source_path, self.bot_id)?; fs::write(&ast_path, &ast_content).map_err(|e| format!("Failed to write AST file: {e}"))?; let (mcp_json, tool_json) = if tool_def.parameters.is_empty() { // No parameters — generate minimal mcp.json so USE TOOL can find this tool @@ -244,12 +250,7 @@ impl BasicCompiler { // Parse the array elements let values: Vec = array_content .split(',') - .map(|s| { - s.trim() - .trim_matches('"') - .trim_matches('\'') - .to_string() - }) + .map(|s| s.trim().trim_matches('"').trim_matches('\'').to_string()) .filter(|s| !s.is_empty()) .collect(); Some(values) @@ -470,7 +471,8 @@ impl BasicCompiler { .ok(); } - let website_regex = Regex::new(r#"(?i)USE\s+WEBSITE\s+"([^"]+)"(?:\s+REFRESH\s+"([^"]+)")?"#)?; + let website_regex = + Regex::new(r#"(?i)USE\s+WEBSITE\s+"([^"]+)"(?:\s+REFRESH\s+"([^"]+)")?"#)?; for line in source.lines() { let trimmed = line.trim(); @@ -498,7 +500,8 @@ impl BasicCompiler { .conn .get() .map_err(|e| format!("Failed to get database connection: {e}"))?; - if let Err(e) = execute_set_schedule(&mut conn, cron, &script_name, bot_id) { + if let Err(e) = execute_set_schedule(&mut conn, cron, &script_name, bot_id) + { log::error!( "Failed to schedule SET SCHEDULE during preprocessing: {}", e @@ -561,7 +564,7 @@ impl BasicCompiler { url, refresh ); } - + result.push_str(&format!("USE_WEBSITE(\"{}\", \"{}\");\n", url, refresh)); continue; } @@ -682,7 +685,11 @@ impl BasicCompiler { let table_name = table_name.trim_matches('"'); // Debug log to see what we're querying - log::trace!("Converting SAVE for table: '{}' (original: '{}')", table_name, &parts[0]); + log::trace!( + "Converting SAVE for table: '{}' (original: '{}')", + table_name, + &parts[0] + ); // Get column names from TABLE definition (preserves order from .bas file) let column_names = self.get_table_columns_for_save(table_name, bot_id)?; @@ -691,13 +698,20 @@ impl BasicCompiler { let values: Vec<&String> = parts.iter().skip(1).collect(); let mut map_pairs = Vec::new(); - log::trace!("Matching {} variables to {} columns", values.len(), column_names.len()); + log::trace!( + "Matching {} variables to {} columns", + values.len(), + column_names.len() + ); for value_var in values.iter() { // Find the column that matches this variable (case-insensitive) let value_lower = value_var.to_lowercase(); - if let Some(column_name) = column_names.iter().find(|col| col.to_lowercase() == value_lower) { + if let Some(column_name) = column_names + .iter() + .find(|col| col.to_lowercase() == value_lower) + { map_pairs.push(format!("{}: {}", column_name, value_var)); } else { log::warn!("No matching column for variable '{}'", value_var); @@ -709,13 +723,19 @@ impl BasicCompiler { *save_counter += 1; // Generate: let __save_data_N__ = #{...}; SAVE "table", __save_data_N__ - let converted = format!("let {} = {}; SAVE {}, {}", data_var, map_expr, table_name, data_var); + let converted = format!( + "let {} = {}; SAVE {}, {}", + data_var, map_expr, table_name, data_var + ); Ok(Some(converted)) } /// Parse SAVE statement into parts - fn parse_save_statement(&self, content: &str) -> Result, Box> { + fn parse_save_statement( + &self, + content: &str, + ) -> Result, Box> { // Simple parsing - split by comma, but respect quoted strings let mut parts = Vec::new(); let mut current = String::new(); @@ -759,7 +779,11 @@ impl BasicCompiler { // Try to parse TABLE definition from the bot's .bas files to get correct field order if let Ok(columns) = self.get_columns_from_table_definition(table_name, bot_id) { if !columns.is_empty() { - log::trace!("Using TABLE definition for '{}': {} columns", table_name, columns.len()); + log::trace!( + "Using TABLE definition for '{}': {} columns", + table_name, + columns.len() + ); return Ok(columns); } } @@ -778,7 +802,10 @@ impl BasicCompiler { // Find the tables.bas file in the bot's data directory let bot_name = self.get_bot_name_by_id(bot_id)?; - let tables_path = format!("/opt/gbo/data/{}.gbai/{}.gbdialog/tables.bas", bot_name, bot_name); + let tables_path = format!( + "/opt/gbo/data/{}.gbai/{}.gbdialog/tables.bas", + bot_name, bot_name + ); let tables_content = fs::read_to_string(&tables_path)?; let columns = self.parse_table_definition_for_fields(&tables_content, table_name)?; @@ -822,12 +849,63 @@ impl BasicCompiler { Ok(columns) } + /// Process tables.bas file to ensure all tables are created + pub fn process_tables_bas( + state: &Arc, + bot_id: uuid::Uuid, + ) -> Result<(), Box> { + let bot_name = Self::get_bot_name_from_state(state, bot_id)?; + let tables_path = format!( + "/opt/gbo/data/{}.gbai/{}.gbdialog/tables.bas", + bot_name, bot_name + ); + + if !Path::new(&tables_path).exists() { + trace!("tables.bas not found for bot {}, skipping", bot_name); + return Ok(()); + } + + let tables_content = fs::read_to_string(&tables_path)?; + + trace!( + "Processing tables.bas for bot {}: {}", + bot_name, + tables_path + ); + + // This will create/sync all tables defined in tables.bas + process_table_definitions(Arc::clone(state), bot_id, &tables_content)?; + + info!("Successfully processed tables.bas for bot {}", bot_name); + Ok(()) + } + + /// Get bot name from state using bot_id + fn get_bot_name_from_state( + state: &Arc, + bot_id: uuid::Uuid, + ) -> Result> { + let mut conn = state.conn.get()?; + use crate::core::shared::models::schema::bots::dsl::*; + + bots.filter(id.eq(bot_id)) + .select(name) + .first::(&mut *conn) + .map_err(|e| format!("Failed to get bot name: {}", e).into()) + } + /// Get bot name by bot_id - fn get_bot_name_by_id(&self, bot_id: uuid::Uuid) -> Result> { + fn get_bot_name_by_id( + &self, + bot_id: uuid::Uuid, + ) -> Result> { use crate::core::shared::models::schema::bots::dsl::*; use diesel::QueryDsl; - let mut conn = self.state.conn.get() + let mut conn = self + .state + .conn + .get() .map_err(|e| format!("Failed to get DB connection: {}", e))?; let bot_name: String = bots @@ -857,7 +935,10 @@ impl BasicCompiler { // First, try to get columns from the main database's information_schema // This works because tables are created in the bot's database which shares the schema - let mut conn = self.state.conn.get() + let mut conn = self + .state + .conn + .get() .map_err(|e| format!("Failed to get DB connection: {}", e))?; let query = format!( @@ -870,12 +951,15 @@ impl BasicCompiler { let columns: Vec = match sql_query(&query).load(&mut conn) { Ok(cols) => { if cols.is_empty() { - log::warn!("Found 0 columns for table '{}' in main database, trying bot database", table_name); + log::warn!( + "Found 0 columns for table '{}' in main database, trying bot database", + table_name + ); // Try bot's database as fallback when main DB returns empty let bot_pool = self.state.bot_database_manager.get_bot_pool(bot_id); if let Ok(pool) = bot_pool { - let mut bot_conn = pool.get() - .map_err(|e| format!("Bot DB error: {}", e))?; + let mut bot_conn = + pool.get().map_err(|e| format!("Bot DB error: {}", e))?; let bot_query = format!( "SELECT column_name FROM information_schema.columns \ @@ -886,13 +970,22 @@ impl BasicCompiler { match sql_query(&bot_query).load(&mut *bot_conn) { Ok(bot_cols) => { - log::trace!("Found {} columns for table '{}' in bot database", bot_cols.len(), table_name); - bot_cols.into_iter() + log::trace!( + "Found {} columns for table '{}' in bot database", + bot_cols.len(), + table_name + ); + bot_cols + .into_iter() .map(|c: ColumnRow| c.column_name) .collect() } Err(e) => { - log::error!("Failed to get columns from bot DB for '{}': {}", table_name, e); + log::error!( + "Failed to get columns from bot DB for '{}': {}", + table_name, + e + ); Vec::new() } } @@ -901,20 +994,25 @@ impl BasicCompiler { Vec::new() } } else { - log::trace!("Found {} columns for table '{}' in main database", cols.len(), table_name); - cols.into_iter() - .map(|c: ColumnRow| c.column_name) - .collect() + log::trace!( + "Found {} columns for table '{}' in main database", + cols.len(), + table_name + ); + cols.into_iter().map(|c: ColumnRow| c.column_name).collect() } } Err(e) => { - log::warn!("Failed to get columns for table '{}' from main DB: {}", table_name, e); + log::warn!( + "Failed to get columns for table '{}' from main DB: {}", + table_name, + e + ); // Try bot's database as fallback let bot_pool = self.state.bot_database_manager.get_bot_pool(bot_id); if let Ok(pool) = bot_pool { - let mut bot_conn = pool.get() - .map_err(|e| format!("Bot DB error: {}", e))?; + let mut bot_conn = pool.get().map_err(|e| format!("Bot DB error: {}", e))?; let bot_query = format!( "SELECT column_name FROM information_schema.columns \ @@ -925,14 +1023,22 @@ impl BasicCompiler { match sql_query(&bot_query).load(&mut *bot_conn) { Ok(cols) => { - log::trace!("Found {} columns for table '{}' in bot database", cols.len(), table_name); + log::trace!( + "Found {} columns for table '{}' in bot database", + cols.len(), + table_name + ); cols.into_iter() .filter(|c: &ColumnRow| c.column_name != "id") .map(|c: ColumnRow| c.column_name) .collect() } Err(e) => { - log::error!("Failed to get columns from bot DB for '{}': {}", table_name, e); + log::error!( + "Failed to get columns from bot DB for '{}': {}", + table_name, + e + ); Vec::new() } } diff --git a/src/basic/keywords/detect.rs b/src/basic/keywords/detect.rs index fe959202..68b899d8 100644 --- a/src/basic/keywords/detect.rs +++ b/src/basic/keywords/detect.rs @@ -2,10 +2,11 @@ use crate::core::shared::models::UserSession; use crate::core::shared::state::AppState; use diesel::prelude::*; use diesel::sql_types::*; -use log::error; +use log::{error, trace}; use rhai::{Dynamic, Engine}; use serde_json::Value; use std::sync::Arc; +use uuid::Uuid; #[derive(Debug, QueryableByName)] struct ColumnRow { @@ -13,8 +14,9 @@ struct ColumnRow { column_name: String, } -pub fn detect_keyword(state: Arc, _user: UserSession, engine: &mut Engine) { +pub fn detect_keyword(state: Arc, user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); + let bot_id = user.bot_id; engine .register_custom_syntax(["DETECT", "$expr$"], false, move |context, inputs| { @@ -27,6 +29,7 @@ pub fn detect_keyword(state: Arc, _user: UserSession, engine: &mut Eng let table_name = context.eval_expression_tree(first_input)?.to_string(); let state_for_thread = Arc::clone(&state_clone); + let bot_id_for_thread = bot_id; let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { @@ -37,7 +40,7 @@ pub fn detect_keyword(state: Arc, _user: UserSession, engine: &mut Eng let send_err = if let Ok(rt) = rt { let result = rt.block_on(async move { - detect_anomalies_in_table(state_for_thread, &table_name).await + detect_anomalies_in_table(state_for_thread, &table_name, bot_id_for_thread).await }); tx.send(result).err() } else { @@ -73,10 +76,13 @@ pub fn detect_keyword(state: Arc, _user: UserSession, engine: &mut Eng async fn detect_anomalies_in_table( state: Arc, table_name: &str, + bot_id: Uuid, ) -> Result> { - let columns = get_table_columns(&state, table_name)?; + let columns = get_table_columns(&state, table_name, bot_id)?; let value_field = find_numeric_field(&columns); + trace!("DETECT: columns = {:?}, value_field = {}", columns, value_field); + #[derive(QueryableByName)] struct JsonRow { #[diesel(sql_type = Text)] @@ -89,8 +95,9 @@ async fn detect_anomalies_in_table( column_list, table_name ); + let pool = state.bot_database_manager.get_bot_pool(bot_id)?; let rows: Vec = diesel::sql_query(&query) - .load(&mut state.conn.get()?)?; + .load(&mut pool.get()?)?; let records: Vec = rows .into_iter() @@ -108,7 +115,7 @@ async fn detect_anomalies_in_table( let client = reqwest::Client::new(); let response = client - .post(format!("{}/api/anomaly/detect", botmodels_host)) + .post(format!("{}/api/detect", botmodels_host)) .header("X-API-Key", &botmodels_key) .json(&serde_json::json!({ "data": records, @@ -129,14 +136,16 @@ async fn detect_anomalies_in_table( fn get_table_columns( state: &Arc, table_name: &str, + bot_id: Uuid, ) -> Result, Box> { let query = format!( "SELECT column_name FROM information_schema.columns WHERE table_name = '{}' ORDER BY ordinal_position", table_name ); + let pool = state.bot_database_manager.get_bot_pool(bot_id)?; let rows: Vec = diesel::sql_query(&query) - .load(&mut state.conn.get()?)?; + .load(&mut pool.get()?)?; Ok(rows.into_iter().map(|r| r.column_name).collect()) } @@ -144,7 +153,8 @@ fn get_table_columns( fn find_numeric_field(columns: &[String]) -> String { let numeric_keywords = ["salario", "salary", "valor", "value", "amount", "preco", "price", "temperatura", "temp", "pressao", "pressure", "quantidade", "quantity", - "decimal", "numerico", "numeric", "base", "liquido", "bruto"]; + "decimal", "numerico", "numeric", "base", "liquido", "bruto", + "desconto", "vantagem", "gratificacao"]; for col in columns { let col_lower = col.to_lowercase(); @@ -155,5 +165,17 @@ fn find_numeric_field(columns: &[String]) -> String { } } - columns.first().cloned().unwrap_or_else(|| "value".to_string()) + let skip_keywords = ["id", "nome", "cpf", "matricula", "orgao", "cargo", "nivel", + "mes", "ano", "tipo", "categoria", "data", "status", "email", + "telefone", "protocolo", "servidor"]; + + for col in columns { + let col_lower = col.to_lowercase(); + let is_string = skip_keywords.iter().any(|kw| col_lower.contains(kw)); + if !is_string { + return col.clone(); + } + } + + columns.last().cloned().unwrap_or_else(|| "value".to_string()) } diff --git a/src/basic/keywords/enhanced_llm.rs b/src/basic/keywords/enhanced_llm.rs index f364b359..59f19ebe 100644 --- a/src/basic/keywords/enhanced_llm.rs +++ b/src/basic/keywords/enhanced_llm.rs @@ -7,94 +7,58 @@ use rhai::Engine; #[cfg(feature = "llm")] use rhai::{Dynamic, Engine}; use std::sync::Arc; +use std::time::Duration; #[cfg(feature = "llm")] -pub fn register_enhanced_llm_keyword(state: Arc, user: UserSession, engine: &mut Engine) { - let state_clone1 = Arc::clone(&state); - let state_clone2 = Arc::clone(&state); - let user_clone = user; +pub fn register_enhanced_llm_keyword(state: Arc, _user: UserSession, engine: &mut Engine) { + let state_clone = Arc::clone(&state); if let Err(e) = engine.register_custom_syntax( - ["LLM", "$string$", "WITH", "OPTIMIZE", "FOR", "$string$"], + ["LLM", "$string$"], false, 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_clone1); - let _user_clone_spawn = user_clone.clone(); - - tokio::spawn(async move { - let router = SmartLLMRouter::new(state_for_spawn); - let goal = OptimizationGoal::from_str_name(&optimization); - - match crate::llm::smart_router::enhanced_llm_call( - &router, &prompt, goal, None, None, - ) - .await - { - Ok(_response) => { - log::info!("LLM response generated with {} optimization", optimization); - } - Err(e) => { - log::error!("Enhanced LLM call failed: {}", e); - } + let state_for_thread = Arc::clone(&state_clone); + let (tx, rx) = std::sync::mpsc::channel(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_all() + .build(); + + if let Ok(rt) = rt { + let result = rt.block_on(async move { + let router = SmartLLMRouter::new(Arc::clone(&state_for_thread)); + crate::llm::smart_router::enhanced_llm_call( + &state_for_thread, &router, &prompt, OptimizationGoal::Balanced, None, None, + ) + .await + }); + let _ = tx.send(result); } }); - - Ok(Dynamic::from("LLM response")) - }, - ) { - log::warn!("Failed to register enhanced LLM syntax: {e}"); - } - - if let Err(e) = engine.register_custom_syntax( - [ - "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_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 - { - Ok(_response) => { - log::info!( - "LLM response with constraints: cost<={}, latency<={}", - max_cost, - max_latency - ); - } - Err(e) => { - log::error!("Constrained LLM call failed: {}", e); - } + + match rx.recv_timeout(Duration::from_secs(60)) { + Ok(Ok(response)) => Ok(Dynamic::from(response)), + Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + e.to_string().into(), + rhai::Position::NONE, + ))), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { + Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + "LLM generation timed out".into(), + rhai::Position::NONE, + ))) } - }); - - Ok(Dynamic::from("LLM response")) + Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + format!("LLM thread failed: {e}").into(), + rhai::Position::NONE, + ))), + } }, ) { - log::warn!("Failed to register constrained LLM syntax: {e}"); + log::warn!("Failed to register simple LLM syntax: {e}"); } } diff --git a/src/basic/mod.rs b/src/basic/mod.rs index ea53616d..a83d3022 100644 --- a/src/basic/mod.rs +++ b/src/basic/mod.rs @@ -325,6 +325,10 @@ impl ScriptService { let _ = self; // silence unused self warning - kept for API consistency let script = preprocess_switch(script); + // Preprocess LLM keyword to add WITH OPTIMIZE FOR "speed" syntax + // This is needed because Rhai's custom syntax requires the full syntax + let script = Self::preprocess_llm_keyword(&script); + // Convert ALL multi-word keywords to underscore versions (e.g., "USE WEBSITE" → "USE_WEBSITE") // This avoids Rhai custom syntax conflicts and makes the system more secure let script = Self::convert_multiword_keywords(&script); @@ -2051,6 +2055,59 @@ impl ScriptService { word.to_lowercase() } } + + fn preprocess_llm_keyword(script: &str) -> String { + // Transform LLM "prompt" to LLM "prompt" WITH OPTIMIZE FOR "speed" + // Handle cases like: + // LLM "text" + // LLM "text" + var + // result = LLM "text" + // result = LLM "text" + var + + let mut result = String::new(); + let chars: Vec = script.chars().collect(); + let mut i = 0; + + while i < chars.len() { + // Check for LLM keyword (case insensitive) + let remaining: String = chars[i..].iter().collect(); + let remaining_upper = remaining.to_uppercase(); + + if remaining_upper.starts_with("LLM ") { + // Found LLM - copy "LLM " and find the quoted string + result.push_str("LLM "); + i += 4; + + // Now find the quoted string + if i < chars.len() && chars[i] == '"' { + result.push('"'); + i += 1; + + // Copy quoted string + while i < chars.len() && chars[i] != '"' { + result.push(chars[i]); + i += 1; + } + if i < chars.len() && chars[i] == '"' { + result.push('"'); + i += 1; + } + + // Add WITH OPTIMIZE FOR "speed" if not present + let before_with = result.trim_end_matches('"'); + if !before_with.to_uppercase().contains("WITH OPTIMIZE") { + result = format!("{} WITH OPTIMIZE FOR \"speed\"", before_with); + } + // Continue copying rest of line in outer loop (don't break) + } + } else { + result.push(chars[i]); + i += 1; + } + } + + result + } } diff --git a/src/core/bootstrap/bootstrap_manager.rs b/src/core/bootstrap/bootstrap_manager.rs index c430d8fa..239ad48c 100644 --- a/src/core/bootstrap/bootstrap_manager.rs +++ b/src/core/bootstrap/bootstrap_manager.rs @@ -186,10 +186,13 @@ impl BootstrapManager { let config_path = self.stack_dir("conf/system/directory_config.json"); if !config_path.exists() { info!("Creating OAuth client for Directory service..."); + #[cfg(feature = "directory")] match crate::core::package_manager::setup_directory().await { Ok(_) => info!("OAuth client created successfully"), Err(e) => warn!("Failed to create OAuth client: {}", e), } + #[cfg(not(feature = "directory"))] + info!("Directory feature not enabled, skipping OAuth setup"); } else { info!("Directory config already exists, skipping OAuth setup"); } @@ -218,10 +221,13 @@ impl BootstrapManager { let config_path = self.stack_dir("conf/system/directory_config.json"); if !config_path.exists() { info!("Creating OAuth client for Directory service..."); + #[cfg(feature = "directory")] match crate::core::package_manager::setup_directory().await { Ok(_) => info!("OAuth client created successfully"), Err(e) => warn!("Failed to create OAuth client: {}", e), } + #[cfg(not(feature = "directory"))] + info!("Directory feature not enabled, skipping OAuth setup"); } } } diff --git a/src/core/shared/admin_email.rs b/src/core/shared/admin_email.rs index 1f3800b4..0f8b7818 100644 --- a/src/core/shared/admin_email.rs +++ b/src/core/shared/admin_email.rs @@ -1,5 +1,7 @@ // Email invitation functions +#[cfg(feature = "mail")] use log::warn; +#[cfg(feature = "mail")] use uuid::Uuid; #[cfg(feature = "mail")] use lettre::{ diff --git a/src/core/shared/models/mod.rs b/src/core/shared/models/mod.rs index 7b8c535c..228fce26 100644 --- a/src/core/shared/models/mod.rs +++ b/src/core/shared/models/mod.rs @@ -1,4 +1,3 @@ - pub mod core; pub use self::core::*; @@ -17,11 +16,11 @@ pub use super::schema; // Re-export core schema tables pub use super::schema::{ - basic_tools, bot_configuration, bot_memories, bots, clicks, - message_history, organizations, rbac_group_roles, rbac_groups, - rbac_permissions, rbac_role_permissions, rbac_roles, rbac_user_groups, rbac_user_roles, - session_tool_associations, system_automations, user_login_tokens, - user_preferences, user_sessions, users, workflow_executions, workflow_events, bot_shared_memory, + basic_tools, bot_configuration, bot_memories, bot_shared_memory, bots, clicks, message_history, + organizations, rbac_group_roles, rbac_groups, rbac_permissions, rbac_role_permissions, + rbac_roles, rbac_user_groups, rbac_user_roles, session_tool_associations, system_automations, + user_login_tokens, user_preferences, user_sessions, users, workflow_events, + workflow_executions, }; // Re-export feature-gated schema tables @@ -31,28 +30,23 @@ pub use super::schema::tasks; #[cfg(feature = "mail")] pub use super::schema::{ distribution_lists, email_auto_responders, email_drafts, email_folders, - email_label_assignments, email_labels, email_rules, email_signatures, - email_templates, global_email_signatures, scheduled_emails, - shared_mailbox_members, shared_mailboxes, user_email_accounts, + email_label_assignments, email_labels, email_rules, email_signatures, email_templates, + global_email_signatures, scheduled_emails, shared_mailbox_members, shared_mailboxes, + user_email_accounts, }; #[cfg(feature = "people")] pub use super::schema::{ - crm_accounts, crm_activities, crm_contacts, crm_leads, crm_notes, - crm_opportunities, crm_pipeline_stages, people, people_departments, - people_org_chart, people_person_skills, people_skills, people_team_members, - people_teams, people_time_off, -}; - -#[cfg(feature = "vectordb")] -pub use super::schema::{ - kb_collections, kb_documents, kb_group_associations, user_kb_associations, + crm_accounts, crm_activities, crm_contacts, crm_leads, crm_notes, crm_opportunities, + crm_pipeline_stages, people, people_departments, people_org_chart, people_person_skills, + people_skills, people_team_members, people_teams, people_time_off, }; +#[cfg(feature = "rbac")] +pub use super::schema::kb_group_associations; pub use botlib::message_types::MessageType; pub use botlib::models::{ApiResponse, Attachment, BotResponse, Session, Suggestion, UserMessage}; // Manually export OrganizationInvitation as it is defined in core but table is organization_invitations pub use self::core::OrganizationInvitation; - diff --git a/src/llm/smart_router.rs b/src/llm/smart_router.rs index 9ebe9b8e..969a2e79 100644 --- a/src/llm/smart_router.rs +++ b/src/llm/smart_router.rs @@ -1,4 +1,6 @@ use crate::core::shared::state::AppState; +use crate::llm::OpenAIClient; +use crate::core::config::ConfigManager; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; @@ -144,6 +146,7 @@ impl SmartLLMRouter { // Enhanced LLM keyword with optimization pub async fn enhanced_llm_call( + state: &Arc, router: &SmartLLMRouter, prompt: &str, optimization_goal: OptimizationGoal, @@ -152,17 +155,37 @@ pub async fn enhanced_llm_call( ) -> Result> { let start_time = Instant::now(); - // Select optimal model + // Select optimal model (for tracking) 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); + // Get actual LLM configuration from bot's config + let config_manager = ConfigManager::new(state.conn.clone()); + let actual_model = config_manager + .get_config(&uuid::Uuid::nil(), "llm-model", None) + .unwrap_or_else(|_| model.clone()); + let key = config_manager + .get_config(&uuid::Uuid::nil(), "llm-key", None) + .unwrap_or_else(|_| String::new()); + + // Build messages for LLM call + let messages = OpenAIClient::build_messages( + "Você é um assistente útil que resume dados em português.", + "", + &[("user".to_string(), prompt.to_string())], + ); + + // Make actual LLM call + let response = state + .llm_provider + .generate(prompt, &messages, &actual_model, &key) + .await + .map_err(|e| format!("LLM error: {}", e))?; // Track performance let latency = start_time.elapsed().as_millis() as u64; - let cost_per_token = match model.as_str() { + let cost_per_token = match actual_model.as_str() { "gpt-4" => 0.03, "gpt-4o-mini" => 0.0015, "claude-3-sonnet" => 0.015, @@ -170,7 +193,7 @@ pub async fn enhanced_llm_call( }; router - .track_performance(&model, latency, cost_per_token, true) + .track_performance(&actual_model, latency, cost_per_token, true) .await?; Ok(response) diff --git a/src/main_module/server.rs b/src/main_module/server.rs index ceb4220f..6b049a7d 100644 --- a/src/main_module/server.rs +++ b/src/main_module/server.rs @@ -187,6 +187,72 @@ pub async fn run_axum_server( .nest(ApiUrls::AUTH, crate::directory::auth_routes::configure()); } + #[cfg(not(feature = "directory"))] + { + use axum::extract::State; + use axum::response::IntoResponse; + use std::collections::HashMap; + + async fn anonymous_auth_handler( + State(state): State>, + axum::extract::Query(params): axum::extract::Query>, + ) -> impl IntoResponse { + let bot_name = params.get("bot_name").cloned().unwrap_or_default(); + let existing_session_id = params.get("session_id").cloned(); + let existing_user_id = params.get("user_id").cloned(); + + let user_id = existing_user_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); + let session_id = existing_session_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); + + // Create session in DB if it doesn't exist + let session_uuid = match uuid::Uuid::parse_str(&session_id) { + Ok(uuid) => uuid, + Err(_) => uuid::Uuid::new_v4(), + }; + let user_uuid = match uuid::Uuid::parse_str(&user_id) { + Ok(uuid) => uuid, + Err(_) => uuid::Uuid::new_v4(), + }; + + // Get bot_id from bot_name + let bot_id = { + let conn = state.conn.get().ok(); + if let Some(mut db_conn) = conn { + use crate::core::shared::models::schema::bots::dsl::*; + use diesel::prelude::*; + bots.filter(name.eq(&bot_name)) + .select(id) + .first::(&mut db_conn) + .ok() + .unwrap_or_else(uuid::Uuid::nil) + } else { + uuid::Uuid::nil() + } + }; + + // Create session if it doesn't exist + let _ = { + let mut sm = state.session_manager.lock().await; + sm.get_or_create_anonymous_user(Some(user_uuid)).ok(); + sm.create_session(user_uuid, bot_id, "Anonymous Chat").ok() + }; + + info!("Anonymous auth for bot: {}, session: {}", bot_name, session_id); + + ( + axum::http::StatusCode::OK, + Json(serde_json::json!({ + "user_id": user_id, + "session_id": session_id, + "bot_name": bot_name, + "status": "anonymous" + })), + ) + } + + api_router = api_router.route(ApiUrls::AUTH, get(anonymous_auth_handler)); + } + #[cfg(feature = "meet")] { api_router = api_router.merge(crate::meet::configure()); @@ -395,8 +461,11 @@ pub async fn run_axum_server( api_router = api_router.merge(crate::api::editor::configure_editor_routes()); api_router = api_router.merge(crate::api::database::configure_database_routes()); api_router = api_router.merge(crate::api::git::configure_git_routes()); - api_router = api_router.merge(crate::api::terminal::configure_terminal_routes()); api_router = api_router.merge(crate::browser::api::configure_browser_routes()); + #[cfg(feature = "terminal")] + { + api_router = api_router.merge(crate::api::terminal::configure_terminal_routes()); + } let site_path = app_state .config diff --git a/src/settings/mod.rs b/src/settings/mod.rs index fd484937..d8e1b6ad 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -2,6 +2,8 @@ pub mod audit_log; pub mod menu_config; pub mod permission_inheritance; pub mod rbac; + +#[cfg(feature = "rbac")] pub mod rbac_kb; pub mod rbac_ui; pub mod security_admin; @@ -328,8 +330,9 @@ r##"
async fn get_trusted_devices(State(_state): State>) -> Html { Html( -r##"
+r####"
+ "## 💻
Current Device @@ -338,4 +341,4 @@ r##"
Trusted -

No other trusted devices

"## .to_string(), ) } +

No other trusted devices

"#### .to_string(), ) } diff --git a/src/settings/rbac.rs b/src/settings/rbac.rs index 844afab7..60c2697a 100644 --- a/src/settings/rbac.rs +++ b/src/settings/rbac.rs @@ -18,6 +18,7 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use uuid::Uuid; +#[cfg(feature = "rbac")] use crate::settings::rbac_kb::{ assign_kb_to_group, get_accessible_kbs_for_user, get_kb_groups, remove_kb_from_group, }; @@ -38,10 +39,6 @@ pub fn configure_rbac_routes() -> Router> { .route("/api/rbac/groups/{group_id}/roles", get(get_group_roles)) .route("/api/rbac/groups/{group_id}/roles/{role_id}", post(assign_role_to_group).delete(remove_role_from_group)) .route("/api/rbac/users/{user_id}/permissions", get(get_effective_permissions)) - // KB-group management - .route("/api/rbac/kbs/{kb_id}/groups", get(get_kb_groups)) - .route("/api/rbac/kbs/{kb_id}/groups/{group_id}", post(assign_kb_to_group).delete(remove_kb_from_group)) - .route("/api/rbac/users/{user_id}/accessible-kbs", get(get_accessible_kbs_for_user)) .route("/settings/rbac", get(rbac_settings_page)) .route("/settings/rbac/users", get(rbac_users_list)) .route("/settings/rbac/roles", get(rbac_roles_list))