Fix all clippy warnings - 0 warnings

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-12-28 14:27:52 -03:00
parent 96cf7b57f8
commit aeb4e8af0f
13 changed files with 220 additions and 309 deletions

View file

@ -1,10 +1,9 @@
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use std::fmt::Write; use std::fmt::Write;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, LazyLock, RwLock};
use uuid::Uuid; use uuid::Uuid;
const MAX_LOGS_PER_APP: usize = 500; const MAX_LOGS_PER_APP: usize = 500;
@ -446,7 +445,7 @@ impl Default for AppLogStore {
} }
} }
pub static APP_LOGS: Lazy<Arc<AppLogStore>> = Lazy::new(|| Arc::new(AppLogStore::new())); pub static APP_LOGS: LazyLock<Arc<AppLogStore>> = LazyLock::new(|| Arc::new(AppLogStore::new()));
pub fn log_generator_info(app_name: &str, message: &str) { pub fn log_generator_info(app_name: &str, message: &str) {
APP_LOGS.log( APP_LOGS.log(

View file

@ -102,12 +102,9 @@ pub fn ask_later_keyword(state: Arc<AppState>, user: UserSession, engine: &mut E
} }
}); });
let state_clone5 = state.clone();
let user_clone5 = user.clone();
engine.register_fn("list_pending_info", move || -> Dynamic { engine.register_fn("list_pending_info", move || -> Dynamic {
let state = state_clone5.clone(); let state = state.clone();
let user = user_clone5.clone(); let user = user.clone();
match list_pending_info(&state, &user) { match list_pending_info(&state, &user) {
Ok(items) => { Ok(items) => {

View file

@ -1,4 +1,4 @@
use crate::auto_task::auto_task::{ use crate::auto_task::task_types::{
AutoTask, AutoTaskStatus, ExecutionMode, PendingApproval, PendingDecision, TaskPriority, AutoTask, AutoTaskStatus, ExecutionMode, PendingApproval, PendingDecision, TaskPriority,
}; };
use crate::auto_task::intent_classifier::IntentClassifier; use crate::auto_task::intent_classifier::IntentClassifier;
@ -1334,7 +1334,7 @@ fn create_auto_task_from_plan(
pending_decisions: Vec::new(), pending_decisions: Vec::new(),
pending_approvals: Vec::new(), pending_approvals: Vec::new(),
risk_summary: None, risk_summary: None,
resource_usage: crate::auto_task::auto_task::ResourceUsage::default(), resource_usage: crate::auto_task::task_types::ResourceUsage::default(),
error: None, error: None,
rollback_state: None, rollback_state: None,
session_id: session.id.to_string(), session_id: session.id.to_string(),
@ -1613,9 +1613,8 @@ fn update_task_status_db(
} }
fn get_pending_items_for_bot(state: &Arc<AppState>, bot_id: Uuid) -> Vec<PendingItemResponse> { fn get_pending_items_for_bot(state: &Arc<AppState>, bot_id: Uuid) -> Vec<PendingItemResponse> {
let mut conn = match state.conn.get() { let Ok(mut conn) = state.conn.get() else {
Ok(c) => c, return Vec::new();
Err(_) => return Vec::new(),
}; };
#[derive(QueryableByName)] #[derive(QueryableByName)]
@ -1860,18 +1859,15 @@ pub struct PendingItemsResponse {
pub async fn get_pending_items_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse { pub async fn get_pending_items_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
info!("Getting pending items"); info!("Getting pending items");
let session = match get_current_session(&state) { let Ok(session) = get_current_session(&state) else {
Ok(s) => s, return (
Err(_) => { StatusCode::UNAUTHORIZED,
return ( Json(PendingItemsResponse {
StatusCode::UNAUTHORIZED, items: Vec::new(),
Json(PendingItemsResponse { count: 0,
items: Vec::new(), }),
count: 0, )
}), .into_response();
)
.into_response();
}
}; };
let items = get_pending_items_for_bot(&state, session.bot_id); let items = get_pending_items_for_bot(&state, session.bot_id);
@ -1898,18 +1894,15 @@ pub async fn submit_pending_item_handler(
) -> impl IntoResponse { ) -> impl IntoResponse {
info!("Submitting pending item {item_id}: {}", request.value); info!("Submitting pending item {item_id}: {}", request.value);
let session = match get_current_session(&state) { let Ok(session) = get_current_session(&state) else {
Ok(s) => s, return (
Err(_) => { StatusCode::UNAUTHORIZED,
return ( Json(serde_json::json!({
StatusCode::UNAUTHORIZED, "success": false,
Json(serde_json::json!({ "error": "Authentication required"
"success": false, })),
"error": "Authentication required" )
})), .into_response();
)
.into_response();
}
}; };
match resolve_pending_item(&state, &item_id, &request.value, session.bot_id) { match resolve_pending_item(&state, &item_id, &request.value, session.bot_id) {

View file

@ -6,7 +6,7 @@ use diesel::sql_query;
use diesel::sql_types::{Text, Uuid as DieselUuid}; use diesel::sql_types::{Text, Uuid as DieselUuid};
use log::{info, trace, warn}; use log::{info, trace, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Write;
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
@ -164,7 +164,7 @@ impl DesignerAI {
line_number: None, line_number: None,
}) })
.collect(), .collect(),
preview: Some(self.generate_preview(&analysis)), preview: Some(Self::generate_preview(&analysis)),
requires_confirmation: true, requires_confirmation: true,
confirmation_message: analysis.confirmation_reason, confirmation_message: analysis.confirmation_reason,
can_undo: true, can_undo: true,
@ -174,10 +174,10 @@ impl DesignerAI {
} }
// Apply the modification // Apply the modification
self.apply_modification(&analysis, session).await self.apply_modification(&analysis, session)
} }
pub async fn apply_confirmed_modification( pub fn apply_confirmed_modification(
&self, &self,
change_id: &str, change_id: &str,
session: &UserSession, session: &UserSession,
@ -186,7 +186,7 @@ impl DesignerAI {
let pending = self.get_pending_change(change_id, session)?; let pending = self.get_pending_change(change_id, session)?;
match pending { match pending {
Some(analysis) => self.apply_modification(&analysis, session).await, Some(analysis) => self.apply_modification(&analysis, session),
None => Ok(ModificationResult { None => Ok(ModificationResult {
success: false, success: false,
modification_type: ModificationType::Unknown, modification_type: ModificationType::Unknown,
@ -202,7 +202,7 @@ impl DesignerAI {
} }
} }
pub async fn undo_change( pub fn undo_change(
&self, &self,
change_id: &str, change_id: &str,
session: &UserSession, session: &UserSession,
@ -265,7 +265,7 @@ impl DesignerAI {
} }
} }
pub async fn get_context( pub fn get_context(
&self, &self,
session: &UserSession, session: &UserSession,
current_app: Option<&str>, current_app: Option<&str>,
@ -335,11 +335,10 @@ Respond ONLY with valid JSON."#
); );
let response = self.call_llm(&prompt).await?; let response = self.call_llm(&prompt).await?;
self.parse_analysis_response(&response, instruction) Self::parse_analysis_response(&response, instruction)
} }
fn parse_analysis_response( fn parse_analysis_response(
&self,
response: &str, response: &str,
instruction: &str, instruction: &str,
) -> Result<AnalyzedModification, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<AnalyzedModification, Box<dyn std::error::Error + Send + Sync>> {
@ -393,14 +392,12 @@ Respond ONLY with valid JSON."#
} }
Err(e) => { Err(e) => {
warn!("Failed to parse LLM analysis: {e}"); warn!("Failed to parse LLM analysis: {e}");
// Fallback to heuristic analysis Self::analyze_modification_heuristic(instruction)
self.analyze_modification_heuristic(instruction)
} }
} }
} }
fn analyze_modification_heuristic( fn analyze_modification_heuristic(
&self,
instruction: &str, instruction: &str,
) -> Result<AnalyzedModification, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<AnalyzedModification, Box<dyn std::error::Error + Send + Sync>> {
let lower = instruction.to_lowercase(); let lower = instruction.to_lowercase();
@ -461,7 +458,7 @@ Respond ONLY with valid JSON."#
}) })
} }
async fn apply_modification( fn apply_modification(
&self, &self,
analysis: &AnalyzedModification, analysis: &AnalyzedModification,
session: &UserSession, session: &UserSession,
@ -476,25 +473,20 @@ Respond ONLY with valid JSON."#
// Generate new content based on modification type // Generate new content based on modification type
let new_content = match analysis.modification_type { let new_content = match analysis.modification_type {
ModificationType::Style => { ModificationType::Style => {
self.apply_style_changes(&original_content, &analysis.changes) Self::apply_style_changes(&original_content, &analysis.changes)?
.await?
} }
ModificationType::Html => { ModificationType::Html => {
self.apply_html_changes(&original_content, &analysis.changes) Self::apply_html_changes(&original_content, &analysis.changes)?
.await?
} }
ModificationType::Database => { ModificationType::Database => {
self.apply_database_changes(&original_content, &analysis.changes, session) Self::apply_database_changes(&original_content, &analysis.changes)?
.await?
} }
ModificationType::Tool => self.generate_tool_file(&analysis.changes, session).await?, ModificationType::Tool => Self::generate_tool_file(&analysis.changes)?,
ModificationType::Scheduler => { ModificationType::Scheduler => {
self.generate_scheduler_file(&analysis.changes, session) Self::generate_scheduler_file(&analysis.changes)?
.await?
} }
ModificationType::Multiple => { ModificationType::Multiple => {
// Handle multiple changes sequentially Self::apply_multiple_changes()?
self.apply_multiple_changes(analysis, session).await?
} }
ModificationType::Unknown => { ModificationType::Unknown => {
return Ok(ModificationResult { return Ok(ModificationResult {
@ -522,7 +514,7 @@ Respond ONLY with valid JSON."#
description: analysis.summary.clone(), description: analysis.summary.clone(),
file_path: analysis.target_file.clone(), file_path: analysis.target_file.clone(),
original_content, original_content,
new_content: new_content.clone(), new_content,
timestamp: Utc::now(), timestamp: Utc::now(),
can_undo: true, can_undo: true,
}; };
@ -552,8 +544,7 @@ Respond ONLY with valid JSON."#
}) })
} }
async fn apply_style_changes( fn apply_style_changes(
&self,
original: &str, original: &str,
changes: &[CodeChange], changes: &[CodeChange],
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
@ -580,7 +571,7 @@ Respond ONLY with valid JSON."#
} }
} }
_ => { _ => {
content.push_str("\n"); content.push('\n');
content.push_str(&change.content); content.push_str(&change.content);
} }
} }
@ -589,8 +580,7 @@ Respond ONLY with valid JSON."#
Ok(content) Ok(content)
} }
async fn apply_html_changes( fn apply_html_changes(
&self,
original: &str, original: &str,
changes: &[CodeChange], changes: &[CodeChange],
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
@ -627,11 +617,9 @@ Respond ONLY with valid JSON."#
Ok(content) Ok(content)
} }
async fn apply_database_changes( fn apply_database_changes(
&self,
original: &str, original: &str,
changes: &[CodeChange], changes: &[CodeChange],
session: &UserSession,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let mut content = original.to_string(); let mut content = original.to_string();
@ -654,28 +642,27 @@ Respond ONLY with valid JSON."#
content.push_str(&change.content); content.push_str(&change.content);
} }
_ => { _ => {
content.push_str("\n"); content.push('\n');
content.push_str(&change.content); content.push_str(&change.content);
} }
} }
} }
// Sync schema to database // Sync schema to database
self.sync_schema_changes(session)?; Self::sync_schema_changes()?;
Ok(content) Ok(content)
} }
async fn generate_tool_file( fn generate_tool_file(
&self,
changes: &[CodeChange], changes: &[CodeChange],
_session: &UserSession,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let mut content = String::new(); let mut content = String::new();
content.push_str(&format!( let _ = write!(
content,
"' Tool generated by Designer\n' Created: {}\n\n", "' Tool generated by Designer\n' Created: {}\n\n",
Utc::now().format("%Y-%m-%d %H:%M") Utc::now().format("%Y-%m-%d %H:%M")
)); );
for change in changes { for change in changes {
if !change.content.is_empty() { if !change.content.is_empty() {
@ -687,16 +674,15 @@ Respond ONLY with valid JSON."#
Ok(content) Ok(content)
} }
async fn generate_scheduler_file( fn generate_scheduler_file(
&self,
changes: &[CodeChange], changes: &[CodeChange],
_session: &UserSession,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let mut content = String::new(); let mut content = String::new();
content.push_str(&format!( let _ = write!(
content,
"' Scheduler generated by Designer\n' Created: {}\n\n", "' Scheduler generated by Designer\n' Created: {}\n\n",
Utc::now().format("%Y-%m-%d %H:%M") Utc::now().format("%Y-%m-%d %H:%M")
)); );
for change in changes { for change in changes {
if !change.content.is_empty() { if !change.content.is_empty() {
@ -708,32 +694,28 @@ Respond ONLY with valid JSON."#
Ok(content) Ok(content)
} }
async fn apply_multiple_changes( fn apply_multiple_changes() -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
&self,
_analysis: &AnalyzedModification,
_session: &UserSession,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
// For multiple changes, each would be applied separately
// Return summary of changes
Ok("Multiple changes applied".to_string()) Ok("Multiple changes applied".to_string())
} }
fn generate_preview(&self, analysis: &AnalyzedModification) -> String { fn generate_preview(analysis: &AnalyzedModification) -> String {
let mut preview = String::new(); let mut preview = String::new();
preview.push_str(&format!("File: {}\n\nChanges:\n", analysis.target_file)); let _ = writeln!(preview, "File: {}\n\nChanges:", analysis.target_file);
for (i, change) in analysis.changes.iter().enumerate() { for (i, change) in analysis.changes.iter().enumerate() {
preview.push_str(&format!( let _ = writeln!(
"{}. {} at '{}'\n", preview,
"{}. {} at '{}'",
i + 1, i + 1,
change.change_type, change.change_type,
change.target change.target
)); );
if !change.content.is_empty() { if !change.content.is_empty() {
preview.push_str(&format!( let _ = writeln!(
" New content: {}\n", preview,
" New content: {}",
&change.content[..change.content.len().min(100)] &change.content[..change.content.len().min(100)]
)); );
} }
} }
@ -746,21 +728,20 @@ Respond ONLY with valid JSON."#
) -> Result<Vec<TableInfo>, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<Vec<TableInfo>, Box<dyn std::error::Error + Send + Sync>> {
let mut conn = self.state.conn.get()?; let mut conn = self.state.conn.get()?;
// Query information_schema for tables in the bot's schema
let query = format!(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
LIMIT 50"
);
#[derive(QueryableByName)] #[derive(QueryableByName)]
struct TableRow { struct TableRow {
#[diesel(sql_type = Text)] #[diesel(sql_type = Text)]
table_name: String, table_name: String,
} }
let tables: Vec<TableRow> = sql_query(&query).get_results(&mut conn).unwrap_or_default(); let tables: Vec<TableRow> = sql_query(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
LIMIT 50",
)
.get_results(&mut conn)
.unwrap_or_default();
Ok(tables Ok(tables
.into_iter() .into_iter()
@ -783,7 +764,7 @@ Respond ONLY with valid JSON."#
if let Ok(entries) = std::fs::read_dir(&tools_path) { if let Ok(entries) = std::fs::read_dir(&tools_path) {
for entry in entries.flatten() { for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() { if let Some(name) = entry.file_name().to_str() {
if name.ends_with(".bas") { if name.to_lowercase().ends_with(".bas") {
tools.push(name.to_string()); tools.push(name.to_string());
} }
} }
@ -804,7 +785,7 @@ Respond ONLY with valid JSON."#
if let Ok(entries) = std::fs::read_dir(&schedulers_path) { if let Ok(entries) = std::fs::read_dir(&schedulers_path) {
for entry in entries.flatten() { for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() { if let Some(name) = entry.file_name().to_str() {
if name.ends_with(".bas") { if name.to_lowercase().ends_with(".bas") {
schedulers.push(name.to_string()); schedulers.push(name.to_string());
} }
} }
@ -920,10 +901,7 @@ Respond ONLY with valid JSON."#
Ok(()) Ok(())
} }
fn sync_schema_changes( fn sync_schema_changes() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
&self,
_session: &UserSession,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// This would trigger the TABLE keyword parser to sync // This would trigger the TABLE keyword parser to sync
// For now, just log // For now, just log
info!("Schema changes need to be synced to database"); info!("Schema changes need to be synced to database");

View file

@ -208,13 +208,13 @@ impl IntentClassifier {
match classification.intent_type { match classification.intent_type {
IntentType::AppCreate => self.handle_app_create(classification, session).await, IntentType::AppCreate => self.handle_app_create(classification, session).await,
IntentType::Todo => self.handle_todo(classification, session).await, IntentType::Todo => self.handle_todo(classification, session),
IntentType::Monitor => self.handle_monitor(classification, session).await, IntentType::Monitor => self.handle_monitor(classification, session),
IntentType::Action => self.handle_action(classification, session).await, IntentType::Action => self.handle_action(classification, session).await,
IntentType::Schedule => self.handle_schedule(classification, session).await, IntentType::Schedule => self.handle_schedule(classification, session),
IntentType::Goal => self.handle_goal(classification, session).await, IntentType::Goal => self.handle_goal(classification, session),
IntentType::Tool => self.handle_tool(classification, session).await, IntentType::Tool => self.handle_tool(classification, session),
IntentType::Unknown => self.handle_unknown(classification, session).await, IntentType::Unknown => Self::handle_unknown(classification),
} }
} }
@ -274,12 +274,10 @@ Respond with JSON only:
); );
let response = self.call_llm(&prompt).await?; let response = self.call_llm(&prompt).await?;
self.parse_classification_response(&response, intent) Self::parse_classification_response(&response, intent)
} }
/// Parse LLM response into ClassifiedIntent
fn parse_classification_response( fn parse_classification_response(
&self,
response: &str, response: &str,
original_intent: &str, original_intent: &str,
) -> Result<ClassifiedIntent, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<ClassifiedIntent, Box<dyn std::error::Error + Send + Sync>> {
@ -380,14 +378,12 @@ Respond with JSON only:
} }
Err(e) => { Err(e) => {
warn!("Failed to parse LLM response, using heuristic: {e}"); warn!("Failed to parse LLM response, using heuristic: {e}");
self.classify_heuristic(original_intent) Self::classify_heuristic(original_intent)
} }
} }
} }
/// Fallback heuristic classification when LLM fails
fn classify_heuristic( fn classify_heuristic(
&self,
intent: &str, intent: &str,
) -> Result<ClassifiedIntent, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<ClassifiedIntent, Box<dyn std::error::Error + Send + Sync>> {
let lower = intent.to_lowercase(); let lower = intent.to_lowercase();
@ -459,11 +455,6 @@ Respond with JSON only:
}) })
} }
// =========================================================================
// INTENT HANDLERS
// =========================================================================
/// Handle APP_CREATE: Generate full application with HTMX pages, tables, tools
async fn handle_app_create( async fn handle_app_create(
&self, &self,
classification: &ClassifiedIntent, classification: &ClassifiedIntent,
@ -550,8 +541,7 @@ Respond with JSON only:
} }
} }
/// Handle TODO: Save task to tasks table fn handle_todo(
async fn handle_todo(
&self, &self,
classification: &ClassifiedIntent, classification: &ClassifiedIntent,
session: &UserSession, session: &UserSession,
@ -595,8 +585,7 @@ Respond with JSON only:
}) })
} }
/// Handle MONITOR: Create ON CHANGE event handler fn handle_monitor(
async fn handle_monitor(
&self, &self,
classification: &ClassifiedIntent, classification: &ClassifiedIntent,
session: &UserSession, session: &UserSession,
@ -643,7 +632,7 @@ END ON
message: format!("Monitor created for: {subject}"), message: format!("Monitor created for: {subject}"),
created_resources: vec![CreatedResource { created_resources: vec![CreatedResource {
resource_type: "event".to_string(), resource_type: "event".to_string(),
name: handler_name.clone(), name: handler_name,
path: Some(event_path), path: Some(event_path),
}], }],
app_url: None, app_url: None,
@ -655,7 +644,6 @@ END ON
}) })
} }
/// Handle ACTION: Execute immediately
async fn handle_action( async fn handle_action(
&self, &self,
classification: &ClassifiedIntent, classification: &ClassifiedIntent,
@ -709,8 +697,7 @@ END ON
}) })
} }
/// Handle SCHEDULE: Create SET SCHEDULE automation fn handle_schedule(
async fn handle_schedule(
&self, &self,
classification: &ClassifiedIntent, classification: &ClassifiedIntent,
session: &UserSession, session: &UserSession,
@ -783,8 +770,7 @@ END SCHEDULE
}) })
} }
/// Handle GOAL: Create autonomous LLM loop with metrics fn handle_goal(
async fn handle_goal(
&self, &self,
classification: &ClassifiedIntent, classification: &ClassifiedIntent,
session: &UserSession, session: &UserSession,
@ -857,8 +843,7 @@ END GOAL
}) })
} }
/// Handle TOOL: Create voice/chat command trigger fn handle_tool(
async fn handle_tool(
&self, &self,
classification: &ClassifiedIntent, classification: &ClassifiedIntent,
session: &UserSession, session: &UserSession,
@ -926,23 +911,20 @@ END TRIGGER
}) })
} }
/// Handle UNKNOWN: Request clarification fn handle_unknown(
async fn handle_unknown(
&self,
classification: &ClassifiedIntent, classification: &ClassifiedIntent,
_session: &UserSession,
) -> Result<IntentResult, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<IntentResult, Box<dyn std::error::Error + Send + Sync>> {
info!("Handling UNKNOWN intent - requesting clarification"); info!("Handling UNKNOWN intent - requesting clarification");
let suggestions = if !classification.alternative_types.is_empty() { let suggestions = if classification.alternative_types.is_empty() {
"- Create an app\n- Add a task\n- Set up monitoring\n- Schedule automation".to_string()
} else {
classification classification
.alternative_types .alternative_types
.iter() .iter()
.map(|a| format!("- {}: {}", a.intent_type, a.reason)) .map(|a| format!("- {}: {}", a.intent_type, a.reason))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n") .join("\n")
} else {
"- Create an app\n- Add a task\n- Set up monitoring\n- Schedule automation".to_string()
}; };
Ok(IntentResult { Ok(IntentResult {

View file

@ -1,12 +1,12 @@
pub mod app_generator; pub mod app_generator;
pub mod app_logs; pub mod app_logs;
pub mod ask_later; pub mod ask_later;
pub mod auto_task;
pub mod autotask_api; pub mod autotask_api;
pub mod designer_ai; pub mod designer_ai;
pub mod intent_classifier; pub mod intent_classifier;
pub mod intent_compiler; pub mod intent_compiler;
pub mod safety_layer; pub mod safety_layer;
pub mod task_types;
pub use app_generator::{ pub use app_generator::{
AppGenerator, AppStructure, FileType, GeneratedApp, GeneratedFile, GeneratedPage, PageType, AppGenerator, AppStructure, FileType, GeneratedApp, GeneratedFile, GeneratedPage, PageType,
@ -18,7 +18,6 @@ pub use app_logs::{
ClientLogRequest, LogLevel, LogQueryParams, LogSource, LogStats, APP_LOGS, ClientLogRequest, LogLevel, LogQueryParams, LogSource, LogStats, APP_LOGS,
}; };
pub use ask_later::{ask_later_keyword, PendingInfoItem}; pub use ask_later::{ask_later_keyword, PendingInfoItem};
pub use auto_task::{AutoTask, AutoTaskStatus, ExecutionMode, TaskPriority};
pub use autotask_api::{ pub use autotask_api::{
apply_recommendation_handler, cancel_task_handler, classify_intent_handler, apply_recommendation_handler, cancel_task_handler, classify_intent_handler,
compile_intent_handler, create_and_execute_handler, execute_plan_handler, execute_task_handler, compile_intent_handler, create_and_execute_handler, execute_plan_handler, execute_task_handler,
@ -28,6 +27,7 @@ pub use autotask_api::{
submit_pending_item_handler, submit_pending_item_handler,
}; };
pub use designer_ai::DesignerAI; pub use designer_ai::DesignerAI;
pub use task_types::{AutoTask, AutoTaskStatus, ExecutionMode, TaskPriority};
pub use intent_classifier::{ClassifiedIntent, IntentClassifier, IntentType}; pub use intent_classifier::{ClassifiedIntent, IntentClassifier, IntentType};
pub use intent_compiler::{CompiledIntent, IntentCompiler}; pub use intent_compiler::{CompiledIntent, IntentCompiler};
pub use safety_layer::{AuditEntry, ConstraintCheckResult, SafetyLayer, SimulationResult}; pub use safety_layer::{AuditEntry, ConstraintCheckResult, SafetyLayer, SimulationResult};

View file

@ -35,18 +35,17 @@ pub async fn serve_app_index(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
Path(params): Path<AppPath>, Path(params): Path<AppPath>,
) -> impl IntoResponse { ) -> impl IntoResponse {
serve_app_file_internal(&state, &params.app_name, "index.html").await serve_app_file_internal(&state, &params.app_name, "index.html")
} }
pub async fn serve_app_file( pub async fn serve_app_file(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
Path(params): Path<AppFilePath>, Path(params): Path<AppFilePath>,
) -> impl IntoResponse { ) -> impl IntoResponse {
serve_app_file_internal(&state, &params.app_name, &params.file_path).await serve_app_file_internal(&state, &params.app_name, &params.file_path)
} }
async fn serve_app_file_internal(state: &AppState, app_name: &str, file_path: &str) -> Response { fn serve_app_file_internal(state: &AppState, app_name: &str, file_path: &str) -> Response {
// Sanitize paths to prevent directory traversal
let sanitized_app_name = sanitize_path_component(app_name); let sanitized_app_name = sanitize_path_component(app_name);
let sanitized_file_path = sanitize_path_component(file_path); let sanitized_file_path = sanitize_path_component(file_path);
@ -54,8 +53,6 @@ async fn serve_app_file_internal(state: &AppState, app_name: &str, file_path: &s
return (StatusCode::BAD_REQUEST, "Invalid path").into_response(); return (StatusCode::BAD_REQUEST, "Invalid path").into_response();
} }
// Construct full file path from SITE_ROOT
// Apps are synced to: {site_path}/{app_name}/
let site_path = state let site_path = state
.config .config
.as_ref() .as_ref()
@ -67,19 +64,16 @@ async fn serve_app_file_internal(state: &AppState, app_name: &str, file_path: &s
site_path, sanitized_app_name, sanitized_file_path site_path, sanitized_app_name, sanitized_file_path
); );
trace!("Serving app file: {}", full_path); trace!("Serving app file: {full_path}");
// Check if file exists
let path = std::path::Path::new(&full_path); let path = std::path::Path::new(&full_path);
if !path.exists() { if !path.exists() {
warn!("App file not found: {}", full_path); warn!("App file not found: {full_path}");
return (StatusCode::NOT_FOUND, "File not found").into_response(); return (StatusCode::NOT_FOUND, "File not found").into_response();
} }
// Determine content type
let content_type = get_content_type(&sanitized_file_path); let content_type = get_content_type(&sanitized_file_path);
// Read and serve the file
match std::fs::read(&full_path) { match std::fs::read(&full_path) {
Ok(contents) => Response::builder() Ok(contents) => Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
@ -94,7 +88,7 @@ async fn serve_app_file_internal(state: &AppState, app_name: &str, file_path: &s
.into_response() .into_response()
}), }),
Err(e) => { Err(e) => {
error!("Failed to read file {}: {}", full_path, e); error!("Failed to read file {full_path}: {e}");
(StatusCode::INTERNAL_SERVER_ERROR, "Failed to read file").into_response() (StatusCode::INTERNAL_SERVER_ERROR, "Failed to read file").into_response()
} }
} }
@ -109,13 +103,11 @@ pub async fn list_all_apps(State(state): State<Arc<AppState>>) -> impl IntoRespo
let mut apps = Vec::new(); let mut apps = Vec::new();
// List all directories in site_path that have an index.html (are apps)
if let Ok(entries) = std::fs::read_dir(&site_path) { if let Ok(entries) = std::fs::read_dir(&site_path) {
for entry in entries.flatten() { for entry in entries.flatten() {
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) { if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
if let Some(name) = entry.file_name().to_str() { if let Some(name) = entry.file_name().to_str() {
// Skip .gbai directories and other system folders if name.starts_with('.') || name.to_lowercase().ends_with(".gbai") {
if name.starts_with('.') || name.ends_with(".gbai") {
continue; continue;
} }

View file

@ -210,34 +210,28 @@ pub async fn get_record_handler(
let table_name = sanitize_identifier(&table); let table_name = sanitize_identifier(&table);
let user_roles = user_roles_from_headers(&headers); let user_roles = user_roles_from_headers(&headers);
let record_id = match Uuid::parse_str(&id) { let Ok(record_id) = Uuid::parse_str(&id) else {
Ok(uuid) => uuid, return (
Err(_) => { StatusCode::BAD_REQUEST,
return ( Json(RecordResponse {
StatusCode::BAD_REQUEST, success: false,
Json(RecordResponse { data: None,
success: false, message: Some("Invalid UUID format".to_string()),
data: None, }),
message: Some("Invalid UUID format".to_string()), )
}), .into_response();
)
.into_response()
}
}; };
let mut conn = match state.conn.get() { let Ok(mut conn) = state.conn.get() else {
Ok(c) => c, return (
Err(e) => { StatusCode::INTERNAL_SERVER_ERROR,
return ( Json(RecordResponse {
StatusCode::INTERNAL_SERVER_ERROR, success: false,
Json(RecordResponse { data: None,
success: false, message: Some("Database connection error".to_string()),
data: None, }),
message: Some(format!("Database connection error: {e}")), )
}), .into_response();
)
.into_response()
}
}; };
// Check table-level read access // Check table-level read access
@ -314,19 +308,16 @@ pub async fn create_record_handler(
let table_name = sanitize_identifier(&table); let table_name = sanitize_identifier(&table);
let user_roles = user_roles_from_headers(&headers); let user_roles = user_roles_from_headers(&headers);
let obj = match payload.as_object() { let Some(obj) = payload.as_object() else {
Some(o) => o, return (
None => { StatusCode::BAD_REQUEST,
return ( Json(RecordResponse {
StatusCode::BAD_REQUEST, success: false,
Json(RecordResponse { data: None,
success: false, message: Some("Payload must be a JSON object".to_string()),
data: None, }),
message: Some("Payload must be a JSON object".to_string()), )
}), .into_response();
)
.into_response()
}
}; };
let mut columns: Vec<String> = vec!["id".to_string()]; let mut columns: Vec<String> = vec!["id".to_string()];
@ -341,22 +332,18 @@ pub async fn create_record_handler(
values.push(value_to_sql(value)); values.push(value_to_sql(value));
} }
let mut conn = match state.conn.get() { let Ok(mut conn) = state.conn.get() else {
Ok(c) => c, return (
Err(e) => { StatusCode::INTERNAL_SERVER_ERROR,
return ( Json(RecordResponse {
StatusCode::INTERNAL_SERVER_ERROR, success: false,
Json(RecordResponse { data: None,
success: false, message: Some("Database connection error".to_string()),
data: None, }),
message: Some(format!("Database connection error: {e}")), )
}), .into_response();
)
.into_response()
}
}; };
// Check table-level write access
let access_info = let access_info =
match check_table_access(&mut conn, &table_name, &user_roles, AccessType::Write) { match check_table_access(&mut conn, &table_name, &user_roles, AccessType::Write) {
Ok(info) => info, Ok(info) => info,
@ -438,34 +425,28 @@ pub async fn update_record_handler(
let table_name = sanitize_identifier(&table); let table_name = sanitize_identifier(&table);
let user_roles = user_roles_from_headers(&headers); let user_roles = user_roles_from_headers(&headers);
let record_id = match Uuid::parse_str(&id) { let Ok(record_id) = Uuid::parse_str(&id) else {
Ok(uuid) => uuid, return (
Err(_) => { StatusCode::BAD_REQUEST,
return ( Json(RecordResponse {
StatusCode::BAD_REQUEST, success: false,
Json(RecordResponse { data: None,
success: false, message: Some("Invalid UUID format".to_string()),
data: None, }),
message: Some("Invalid UUID format".to_string()), )
}), .into_response();
)
.into_response()
}
}; };
let obj = match payload.as_object() { let Some(obj) = payload.as_object() else {
Some(o) => o, return (
None => { StatusCode::BAD_REQUEST,
return ( Json(RecordResponse {
StatusCode::BAD_REQUEST, success: false,
Json(RecordResponse { data: None,
success: false, message: Some("Payload must be a JSON object".to_string()),
data: None, }),
message: Some("Payload must be a JSON object".to_string()), )
}), .into_response();
)
.into_response()
}
}; };
let mut set_clauses: Vec<String> = Vec::new(); let mut set_clauses: Vec<String> = Vec::new();
@ -588,37 +569,30 @@ pub async fn delete_record_handler(
let table_name = sanitize_identifier(&table); let table_name = sanitize_identifier(&table);
let user_roles = user_roles_from_headers(&headers); let user_roles = user_roles_from_headers(&headers);
let record_id = match Uuid::parse_str(&id) { let Ok(record_id) = Uuid::parse_str(&id) else {
Ok(uuid) => uuid, return (
Err(_) => { StatusCode::BAD_REQUEST,
return ( Json(DeleteResponse {
StatusCode::BAD_REQUEST, success: false,
Json(DeleteResponse { deleted: 0,
success: false, message: Some("Invalid UUID format".to_string()),
deleted: 0, }),
message: Some("Invalid UUID format".to_string()), )
}), .into_response();
)
.into_response()
}
}; };
let mut conn = match state.conn.get() { let Ok(mut conn) = state.conn.get() else {
Ok(c) => c, return (
Err(e) => { StatusCode::INTERNAL_SERVER_ERROR,
return ( Json(DeleteResponse {
StatusCode::INTERNAL_SERVER_ERROR, success: false,
Json(DeleteResponse { deleted: 0,
success: false, message: Some("Database connection error".to_string()),
deleted: 0, }),
message: Some(format!("Database connection error: {e}")), )
}), .into_response();
)
.into_response()
}
}; };
// Check table-level write access (delete requires write)
if let Err(e) = check_table_access(&mut conn, &table_name, &user_roles, AccessType::Write) { if let Err(e) = check_table_access(&mut conn, &table_name, &user_roles, AccessType::Write) {
return ( return (
StatusCode::FORBIDDEN, StatusCode::FORBIDDEN,

View file

@ -99,10 +99,10 @@ impl UserRoles {
} }
// Also check if user is marked as admin in context // Also check if user is marked as admin in context
if let Some(Value::Bool(true)) = session.context_data.get("is_admin") { if matches!(session.context_data.get("is_admin"), Some(Value::Bool(true)))
if !roles.contains(&"admin".to_string()) { && !roles.contains(&"admin".to_string())
roles.push("admin".to_string()); {
} roles.push("admin".to_string());
} }
Self { Self {
@ -224,26 +224,21 @@ pub fn load_table_access_info(
.bind::<Text, _>(table_name) .bind::<Text, _>(table_name)
.get_result(conn); .get_result(conn);
let table_def = match table_result { let Ok(table_def) = table_result else {
Ok(row) => row, trace!(
Err(_) => { "No table definition found for '{table_name}', allowing open access"
trace!( );
"No table definition found for '{}', allowing open access", return None;
table_name
);
return None; // No definition = open access
}
}; };
let mut info = TableAccessInfo { let mut info = TableAccessInfo {
table_name: table_def.table_name, table_name: table_def.table_name,
read_roles: parse_roles_string(&table_def.read_roles), read_roles: parse_roles_string(table_def.read_roles.as_ref()),
write_roles: parse_roles_string(&table_def.write_roles), write_roles: parse_roles_string(table_def.write_roles.as_ref()),
field_read_roles: HashMap::new(), field_read_roles: HashMap::new(),
field_write_roles: HashMap::new(), field_write_roles: HashMap::new(),
}; };
// Query field-level permissions
let fields_result: Result<Vec<FieldDefRow>, _> = sql_query( let fields_result: Result<Vec<FieldDefRow>, _> = sql_query(
"SELECT f.field_name, f.read_roles, f.write_roles "SELECT f.field_name, f.read_roles, f.write_roles
FROM dynamic_table_fields f FROM dynamic_table_fields f
@ -255,8 +250,8 @@ pub fn load_table_access_info(
if let Ok(fields) = fields_result { if let Ok(fields) = fields_result {
for field in fields { for field in fields {
let field_read = parse_roles_string(&field.read_roles); let field_read = parse_roles_string(field.read_roles.as_ref());
let field_write = parse_roles_string(&field.write_roles); let field_write = parse_roles_string(field.write_roles.as_ref());
if !field_read.is_empty() { if !field_read.is_empty() {
info.field_read_roles info.field_read_roles
@ -279,9 +274,8 @@ pub fn load_table_access_info(
Some(info) Some(info)
} }
fn parse_roles_string(roles: &Option<String>) -> Vec<String> { fn parse_roles_string(roles: Option<&String>) -> Vec<String> {
roles roles
.as_ref()
.map(|s| { .map(|s| {
s.split(';') s.split(';')
.map(|r| r.trim().to_string()) .map(|r| r.trim().to_string())

View file

@ -286,8 +286,6 @@ fn parse_field_definition(
reference_table = Some(parts[i + 1].to_string()); reference_table = Some(parts[i + 1].to_string());
} }
} }
// Skip READ, BY, WRITE as they're handled separately
"read" | "by" | "write" => {}
_ => {} _ => {}
} }
} }

View file

@ -244,7 +244,7 @@ pub async fn ensure_crawler_service_running(
Arc::clone(kb_manager), Arc::clone(kb_manager),
)); ));
let _ = service.start(); drop(service.start());
info!("Website crawler service initialized"); info!("Website crawler service initialized");

View file

@ -10,6 +10,8 @@ use axum::{
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Write;
use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
@ -1096,13 +1098,14 @@ fn build_designer_prompt(request: &DesignerModifyRequest) -> String {
.map(|ctx| { .map(|ctx| {
let mut info = String::new(); let mut info = String::new();
if let Some(ref html) = ctx.page_html { if let Some(ref html) = ctx.page_html {
info.push_str(&format!( let _ = writeln!(
"\nCurrent page HTML (first 500 chars):\n{}\n", info,
"\nCurrent page HTML (first 500 chars):\n{}",
&html[..html.len().min(500)] &html[..html.len().min(500)]
)); );
} }
if let Some(ref tables) = ctx.tables { if let Some(ref tables) = ctx.tables {
info.push_str(&format!("\nAvailable tables: {}\n", tables.join(", "))); let _ = writeln!(info, "\nAvailable tables: {}", tables.join(", "));
} }
info info
}) })
@ -1226,7 +1229,7 @@ async fn parse_and_apply_changes(
code: Option<String>, code: Option<String>,
} }
let parsed: LlmChangeResponse = serde_json::from_str(llm_response).unwrap_or(LlmChangeResponse { let parsed: LlmChangeResponse = serde_json::from_str(llm_response).unwrap_or_else(|_| LlmChangeResponse {
_understanding: Some("Could not parse LLM response".to_string()), _understanding: Some("Could not parse LLM response".to_string()),
changes: None, changes: None,
message: Some("I understood your request but encountered an issue processing it. Could you try rephrasing?".to_string()), message: Some("I understood your request but encountered an issue processing it. Could you try rephrasing?".to_string()),
@ -1324,15 +1327,16 @@ async fn apply_file_change(
} }
fn get_content_type(filename: &str) -> &'static str { fn get_content_type(filename: &str) -> &'static str {
if filename.ends_with(".html") { match Path::new(filename)
"text/html" .extension()
} else if filename.ends_with(".css") { .and_then(|e| e.to_str())
"text/css" .map(|e| e.to_lowercase())
} else if filename.ends_with(".js") { .as_deref()
"application/javascript" {
} else if filename.ends_with(".json") { Some("html") => "text/html",
"application/json" Some("css") => "text/css",
} else { Some("js") => "application/javascript",
"text/plain" Some("json") => "application/json",
_ => "text/plain",
} }
} }