Compare commits

...

2 commits

Author SHA1 Message Date
90c14bcd09 Fix DETECT: use bot-specific DB pool, add anonymous auth when directory disabled
All checks were successful
BotServer CI/CD / build (push) Successful in 12m42s
2026-04-06 13:37:23 -03:00
8d3c28e441 Fix SMTP: use starttls_relay for port 587, relay for 465 2026-04-06 11:00:23 -03:00
25 changed files with 548 additions and 264 deletions

View file

@ -10,7 +10,12 @@ features = ["database", "i18n"]
[features] [features]
# ===== DEFAULT ===== # ===== 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"] browser = ["automation", "drive", "cache"]
terminal = ["automation", "drive", "cache"] terminal = ["automation", "drive", "cache"]
@ -21,7 +26,8 @@ scripting = ["dep:rhai"]
automation = ["scripting", "dep:cron"] automation = ["scripting", "dep:cron"]
drive = ["dep:aws-config", "dep:aws-sdk-s3", "dep:aws-smithy-async", "dep:pdf-extract", "dep:notify"] drive = ["dep:aws-config", "dep:aws-sdk-s3", "dep:aws-smithy-async", "dep:pdf-extract", "dep:notify"]
cache = ["dep:redis"] cache = ["dep:redis"]
directory = [] directory = ["rbac"]
rbac = []
crawler = ["drive", "cache"] crawler = ["drive", "cache"]
# ===== APPS (Each includes what it needs from core) ===== # ===== APPS (Each includes what it needs from core) =====
@ -90,6 +96,7 @@ console = ["automation", "drive", "cache", "dep:crossterm", "dep:ratatui"]
# ===== BUNDLES (Optional - for convenience) ===== # ===== BUNDLES (Optional - for convenience) =====
minimal = ["chat"] minimal = ["chat"]
minimal-chat = ["chat", "automation", "drive", "cache"] # No security at all
lightweight = ["chat", "tasks", "people"] lightweight = ["chat", "tasks", "people"]
full = ["chat", "people", "mail", "tasks", "calendar", "drive", "docs", "llm", "cache", "compliance"] full = ["chat", "people", "mail", "tasks", "calendar", "drive", "docs", "llm", "cache", "compliance"]
embed-ui = ["dep:rust-embed"] embed-ui = ["dep:rust-embed"]

View file

@ -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" PARAM numeroVisitantes AS INTEGER LIKE "3" DESCRIPTION "Número de visitantes"
id = "VIS-" + FORMAT(NOW(), "yyyyMMdd") + "-" + FORMAT(RANDOM(1000, 9999), "0000") id = "VIS-" + FORMAT(NOW(), "yyyyMMdd") + "-" + FORMAT(RANDOM(1000, 9999), "0000")
protocoloNumero = "VIS" + FORMAT(RANDOM(100000, 999999), "000000") protocoloNumero = "VIS" + FORMAT(RANDOM(100000, 999999), "000000")
dataCadastro = FORMAT(NOW(), "yyyy-MM-dd HH:mm:ss") dataCadastro = FORMAT(NOW(), "yyyy-MM-dd HH:mm:ss")

View file

@ -4,15 +4,15 @@
"input_schema": { "input_schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"telefone": { "horario": {
"type": "string", "type": "string",
"description": "Telefone com DDD", "description": "Horário preferencial (formato HH:MM, entre 8h e 17h)",
"example": "(21) 99999-9999" "example": "10:00"
}, },
"email": { "nomeResponsavel": {
"type": "string", "type": "string",
"description": "Email para contato", "description": "Nome do responsável",
"example": "joao@example.com" "example": "João Silva"
}, },
"dataVisita": { "dataVisita": {
"type": "string", "type": "string",
@ -20,20 +20,20 @@
"example": "2026-03-15", "example": "2026-03-15",
"format": "date" "format": "date"
}, },
"horario": { "email": {
"type": "string", "type": "string",
"description": "Horário preferencial (formato HH:MM, entre 8h e 17h)", "description": "Email para contato",
"example": "10:00" "example": "joao@example.com"
}, },
"numeroVisitantes": { "numeroVisitantes": {
"type": "integer", "type": "integer",
"description": "Número de visitantes", "description": "Número de visitantes",
"example": "3" "example": "3"
}, },
"nomeResponsavel": { "telefone": {
"type": "string", "type": "string",
"description": "Nome do responsável", "description": "Telefone com DDD",
"example": "João Silva" "example": "(21) 99999-9999"
} }
}, },
"required": [ "required": [

View file

@ -6,11 +6,15 @@
"parameters": { "parameters": {
"type": "object", "type": "object",
"properties": { "properties": {
"dataVisita": { "nomeResponsavel": {
"type": "string", "type": "string",
"description": "Data desejada para visita (formato ISO: YYYY-MM-DD)", "description": "Nome do responsável",
"example": "2026-03-15", "example": "João Silva"
"format": "date" },
"email": {
"type": "string",
"description": "Email para contato",
"example": "joao@example.com"
}, },
"numeroVisitantes": { "numeroVisitantes": {
"type": "integer", "type": "integer",
@ -22,15 +26,11 @@
"description": "Telefone com DDD", "description": "Telefone com DDD",
"example": "(21) 99999-9999" "example": "(21) 99999-9999"
}, },
"email": { "dataVisita": {
"type": "string", "type": "string",
"description": "Email para contato", "description": "Data desejada para visita (formato ISO: YYYY-MM-DD)",
"example": "joao@example.com" "example": "2026-03-15",
}, "format": "date"
"nomeResponsavel": {
"type": "string",
"description": "Nome do responsável",
"example": "João Silva"
}, },
"horario": { "horario": {
"type": "string", "type": "string",

View file

@ -4,11 +4,6 @@
"input_schema": { "input_schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"email": {
"type": "string",
"description": "Email para contato",
"example": "joao@example.com"
},
"nome": { "nome": {
"type": "string", "type": "string",
"description": "Nome do interessado", "description": "Nome do interessado",
@ -24,6 +19,11 @@
"description": "Mensagem completa", "description": "Mensagem completa",
"example": "Gostaria de saber as formas de pagamento disponíveis" "example": "Gostaria de saber as formas de pagamento disponíveis"
}, },
"email": {
"type": "string",
"description": "Email para contato",
"example": "joao@example.com"
},
"telefone": { "telefone": {
"type": "string", "type": "string",
"description": "Telefone para retorno", "description": "Telefone para retorno",

View file

@ -4,46 +4,46 @@
"input_schema": { "input_schema": {
"type": "object", "type": "object",
"properties": { "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": { "telefone": {
"type": "string", "type": "string",
"description": "Telefone com DDD", "description": "Telefone com DDD",
"example": "(21) 99999-9999" "example": "(21) 99999-9999"
}, },
"turno": {
"type": "string",
"description": "Turno: MANHA ou TARDE",
"example": "MANHA"
},
"serie": { "serie": {
"type": "string", "type": "string",
"description": "Série desejada", "description": "Série desejada",
"example": "8º ano" "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": { "nomeResponsavel": {
"type": "string", "type": "string",
"description": "Nome do responsável", "description": "Nome do responsável",
"example": "Maria Silva Santos" "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": [ "required": [

View file

@ -6,27 +6,6 @@
"parameters": { "parameters": {
"type": "object", "type": "object",
"properties": { "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": { "turno": {
"type": "string", "type": "string",
"description": "Turno: MANHA ou TARDE", "description": "Turno: MANHA ou TARDE",
@ -36,11 +15,32 @@
"TARDE" "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": { "email": {
"type": "string", "type": "string",
"description": "Email para contato", "description": "Email para contato",
"example": "maria.santos@example.com" "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": { "endereco": {
"type": "string", "type": "string",
"description": "Endereço completo", "description": "Endereço completo",

View file

@ -1,21 +1,20 @@
USE_WEBSITE("https://salesianos.br", "30d"); USE_WEBSITE("https://salesianos.br", "30d");
USE_KB "carta"; USE KB "carta";
USE_KB "proc"; USE KB "proc";
USE TOOL "inscricao"; USE TOOL "inscricao";
USE TOOL "consultar_inscricao"; USE TOOL "consultar_inscricao";
USE TOOL "agendamento_visita"; USE TOOL "agendamento_visita";
USE TOOL "informacoes_curso"; USE TOOL "informacoes_curso";
USE TOOL "documentos_necessarios"; USE TOOL "documentos_necessarios";
USE TOOL "contato_secretaria"; USE TOOL "contato_secretaria";
USE TOOL "";
USE TOOL "calendario_letivo"; USE TOOL "calendario_letivo";
ADD_SUGGESTION_TOOL "inscricao" as "📚 Fazer Inscrição"; ADD_SUGGESTION_TOOL "inscricao" as "Fazer Inscrição";
ADD_SUGGESTION_TOOL "consultar_inscricao" as "🔍 Consultar Inscrição"; ADD_SUGGESTION_TOOL "consultar_inscricao" as "Consultar Inscrição";
ADD_SUGGESTION_TOOL "agendamento_visita" as "🏫 Agendar Visita"; ADD_SUGGESTION_TOOL "agendamento_visita" as "Agendar Visita";
ADD_SUGGESTION_TOOL "informacoes_curso" as "📖 Informações de Cursos"; ADD_SUGGESTION_TOOL "informacoes_curso" as "Informações de Cursos";
ADD_SUGGESTION_TOOL "documentos_necessarios" as "📋 Documentos Necessários"; ADD_SUGGESTION_TOOL "documentos_necessarios" as "Documentos Necessários";
ADD_SUGGESTION_TOOL "contato_secretaria" as "📞 Falar com Secretaria"; ADD_SUGGESTION_TOOL "contato_secretaria" as "Falar com Secretaria";
ADD_SUGGESTION_TOOL "" as "Segunda Via de Boleto"; ADD_SUGGESTION_TOOL "segunda_via" as "Segunda Via de Boleto";
ADD_SUGGESTION_TOOL "calendario_letivo" as "📅 Calendário Letivo"; ADD_SUGGESTION_TOOL "calendario_letivo" as "Calendário Letivo";
ADD_SUGGESTION_TOOL "outros" as "Outros"; 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?"; 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?";

View file

@ -1,7 +1,8 @@
USE_WEBSITE("https://salesianos.br", "30d") USE_WEBSITE("https://salesianos.br", "30d")
USE_KB "carta" USE KB "carta"
USE_KB "proc" USE KB "proc"
USE TOOL "inscricao" USE TOOL "inscricao"
USE TOOL "consultar_inscricao" USE TOOL "consultar_inscricao"
@ -9,17 +10,16 @@ USE TOOL "agendamento_visita"
USE TOOL "informacoes_curso" USE TOOL "informacoes_curso"
USE TOOL "documentos_necessarios" USE TOOL "documentos_necessarios"
USE TOOL "contato_secretaria" USE TOOL "contato_secretaria"
USE TOOL ""
USE TOOL "calendario_letivo" USE TOOL "calendario_letivo"
ADD_SUGGESTION_TOOL "inscricao" AS "📚 Fazer Inscrição" ADD_SUGGESTION_TOOL "inscricao" AS "Fazer Inscrição"
ADD_SUGGESTION_TOOL "consultar_inscricao" AS "🔍 Consultar Inscrição" ADD_SUGGESTION_TOOL "consultar_inscricao" AS "Consultar Inscrição"
ADD_SUGGESTION_TOOL "agendamento_visita" AS "🏫 Agendar Visita" ADD_SUGGESTION_TOOL "agendamento_visita" AS "Agendar Visita"
ADD_SUGGESTION_TOOL "informacoes_curso" AS "📖 Informações de Cursos" ADD_SUGGESTION_TOOL "informacoes_curso" AS "Informações de Cursos"
ADD_SUGGESTION_TOOL "documentos_necessarios" AS "📋 Documentos Necessários" ADD_SUGGESTION_TOOL "documentos_necessarios" AS "Documentos Necessários"
ADD_SUGGESTION_TOOL "contato_secretaria" AS "📞 Falar com Secretaria" ADD_SUGGESTION_TOOL "contato_secretaria" AS "Falar com Secretaria"
ADD_SUGGESTION_TOOL "" AS "Segunda Via de Boleto" ADD_SUGGESTION_TOOL "segunda_via" AS "Segunda Via de Boleto"
ADD_SUGGESTION_TOOL "calendario_letivo" AS "📅 Calendário Letivo" ADD_SUGGESTION_TOOL "calendario_letivo" AS "Calendário Letivo"
ADD_SUGGESTION_TOOL "outros" AS "Outros" ADD_SUGGESTION_TOOL "outros" AS "Outros"
REM Validar região para escolha de secretaria. REM Validar região para escolha de secretaria.
REM Sincronizar as bases entre o Bot e a Org. REM Sincronizar as bases entre o Bot e a Org.

View file

@ -1,4 +1,5 @@
pub mod editor; pub mod editor;
pub mod database; pub mod database;
pub mod git; pub mod git;
#[cfg(feature = "terminal")]
pub mod terminal; pub mod terminal;

View file

@ -1,3 +1,4 @@
#[cfg(feature = "terminal")]
use axum::{ use axum::{
extract::{ extract::{
Query, Query,

View file

@ -44,7 +44,14 @@ pub fn convert_mail_line_with_substitution(line: &str) -> String {
current_var.clear(); current_var.clear();
} }
_ if in_substitution => { _ 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); current_var.push(c);
} }
} }
@ -136,7 +143,10 @@ pub fn convert_mail_block(recipient: &str, lines: &[String]) -> String {
} else { } else {
recipient.to_string() 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); trace!("Converted MAIL block → {}", result);
result result

View file

@ -30,7 +30,10 @@ pub fn convert_begin_blocks(script: &str) -> String {
} }
if upper == "END TALK" { 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; in_talk_block = false;
let converted = convert_talk_block(&talk_block_lines); let converted = convert_talk_block(&talk_block_lines);
result.push_str(&converted); result.push_str(&converted);
@ -53,7 +56,10 @@ pub fn convert_begin_blocks(script: &str) -> String {
} }
if upper == "END MAIL" { 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; in_mail_block = false;
let converted = convert_mail_block(&mail_recipient, &mail_block_lines); let converted = convert_mail_block(&mail_recipient, &mail_block_lines);
result.push_str(&converted); result.push_str(&converted);

View file

@ -53,7 +53,14 @@ pub fn convert_talk_line_with_substitution(line: &str) -> String {
} }
} }
_ if in_substitution => { _ 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); current_var.push(c);
} else if c == '(' { } else if c == '(' {
current_var.push(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 { pub fn convert_talk_block(lines: &[String]) -> String {
// Convert all lines first // Convert all lines first
let converted_lines: Vec<String> = lines.iter() let converted_lines: Vec<String> = lines
.iter()
.map(|line| convert_talk_line_with_substitution(line)) .map(|line| convert_talk_line_with_substitution(line))
.collect(); .collect();
// Extract content after "TALK " prefix // Extract content after "TALK " prefix
let line_contents: Vec<String> = converted_lines.iter() let line_contents: Vec<String> = converted_lines
.iter()
.map(|line| { .map(|line| {
if let Some(stripped) = line.strip_prefix("TALK ") { if let Some(stripped) = line.strip_prefix("TALK ") {
stripped.trim().to_string() stripped.trim().to_string()

View file

@ -9,11 +9,11 @@ use diesel::QueryableByName;
use diesel::ExpressionMethods; use diesel::ExpressionMethods;
use diesel::QueryDsl; use diesel::QueryDsl;
use diesel::RunQueryDsl; use diesel::RunQueryDsl;
use log::{trace, warn}; use log::{info, trace, warn};
use regex::Regex; use regex::Regex;
pub mod goto_transform;
pub mod blocks; pub mod blocks;
pub mod goto_transform;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::HashSet; use std::collections::HashSet;
@ -116,6 +116,11 @@ impl BasicCompiler {
let source_content = fs::read_to_string(source_path) let source_content = fs::read_to_string(source_path)
.map_err(|e| format!("Failed to read source file: {e}"))?; .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) = if let Err(e) =
process_table_definitions(Arc::clone(&self.state), self.bot_id, &source_content) 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 source_with_suggestions = self.generate_enum_suggestions(&source_content, &tool_def)?;
let ast_path = format!("{output_dir}/{file_name}.ast"); 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}"))?; 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() { let (mcp_json, tool_json) = if tool_def.parameters.is_empty() {
// No parameters — generate minimal mcp.json so USE TOOL can find this tool // No parameters — generate minimal mcp.json so USE TOOL can find this tool
@ -244,12 +250,7 @@ impl BasicCompiler {
// Parse the array elements // Parse the array elements
let values: Vec<String> = array_content let values: Vec<String> = array_content
.split(',') .split(',')
.map(|s| { .map(|s| s.trim().trim_matches('"').trim_matches('\'').to_string())
s.trim()
.trim_matches('"')
.trim_matches('\'')
.to_string()
})
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.collect(); .collect();
Some(values) Some(values)
@ -470,7 +471,8 @@ impl BasicCompiler {
.ok(); .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() { for line in source.lines() {
let trimmed = line.trim(); let trimmed = line.trim();
@ -498,7 +500,8 @@ impl BasicCompiler {
.conn .conn
.get() .get()
.map_err(|e| format!("Failed to get database connection: {e}"))?; .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!( log::error!(
"Failed to schedule SET SCHEDULE during preprocessing: {}", "Failed to schedule SET SCHEDULE during preprocessing: {}",
e e
@ -682,7 +685,11 @@ impl BasicCompiler {
let table_name = table_name.trim_matches('"'); let table_name = table_name.trim_matches('"');
// Debug log to see what we're querying // 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) // Get column names from TABLE definition (preserves order from .bas file)
let column_names = self.get_table_columns_for_save(table_name, bot_id)?; 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 values: Vec<&String> = parts.iter().skip(1).collect();
let mut map_pairs = Vec::new(); 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() { for value_var in values.iter() {
// Find the column that matches this variable (case-insensitive) // Find the column that matches this variable (case-insensitive)
let value_lower = value_var.to_lowercase(); 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)); map_pairs.push(format!("{}: {}", column_name, value_var));
} else { } else {
log::warn!("No matching column for variable '{}'", value_var); log::warn!("No matching column for variable '{}'", value_var);
@ -709,13 +723,19 @@ impl BasicCompiler {
*save_counter += 1; *save_counter += 1;
// Generate: let __save_data_N__ = #{...}; SAVE "table", __save_data_N__ // 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)) Ok(Some(converted))
} }
/// Parse SAVE statement into parts /// Parse SAVE statement into parts
fn parse_save_statement(&self, content: &str) -> Result<Vec<String>, Box<dyn Error + Send + Sync>> { fn parse_save_statement(
&self,
content: &str,
) -> Result<Vec<String>, Box<dyn Error + Send + Sync>> {
// Simple parsing - split by comma, but respect quoted strings // Simple parsing - split by comma, but respect quoted strings
let mut parts = Vec::new(); let mut parts = Vec::new();
let mut current = String::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 // 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 let Ok(columns) = self.get_columns_from_table_definition(table_name, bot_id) {
if !columns.is_empty() { 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); return Ok(columns);
} }
} }
@ -778,7 +802,10 @@ impl BasicCompiler {
// Find the tables.bas file in the bot's data directory // Find the tables.bas file in the bot's data directory
let bot_name = self.get_bot_name_by_id(bot_id)?; 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 tables_content = fs::read_to_string(&tables_path)?;
let columns = self.parse_table_definition_for_fields(&tables_content, table_name)?; let columns = self.parse_table_definition_for_fields(&tables_content, table_name)?;
@ -822,12 +849,63 @@ impl BasicCompiler {
Ok(columns) Ok(columns)
} }
/// Process tables.bas file to ensure all tables are created
pub fn process_tables_bas(
state: &Arc<AppState>,
bot_id: uuid::Uuid,
) -> Result<(), Box<dyn Error + Send + Sync>> {
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<AppState>,
bot_id: uuid::Uuid,
) -> Result<String, Box<dyn Error + Send + Sync>> {
let mut conn = state.conn.get()?;
use crate::core::shared::models::schema::bots::dsl::*;
bots.filter(id.eq(bot_id))
.select(name)
.first::<String>(&mut *conn)
.map_err(|e| format!("Failed to get bot name: {}", e).into())
}
/// Get bot name by bot_id /// Get bot name by bot_id
fn get_bot_name_by_id(&self, bot_id: uuid::Uuid) -> Result<String, Box<dyn Error + Send + Sync>> { fn get_bot_name_by_id(
&self,
bot_id: uuid::Uuid,
) -> Result<String, Box<dyn Error + Send + Sync>> {
use crate::core::shared::models::schema::bots::dsl::*; use crate::core::shared::models::schema::bots::dsl::*;
use diesel::QueryDsl; 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))?; .map_err(|e| format!("Failed to get DB connection: {}", e))?;
let bot_name: String = bots let bot_name: String = bots
@ -857,7 +935,10 @@ impl BasicCompiler {
// First, try to get columns from the main database's information_schema // 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 // 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))?; .map_err(|e| format!("Failed to get DB connection: {}", e))?;
let query = format!( let query = format!(
@ -870,12 +951,15 @@ impl BasicCompiler {
let columns: Vec<String> = match sql_query(&query).load(&mut conn) { let columns: Vec<String> = match sql_query(&query).load(&mut conn) {
Ok(cols) => { Ok(cols) => {
if cols.is_empty() { 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 // Try bot's database as fallback when main DB returns empty
let bot_pool = self.state.bot_database_manager.get_bot_pool(bot_id); let bot_pool = self.state.bot_database_manager.get_bot_pool(bot_id);
if let Ok(pool) = bot_pool { if let Ok(pool) = bot_pool {
let mut bot_conn = pool.get() let mut bot_conn =
.map_err(|e| format!("Bot DB error: {}", e))?; pool.get().map_err(|e| format!("Bot DB error: {}", e))?;
let bot_query = format!( let bot_query = format!(
"SELECT column_name FROM information_schema.columns \ "SELECT column_name FROM information_schema.columns \
@ -886,13 +970,22 @@ impl BasicCompiler {
match sql_query(&bot_query).load(&mut *bot_conn) { match sql_query(&bot_query).load(&mut *bot_conn) {
Ok(bot_cols) => { Ok(bot_cols) => {
log::trace!("Found {} columns for table '{}' in bot database", bot_cols.len(), table_name); log::trace!(
bot_cols.into_iter() "Found {} columns for table '{}' in bot database",
bot_cols.len(),
table_name
);
bot_cols
.into_iter()
.map(|c: ColumnRow| c.column_name) .map(|c: ColumnRow| c.column_name)
.collect() .collect()
} }
Err(e) => { 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() Vec::new()
} }
} }
@ -901,20 +994,25 @@ impl BasicCompiler {
Vec::new() Vec::new()
} }
} else { } else {
log::trace!("Found {} columns for table '{}' in main database", cols.len(), table_name); log::trace!(
cols.into_iter() "Found {} columns for table '{}' in main database",
.map(|c: ColumnRow| c.column_name) cols.len(),
.collect() table_name
);
cols.into_iter().map(|c: ColumnRow| c.column_name).collect()
} }
} }
Err(e) => { 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 // Try bot's database as fallback
let bot_pool = self.state.bot_database_manager.get_bot_pool(bot_id); let bot_pool = self.state.bot_database_manager.get_bot_pool(bot_id);
if let Ok(pool) = bot_pool { if let Ok(pool) = bot_pool {
let mut bot_conn = pool.get() let mut bot_conn = pool.get().map_err(|e| format!("Bot DB error: {}", e))?;
.map_err(|e| format!("Bot DB error: {}", e))?;
let bot_query = format!( let bot_query = format!(
"SELECT column_name FROM information_schema.columns \ "SELECT column_name FROM information_schema.columns \
@ -925,14 +1023,22 @@ impl BasicCompiler {
match sql_query(&bot_query).load(&mut *bot_conn) { match sql_query(&bot_query).load(&mut *bot_conn) {
Ok(cols) => { 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() cols.into_iter()
.filter(|c: &ColumnRow| c.column_name != "id") .filter(|c: &ColumnRow| c.column_name != "id")
.map(|c: ColumnRow| c.column_name) .map(|c: ColumnRow| c.column_name)
.collect() .collect()
} }
Err(e) => { 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() Vec::new()
} }
} }

View file

@ -2,10 +2,11 @@ use crate::core::shared::models::UserSession;
use crate::core::shared::state::AppState; use crate::core::shared::state::AppState;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::sql_types::*; use diesel::sql_types::*;
use log::error; use log::{error, trace};
use rhai::{Dynamic, Engine}; use rhai::{Dynamic, Engine};
use serde_json::Value; use serde_json::Value;
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid;
#[derive(Debug, QueryableByName)] #[derive(Debug, QueryableByName)]
struct ColumnRow { struct ColumnRow {
@ -13,8 +14,9 @@ struct ColumnRow {
column_name: String, column_name: String,
} }
pub fn detect_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Engine) { pub fn detect_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state); let state_clone = Arc::clone(&state);
let bot_id = user.bot_id;
engine engine
.register_custom_syntax(["DETECT", "$expr$"], false, move |context, inputs| { .register_custom_syntax(["DETECT", "$expr$"], false, move |context, inputs| {
@ -27,6 +29,7 @@ pub fn detect_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Eng
let table_name = context.eval_expression_tree(first_input)?.to_string(); let table_name = context.eval_expression_tree(first_input)?.to_string();
let state_for_thread = Arc::clone(&state_clone); let state_for_thread = Arc::clone(&state_clone);
let bot_id_for_thread = bot_id;
let (tx, rx) = std::sync::mpsc::channel(); let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || { std::thread::spawn(move || {
@ -37,7 +40,7 @@ pub fn detect_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Eng
let send_err = if let Ok(rt) = rt { let send_err = if let Ok(rt) = rt {
let result = rt.block_on(async move { 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() tx.send(result).err()
} else { } else {
@ -73,10 +76,13 @@ pub fn detect_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Eng
async fn detect_anomalies_in_table( async fn detect_anomalies_in_table(
state: Arc<AppState>, state: Arc<AppState>,
table_name: &str, table_name: &str,
bot_id: Uuid,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
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); let value_field = find_numeric_field(&columns);
trace!("DETECT: columns = {:?}, value_field = {}", columns, value_field);
#[derive(QueryableByName)] #[derive(QueryableByName)]
struct JsonRow { struct JsonRow {
#[diesel(sql_type = Text)] #[diesel(sql_type = Text)]
@ -89,8 +95,9 @@ async fn detect_anomalies_in_table(
column_list, table_name column_list, table_name
); );
let pool = state.bot_database_manager.get_bot_pool(bot_id)?;
let rows: Vec<JsonRow> = diesel::sql_query(&query) let rows: Vec<JsonRow> = diesel::sql_query(&query)
.load(&mut state.conn.get()?)?; .load(&mut pool.get()?)?;
let records: Vec<Value> = rows let records: Vec<Value> = rows
.into_iter() .into_iter()
@ -108,7 +115,7 @@ async fn detect_anomalies_in_table(
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let response = client let response = client
.post(format!("{}/api/anomaly/detect", botmodels_host)) .post(format!("{}/api/detect", botmodels_host))
.header("X-API-Key", &botmodels_key) .header("X-API-Key", &botmodels_key)
.json(&serde_json::json!({ .json(&serde_json::json!({
"data": records, "data": records,
@ -129,14 +136,16 @@ async fn detect_anomalies_in_table(
fn get_table_columns( fn get_table_columns(
state: &Arc<AppState>, state: &Arc<AppState>,
table_name: &str, table_name: &str,
bot_id: Uuid,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let query = format!( let query = format!(
"SELECT column_name FROM information_schema.columns WHERE table_name = '{}' ORDER BY ordinal_position", "SELECT column_name FROM information_schema.columns WHERE table_name = '{}' ORDER BY ordinal_position",
table_name table_name
); );
let pool = state.bot_database_manager.get_bot_pool(bot_id)?;
let rows: Vec<ColumnRow> = diesel::sql_query(&query) let rows: Vec<ColumnRow> = diesel::sql_query(&query)
.load(&mut state.conn.get()?)?; .load(&mut pool.get()?)?;
Ok(rows.into_iter().map(|r| r.column_name).collect()) 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 { fn find_numeric_field(columns: &[String]) -> String {
let numeric_keywords = ["salario", "salary", "valor", "value", "amount", "preco", "price", let numeric_keywords = ["salario", "salary", "valor", "value", "amount", "preco", "price",
"temperatura", "temp", "pressao", "pressure", "quantidade", "quantity", "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 { for col in columns {
let col_lower = col.to_lowercase(); 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())
} }

View file

@ -7,94 +7,58 @@ use rhai::Engine;
#[cfg(feature = "llm")] #[cfg(feature = "llm")]
use rhai::{Dynamic, Engine}; use rhai::{Dynamic, Engine};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
#[cfg(feature = "llm")] #[cfg(feature = "llm")]
pub fn register_enhanced_llm_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) { pub fn register_enhanced_llm_keyword(state: Arc<AppState>, _user: UserSession, engine: &mut Engine) {
let state_clone1 = Arc::clone(&state); let state_clone = Arc::clone(&state);
let state_clone2 = Arc::clone(&state);
let user_clone = user;
if let Err(e) = engine.register_custom_syntax( if let Err(e) = engine.register_custom_syntax(
["LLM", "$string$", "WITH", "OPTIMIZE", "FOR", "$string$"], ["LLM", "$string$"],
false, false,
move |context, inputs| { move |context, inputs| {
let prompt = context.eval_expression_tree(&inputs[0])?.to_string(); let prompt = context.eval_expression_tree(&inputs[0])?.to_string();
let optimization = context.eval_expression_tree(&inputs[1])?.to_string(); let state_for_thread = Arc::clone(&state_clone);
let (tx, rx) = std::sync::mpsc::channel();
let state_for_spawn = Arc::clone(&state_clone1); std::thread::spawn(move || {
let _user_clone_spawn = user_clone.clone(); let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build();
tokio::spawn(async move { if let Ok(rt) = rt {
let router = SmartLLMRouter::new(state_for_spawn); let result = rt.block_on(async move {
let goal = OptimizationGoal::from_str_name(&optimization); let router = SmartLLMRouter::new(Arc::clone(&state_for_thread));
crate::llm::smart_router::enhanced_llm_call(
match crate::llm::smart_router::enhanced_llm_call( &state_for_thread, &router, &prompt, OptimizationGoal::Balanced, None, None,
&router, &prompt, goal, None, None,
) )
.await .await
{ });
Ok(_response) => { let _ = tx.send(result);
log::info!("LLM response generated with {} optimization", optimization);
}
Err(e) => {
log::error!("Enhanced LLM call failed: {}", e);
}
} }
}); });
Ok(Dynamic::from("LLM response")) 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,
)))
}
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
format!("LLM thread failed: {e}").into(),
rhai::Position::NONE,
))),
}
}, },
) { ) {
log::warn!("Failed to register enhanced LLM syntax: {e}"); log::warn!("Failed to register simple 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);
}
}
});
Ok(Dynamic::from("LLM response"))
},
) {
log::warn!("Failed to register constrained LLM syntax: {e}");
} }
} }

View file

@ -325,6 +325,10 @@ impl ScriptService {
let _ = self; // silence unused self warning - kept for API consistency let _ = self; // silence unused self warning - kept for API consistency
let script = preprocess_switch(script); 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") // 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 // This avoids Rhai custom syntax conflicts and makes the system more secure
let script = Self::convert_multiword_keywords(&script); let script = Self::convert_multiword_keywords(&script);
@ -2051,6 +2055,59 @@ impl ScriptService {
word.to_lowercase() 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<char> = 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
}
} }

View file

@ -186,10 +186,13 @@ impl BootstrapManager {
let config_path = self.stack_dir("conf/system/directory_config.json"); let config_path = self.stack_dir("conf/system/directory_config.json");
if !config_path.exists() { if !config_path.exists() {
info!("Creating OAuth client for Directory service..."); info!("Creating OAuth client for Directory service...");
#[cfg(feature = "directory")]
match crate::core::package_manager::setup_directory().await { match crate::core::package_manager::setup_directory().await {
Ok(_) => info!("OAuth client created successfully"), Ok(_) => info!("OAuth client created successfully"),
Err(e) => warn!("Failed to create OAuth client: {}", e), Err(e) => warn!("Failed to create OAuth client: {}", e),
} }
#[cfg(not(feature = "directory"))]
info!("Directory feature not enabled, skipping OAuth setup");
} else { } else {
info!("Directory config already exists, skipping OAuth setup"); 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"); let config_path = self.stack_dir("conf/system/directory_config.json");
if !config_path.exists() { if !config_path.exists() {
info!("Creating OAuth client for Directory service..."); info!("Creating OAuth client for Directory service...");
#[cfg(feature = "directory")]
match crate::core::package_manager::setup_directory().await { match crate::core::package_manager::setup_directory().await {
Ok(_) => info!("OAuth client created successfully"), Ok(_) => info!("OAuth client created successfully"),
Err(e) => warn!("Failed to create OAuth client: {}", e), Err(e) => warn!("Failed to create OAuth client: {}", e),
} }
#[cfg(not(feature = "directory"))]
info!("Directory feature not enabled, skipping OAuth setup");
} }
} }
} }

View file

@ -1,5 +1,7 @@
// Email invitation functions // Email invitation functions
#[cfg(feature = "mail")]
use log::warn; use log::warn;
#[cfg(feature = "mail")]
use uuid::Uuid; use uuid::Uuid;
#[cfg(feature = "mail")] #[cfg(feature = "mail")]
use lettre::{ use lettre::{

View file

@ -1,4 +1,3 @@
pub mod core; pub mod core;
pub use self::core::*; pub use self::core::*;
@ -17,11 +16,11 @@ pub use super::schema;
// Re-export core schema tables // Re-export core schema tables
pub use super::schema::{ pub use super::schema::{
basic_tools, bot_configuration, bot_memories, bots, clicks, basic_tools, bot_configuration, bot_memories, bot_shared_memory, bots, clicks, message_history,
message_history, organizations, rbac_group_roles, rbac_groups, organizations, rbac_group_roles, rbac_groups, rbac_permissions, rbac_role_permissions,
rbac_permissions, rbac_role_permissions, rbac_roles, rbac_user_groups, rbac_user_roles, rbac_roles, rbac_user_groups, rbac_user_roles, session_tool_associations, system_automations,
session_tool_associations, system_automations, user_login_tokens, user_login_tokens, user_preferences, user_sessions, users, workflow_events,
user_preferences, user_sessions, users, workflow_executions, workflow_events, bot_shared_memory, workflow_executions,
}; };
// Re-export feature-gated schema tables // Re-export feature-gated schema tables
@ -31,28 +30,23 @@ pub use super::schema::tasks;
#[cfg(feature = "mail")] #[cfg(feature = "mail")]
pub use super::schema::{ pub use super::schema::{
distribution_lists, email_auto_responders, email_drafts, email_folders, distribution_lists, email_auto_responders, email_drafts, email_folders,
email_label_assignments, email_labels, email_rules, email_signatures, email_label_assignments, email_labels, email_rules, email_signatures, email_templates,
email_templates, global_email_signatures, scheduled_emails, global_email_signatures, scheduled_emails, shared_mailbox_members, shared_mailboxes,
shared_mailbox_members, shared_mailboxes, user_email_accounts, user_email_accounts,
}; };
#[cfg(feature = "people")] #[cfg(feature = "people")]
pub use super::schema::{ pub use super::schema::{
crm_accounts, crm_activities, crm_contacts, crm_leads, crm_notes, crm_accounts, crm_activities, crm_contacts, crm_leads, crm_notes, crm_opportunities,
crm_opportunities, crm_pipeline_stages, people, people_departments, crm_pipeline_stages, people, people_departments, people_org_chart, people_person_skills,
people_org_chart, people_person_skills, people_skills, people_team_members, people_skills, people_team_members, people_teams, people_time_off,
people_teams, people_time_off,
};
#[cfg(feature = "vectordb")]
pub use super::schema::{
kb_collections, kb_documents, kb_group_associations, user_kb_associations,
}; };
#[cfg(feature = "rbac")]
pub use super::schema::kb_group_associations;
pub use botlib::message_types::MessageType; pub use botlib::message_types::MessageType;
pub use botlib::models::{ApiResponse, Attachment, BotResponse, Session, Suggestion, UserMessage}; pub use botlib::models::{ApiResponse, Attachment, BotResponse, Session, Suggestion, UserMessage};
// Manually export OrganizationInvitation as it is defined in core but table is organization_invitations // Manually export OrganizationInvitation as it is defined in core but table is organization_invitations
pub use self::core::OrganizationInvitation; pub use self::core::OrganizationInvitation;

View file

@ -1,4 +1,6 @@
use crate::core::shared::state::AppState; use crate::core::shared::state::AppState;
use crate::llm::OpenAIClient;
use crate::core::config::ConfigManager;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
@ -144,6 +146,7 @@ impl SmartLLMRouter {
// Enhanced LLM keyword with optimization // Enhanced LLM keyword with optimization
pub async fn enhanced_llm_call( pub async fn enhanced_llm_call(
state: &Arc<AppState>,
router: &SmartLLMRouter, router: &SmartLLMRouter,
prompt: &str, prompt: &str,
optimization_goal: OptimizationGoal, optimization_goal: OptimizationGoal,
@ -152,17 +155,37 @@ pub async fn enhanced_llm_call(
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let start_time = Instant::now(); let start_time = Instant::now();
// Select optimal model // Select optimal model (for tracking)
let model = router let model = router
.select_optimal_model("general", optimization_goal, max_cost, max_latency) .select_optimal_model("general", optimization_goal, max_cost, max_latency)
.await?; .await?;
// Make LLM call (simplified - would use actual LLM provider) // Get actual LLM configuration from bot's config
let response = format!("Response from {} for: {}", model, prompt); 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 // Track performance
let latency = start_time.elapsed().as_millis() as u64; 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-4" => 0.03,
"gpt-4o-mini" => 0.0015, "gpt-4o-mini" => 0.0015,
"claude-3-sonnet" => 0.015, "claude-3-sonnet" => 0.015,
@ -170,7 +193,7 @@ pub async fn enhanced_llm_call(
}; };
router router
.track_performance(&model, latency, cost_per_token, true) .track_performance(&actual_model, latency, cost_per_token, true)
.await?; .await?;
Ok(response) Ok(response)

View file

@ -187,6 +187,72 @@ pub async fn run_axum_server(
.nest(ApiUrls::AUTH, crate::directory::auth_routes::configure()); .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<Arc<AppState>>,
axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
) -> 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::<uuid::Uuid>(&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")] #[cfg(feature = "meet")]
{ {
api_router = api_router.merge(crate::meet::configure()); 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::editor::configure_editor_routes());
api_router = api_router.merge(crate::api::database::configure_database_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::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()); 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 let site_path = app_state
.config .config

View file

@ -2,6 +2,8 @@ pub mod audit_log;
pub mod menu_config; pub mod menu_config;
pub mod permission_inheritance; pub mod permission_inheritance;
pub mod rbac; pub mod rbac;
#[cfg(feature = "rbac")]
pub mod rbac_kb; pub mod rbac_kb;
pub mod rbac_ui; pub mod rbac_ui;
pub mod security_admin; pub mod security_admin;
@ -223,12 +225,17 @@ log::info!("Testing SMTP connection to {}:{}", config.host, config.port);
let mailer_result = if let (Some(user), Some(pass)) = (config.username, config.password) { let mailer_result = if let (Some(user), Some(pass)) = (config.username, config.password) {
let creds = Credentials::new(user, pass); let creds = Credentials::new(user, pass);
if config.port == 465 {
SmtpTransport::relay(&config.host) SmtpTransport::relay(&config.host)
.map(|b| b.port(config.port as u16).credentials(creds).build()) .map(|b| b.port(config.port as u16).credentials(creds).build())
} else {
SmtpTransport::starttls_relay(&config.host)
.map(|b| b.port(config.port as u16).credentials(creds).build())
}
} else { } else {
Ok(SmtpTransport::builder_dangerous(&config.host) SmtpTransport::builder_dangerous(&config.host)
.port(config.port as u16) .port(config.port as u16)
.build()) .build()
}; };
match mailer_result { match mailer_result {
@ -323,8 +330,9 @@ r##"<div class="success-message">
async fn get_trusted_devices(State(_state): State<Arc<AppState>>) -> Html<String> { async fn get_trusted_devices(State(_state): State<Arc<AppState>>) -> Html<String> {
Html( Html(
r##"<div class="device-item current"> r####"<div class="device-item current">
<div class="device-info"> <div class="device-info">
"##
<span class="device-icon">💻</span> <span class="device-icon">💻</span>
<div class="device-details"> <div class="device-details">
<span class="device-name">Current Device</span> <span class="device-name">Current Device</span>
@ -333,4 +341,4 @@ r##"<div class="device-item current">
</div> </div>
<span class="device-badge trusted">Trusted</span> <span class="device-badge trusted">Trusted</span>
</div> <div class="devices-empty"> <p class="text-muted">No other trusted devices</p> </div>"## .to_string(), ) } </div> <div class="devices-empty"> <p class="text-muted">No other trusted devices</p> </div>"#### .to_string(), ) }

View file

@ -18,6 +18,7 @@ use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
#[cfg(feature = "rbac")]
use crate::settings::rbac_kb::{ use crate::settings::rbac_kb::{
assign_kb_to_group, get_accessible_kbs_for_user, get_kb_groups, remove_kb_from_group, 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<Arc<AppState>> {
.route("/api/rbac/groups/{group_id}/roles", get(get_group_roles)) .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/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)) .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", get(rbac_settings_page))
.route("/settings/rbac/users", get(rbac_users_list)) .route("/settings/rbac/users", get(rbac_users_list))
.route("/settings/rbac/roles", get(rbac_roles_list)) .route("/settings/rbac/roles", get(rbac_roles_list))