Compare commits
2 commits
15d9e3c142
...
90c14bcd09
| Author | SHA1 | Date | |
|---|---|---|---|
| 90c14bcd09 | |||
| 8d3c28e441 |
25 changed files with 548 additions and 264 deletions
11
Cargo.toml
11
Cargo.toml
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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": [
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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": [
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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?";
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
#[cfg(feature = "terminal")]
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{
|
extract::{
|
||||||
Query,
|
Query,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
});
|
||||||
{
|
let _ = tx.send(result);
|
||||||
Ok(_response) => {
|
|
||||||
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(
|
||||||
log::warn!("Failed to register enhanced LLM syntax: {e}");
|
e.to_string().into(),
|
||||||
}
|
rhai::Position::NONE,
|
||||||
|
))),
|
||||||
if let Err(e) = engine.register_custom_syntax(
|
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||||
[
|
Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||||
"LLM",
|
"LLM generation timed out".into(),
|
||||||
"$string$",
|
rhai::Position::NONE,
|
||||||
"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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||||
|
format!("LLM thread failed: {e}").into(),
|
||||||
Ok(Dynamic::from("LLM response"))
|
rhai::Position::NONE,
|
||||||
|
))),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
log::warn!("Failed to register constrained LLM syntax: {e}");
|
log::warn!("Failed to register simple LLM syntax: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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::{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
SmtpTransport::relay(&config.host)
|
if config.port == 465 {
|
||||||
.map(|b| b.port(config.port as u16).credentials(creds).build())
|
SmtpTransport::relay(&config.host)
|
||||||
|
.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(), ) }
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue