use crate::auto_task::app_generator::AppGenerator; use crate::auto_task::intent_compiler::IntentCompiler; use crate::core::config::ConfigManager; use crate::shared::models::UserSession; use crate::shared::state::AppState; use chrono::{DateTime, Utc}; use diesel::prelude::*; use diesel::sql_query; use diesel::sql_types::{Text, Uuid as DieselUuid}; use log::{error, info, trace, warn}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use uuid::Uuid; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum IntentType { AppCreate, Todo, Monitor, Action, Schedule, Goal, Tool, Unknown, } impl std::fmt::Display for IntentType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::AppCreate => write!(f, "APP_CREATE"), Self::Todo => write!(f, "TODO"), Self::Monitor => write!(f, "MONITOR"), Self::Action => write!(f, "ACTION"), Self::Schedule => write!(f, "SCHEDULE"), Self::Goal => write!(f, "GOAL"), Self::Tool => write!(f, "TOOL"), Self::Unknown => write!(f, "UNKNOWN"), } } } impl From<&str> for IntentType { fn from(s: &str) -> Self { match s.to_uppercase().as_str() { "APP_CREATE" | "APP" | "APPLICATION" | "CREATE_APP" => Self::AppCreate, "TODO" | "TASK" | "REMINDER" => Self::Todo, "MONITOR" | "WATCH" | "ALERT" | "ON_CHANGE" => Self::Monitor, "ACTION" | "EXECUTE" | "DO" | "RUN" => Self::Action, "SCHEDULE" | "SCHEDULED" | "DAILY" | "WEEKLY" | "MONTHLY" | "CRON" => Self::Schedule, "GOAL" | "OBJECTIVE" | "TARGET" | "ACHIEVE" => Self::Goal, "TOOL" | "COMMAND" | "TRIGGER" | "WHEN_I_SAY" => Self::Tool, _ => Self::Unknown, } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClassifiedIntent { pub id: String, pub original_text: String, pub intent_type: IntentType, pub confidence: f64, pub entities: ClassifiedEntities, pub suggested_name: Option, pub requires_clarification: bool, pub clarification_question: Option, pub alternative_types: Vec, pub classified_at: DateTime, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ClassifiedEntities { pub subject: Option, pub action: Option, pub domain: Option, pub time_spec: Option, pub condition: Option, pub recipient: Option, pub features: Vec, pub tables: Vec, pub trigger_phrases: Vec, pub target_value: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TimeSpec { pub schedule_type: ScheduleType, pub time: Option, pub day: Option, pub interval: Option, pub cron_expression: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ScheduleType { Once, Daily, Weekly, Monthly, Interval, Cron, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AlternativeClassification { pub intent_type: IntentType, pub confidence: f64, pub reason: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IntentResult { pub success: bool, pub intent_type: IntentType, pub message: String, pub created_resources: Vec, pub app_url: Option, pub task_id: Option, pub schedule_id: Option, pub tool_triggers: Vec, pub next_steps: Vec, pub error: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreatedResource { pub resource_type: String, pub name: String, pub path: Option, } pub struct IntentClassifier { state: Arc, intent_compiler: IntentCompiler, } impl IntentClassifier { pub fn new(state: Arc) -> Self { Self { state: state.clone(), intent_compiler: IntentCompiler::new(state), } } /// Classify an intent and determine which handler should process it pub async fn classify( &self, intent: &str, session: &UserSession, ) -> Result> { info!( "Classifying intent for session {}: {}", session.id, &intent[..intent.len().min(100)] ); // Use LLM to classify the intent let classification = self.classify_with_llm(intent, session.bot_id).await?; // Store classification for analytics self.store_classification(&classification, session)?; Ok(classification) } /// Classify and then process the intent through the appropriate handler pub async fn classify_and_process( &self, intent: &str, session: &UserSession, ) -> Result> { self.classify_and_process_with_task_id(intent, session, None).await } /// Classify and then process the intent through the appropriate handler with task tracking pub async fn classify_and_process_with_task_id( &self, intent: &str, session: &UserSession, task_id: Option, ) -> Result> { let classification = self.classify(intent, session).await?; self.process_classified_intent_with_task_id(&classification, session, task_id) .await } /// Process a classified intent through the appropriate handler pub async fn process_classified_intent( &self, classification: &ClassifiedIntent, session: &UserSession, ) -> Result> { self.process_classified_intent_with_task_id(classification, session, None).await } /// Process a classified intent through the appropriate handler with task tracking pub async fn process_classified_intent_with_task_id( &self, classification: &ClassifiedIntent, session: &UserSession, task_id: Option, ) -> Result> { info!( "Processing {} intent: {}", classification.intent_type, &classification.original_text[..classification.original_text.len().min(50)] ); match classification.intent_type { IntentType::AppCreate => self.handle_app_create(classification, session, task_id).await, IntentType::Todo => self.handle_todo(classification, session), IntentType::Monitor => self.handle_monitor(classification, session), IntentType::Action => self.handle_action(classification, session).await, IntentType::Schedule => self.handle_schedule(classification, session), IntentType::Goal => self.handle_goal(classification, session), IntentType::Tool => self.handle_tool(classification, session), IntentType::Unknown => Self::handle_unknown(classification), } } /// Use LLM to classify the intent async fn classify_with_llm( &self, intent: &str, bot_id: Uuid, ) -> Result> { let prompt = format!( r#"Classify this user request into one of these intent types: USER REQUEST: "{intent}" INTENT TYPES: - APP_CREATE: Create a full application, utility, calculator, tool, system, etc. Keywords: "create", "build", "make", "calculator", "app", "system", "CRM", "tool" IMPORTANT: If user wants to CREATE anything (app, calculator, converter, timer, etc), classify as APP_CREATE - TODO: Simple task or reminder Keywords: "call", "remind me", "don't forget", "tomorrow", "later" - MONITOR: Watch for changes and alert Keywords: "alert when", "notify if", "watch", "monitor", "track changes" - ACTION: Execute something immediately Keywords: "send email", "delete", "update all", "export", "do now" - SCHEDULE: Create recurring automation Keywords: "every day", "daily at", "weekly", "monthly", "at 9am" - GOAL: Long-term objective to achieve Keywords: "increase", "improve", "achieve", "reach target", "grow by" - TOOL: Create a voice/chat command Keywords: "when I say", "create command", "shortcut for", "trigger" YOLO MODE: NEVER ask for clarification. Always make a decision and proceed. For APP_CREATE: Just build whatever makes sense. A "calculator" = basic calculator app. A "CRM" = customer management app. Be creative and decisive. Respond with JSON only: {{ "intent_type": "APP_CREATE|TODO|MONITOR|ACTION|SCHEDULE|GOAL|TOOL|UNKNOWN", "confidence": 0.0-1.0, "subject": "main subject or null", "action": "main action verb or null", "domain": "industry/domain or null", "time_spec": {{"type": "ONCE|DAILY|WEEKLY|MONTHLY", "time": "9:00", "day": "monday"}} or null, "condition": "trigger condition or null", "recipient": "notification recipient or null", "features": ["feature1", "feature2"], "tables": ["table1", "table2"], "trigger_phrases": ["phrase1", "phrase2"], "target_value": "metric target or null", "suggested_name": "short name for the resource", "requires_clarification": false, "clarification_question": null, "alternatives": [] }}"# ); info!("[INTENT_CLASSIFIER] Starting LLM call for classification, prompt_len={} chars", prompt.len()); let start = std::time::Instant::now(); let response = self.call_llm(&prompt, bot_id).await?; let elapsed = start.elapsed(); info!("[INTENT_CLASSIFIER] LLM classification completed in {:?}, response_len={} chars", elapsed, response.len()); trace!("LLM classification response: {}", &response[..response.len().min(500)]); Self::parse_classification_response(&response, intent) } fn parse_classification_response( response: &str, original_intent: &str, ) -> Result> { #[derive(Deserialize)] struct LlmResponse { intent_type: String, confidence: f64, subject: Option, action: Option, domain: Option, time_spec: Option, condition: Option, recipient: Option, features: Option>, tables: Option>, trigger_phrases: Option>, target_value: Option, suggested_name: Option, requires_clarification: Option, clarification_question: Option, alternatives: Option>, } #[derive(Deserialize)] struct TimeSpecResponse { #[serde(rename = "type")] schedule_type: Option, time: Option, day: Option, interval: Option, cron_expression: Option, } #[derive(Deserialize)] struct AlternativeResponse { #[serde(rename = "type")] intent_type: String, confidence: f64, reason: String, } // Clean response - remove markdown code blocks if present let cleaned = response .trim() .trim_start_matches("```json") .trim_start_matches("```JSON") .trim_start_matches("```") .trim_end_matches("```") .trim(); trace!("Cleaned classification response: {}", &cleaned[..cleaned.len().min(300)]); // Try to parse, fall back to heuristic classification let parsed: Result = serde_json::from_str(cleaned); match parsed { Ok(resp) => { let intent_type = IntentType::from(resp.intent_type.as_str()); let time_spec = resp.time_spec.map(|ts| TimeSpec { schedule_type: match ts.schedule_type.as_deref() { Some("DAILY") => ScheduleType::Daily, Some("WEEKLY") => ScheduleType::Weekly, Some("MONTHLY") => ScheduleType::Monthly, Some("INTERVAL") => ScheduleType::Interval, Some("CRON") => ScheduleType::Cron, _ => ScheduleType::Once, }, time: ts.time, day: ts.day, interval: ts.interval, cron_expression: ts.cron_expression, }); let alternatives = resp .alternatives .unwrap_or_default() .into_iter() .map(|a| AlternativeClassification { intent_type: IntentType::from(a.intent_type.as_str()), confidence: a.confidence, reason: a.reason, }) .collect(); Ok(ClassifiedIntent { id: Uuid::new_v4().to_string(), original_text: original_intent.to_string(), intent_type, confidence: resp.confidence, entities: ClassifiedEntities { subject: resp.subject, action: resp.action, domain: resp.domain, time_spec, condition: resp.condition, recipient: resp.recipient, features: resp.features.unwrap_or_default(), tables: resp.tables.unwrap_or_default(), trigger_phrases: resp.trigger_phrases.unwrap_or_default(), target_value: resp.target_value, }, suggested_name: resp.suggested_name, requires_clarification: resp.requires_clarification.unwrap_or(false), clarification_question: resp.clarification_question, alternative_types: alternatives, classified_at: Utc::now(), }) } Err(e) => { warn!("Failed to parse LLM response, using heuristic: {e}"); trace!("Raw response that failed to parse: {}", &response[..response.len().min(200)]); Self::classify_heuristic(original_intent) } } } fn classify_heuristic( intent: &str, ) -> Result> { let lower = intent.to_lowercase(); let (intent_type, confidence) = if lower.contains("create app") || lower.contains("build app") || lower.contains("make app") || lower.contains("crm") || lower.contains("management system") || lower.contains("inventory") || lower.contains("booking") || lower.contains("calculator") || lower.contains("website") || lower.contains("webpage") || lower.contains("web page") || lower.contains("landing page") || lower.contains("dashboard") || lower.contains("form") || lower.contains("todo list") || lower.contains("todo app") || lower.contains("chat") || lower.contains("blog") || lower.contains("portfolio") || lower.contains("store") || lower.contains("shop") || lower.contains("e-commerce") || lower.contains("ecommerce") || (lower.contains("create") && lower.contains("html")) || (lower.contains("make") && lower.contains("html")) || (lower.contains("build") && lower.contains("html")) || (lower.contains("create a") && (lower.contains("simple") || lower.contains("basic"))) || (lower.contains("make a") && (lower.contains("simple") || lower.contains("basic"))) || (lower.contains("build a") && (lower.contains("simple") || lower.contains("basic"))) || lower.contains("criar") || lower.contains("fazer") || lower.contains("construir") { (IntentType::AppCreate, 0.75) } else if lower.contains("remind") || lower.contains("call ") || lower.contains("tomorrow") || lower.contains("don't forget") { (IntentType::Todo, 0.70) } else if lower.contains("alert when") || lower.contains("notify if") || lower.contains("watch for") || lower.contains("monitor") { (IntentType::Monitor, 0.70) } else if lower.contains("send email") || lower.contains("delete all") || lower.contains("update all") || lower.contains("export") { (IntentType::Action, 0.65) } else if lower.contains("every day") || lower.contains("daily") || lower.contains("weekly") || lower.contains("at 9") || lower.contains("at 8") { (IntentType::Schedule, 0.70) } else if lower.contains("increase") || lower.contains("improve") || lower.contains("achieve") || lower.contains("grow by") { (IntentType::Goal, 0.60) } else if lower.contains("when i say") || lower.contains("create command") || lower.contains("shortcut") { (IntentType::Tool, 0.70) } else { (IntentType::Unknown, 0.30) }; Ok(ClassifiedIntent { id: Uuid::new_v4().to_string(), original_text: intent.to_string(), intent_type, confidence, entities: ClassifiedEntities::default(), suggested_name: None, requires_clarification: intent_type == IntentType::Unknown, clarification_question: if intent_type == IntentType::Unknown { Some("Could you please clarify what you'd like me to do?".to_string()) } else { None }, alternative_types: Vec::new(), classified_at: Utc::now(), }) } async fn handle_app_create( &self, classification: &ClassifiedIntent, session: &UserSession, task_id: Option, ) -> Result> { info!("Handling APP_CREATE intent"); let mut app_generator = if let Some(tid) = task_id { AppGenerator::with_task_id(self.state.clone(), tid) } else { AppGenerator::new(self.state.clone()) }; match app_generator .generate_app(&classification.original_text, session) .await { Ok(app) => { let mut resources = Vec::new(); // Track created tables for table in &app.tables { resources.push(CreatedResource { resource_type: "table".to_string(), name: table.name.clone(), path: Some("tables.bas".to_string()), }); } for page in &app.pages { resources.push(CreatedResource { resource_type: "page".to_string(), name: page.filename.clone(), path: Some(page.filename.clone()), }); } for tool in &app.tools { resources.push(CreatedResource { resource_type: "tool".to_string(), name: tool.filename.clone(), path: Some(tool.filename.clone()), }); } let app_url = format!("/apps/{}", app.name.to_lowercase().replace(' ', "-")); Ok(IntentResult { success: true, intent_type: IntentType::AppCreate, message: format!( "Done:\n{}\nApp available at {}", resources .iter() .filter(|r| r.resource_type == "table") .map(|r| format!("{} table created", r.name)) .collect::>() .join("\n"), app_url ), created_resources: resources, app_url: Some(app_url), task_id: None, schedule_id: None, tool_triggers: Vec::new(), next_steps: vec![ "Open the app to start using it".to_string(), "Use Designer to customize the app".to_string(), ], error: None, }) } Err(e) => { error!("Failed to generate app: {e}"); Ok(IntentResult { success: false, intent_type: IntentType::AppCreate, message: "Failed to create the application".to_string(), created_resources: Vec::new(), app_url: None, task_id: None, schedule_id: None, tool_triggers: Vec::new(), next_steps: vec!["Try again with more details".to_string()], error: Some(e.to_string()), }) } } } fn handle_todo( &self, classification: &ClassifiedIntent, session: &UserSession, ) -> Result> { info!("Handling TODO intent"); let task_id = Uuid::new_v4(); let title = classification .suggested_name .clone() .unwrap_or_else(|| classification.original_text.clone()); let mut conn = self.state.conn.get()?; // Insert into tasks table (no bot_id column in tasks table) sql_query( "INSERT INTO tasks (id, title, description, status, priority, created_at) VALUES ($1, $2, $3, 'pending', 'normal', NOW())", ) .bind::(task_id) .bind::(&title) .bind::(&classification.original_text) .execute(&mut conn)?; Ok(IntentResult { success: true, intent_type: IntentType::Todo, message: format!("Task saved: {title}"), created_resources: vec![CreatedResource { resource_type: "task".to_string(), name: title, path: None, }], app_url: None, task_id: Some(task_id.to_string()), schedule_id: None, tool_triggers: Vec::new(), next_steps: vec!["View tasks in your task list".to_string()], error: None, }) } fn handle_monitor( &self, classification: &ClassifiedIntent, session: &UserSession, ) -> Result> { info!("Handling MONITOR intent"); let subject = classification .entities .subject .clone() .unwrap_or_else(|| "data".to_string()); let condition = classification .entities .condition .clone() .unwrap_or_else(|| "changes".to_string()); // Generate ON CHANGE handler BASIC code let handler_name = format!("monitor_{}.bas", subject.to_lowercase().replace(' ', "_")); let basic_code = format!( r#"' Monitor: {subject} ' Condition: {condition} ' Created: {} ON CHANGE "{subject}" current_value = GET "{subject}" IF {condition} THEN TALK "Alert: {subject} has changed" ' Add notification logic here END IF END ON "#, Utc::now().format("%Y-%m-%d %H:%M") ); // Save to .gbdialog/events/ let event_path = format!(".gbdialog/events/{handler_name}"); self.save_basic_file(session.bot_id, &event_path, &basic_code)?; Ok(IntentResult { success: true, intent_type: IntentType::Monitor, message: format!("Monitor created for: {subject}"), created_resources: vec![CreatedResource { resource_type: "event".to_string(), name: handler_name, path: Some(event_path), }], app_url: None, task_id: None, schedule_id: None, tool_triggers: Vec::new(), next_steps: vec![format!("You'll be notified when {subject} {condition}")], error: None, }) } async fn handle_action( &self, classification: &ClassifiedIntent, session: &UserSession, ) -> Result> { info!("Handling ACTION intent"); // Compile the intent into an execution plan let compiled = self .intent_compiler .compile(&classification.original_text, session) .await?; // For immediate actions, we'd execute the plan // For safety, high-risk actions require approval if compiled.risk_assessment.requires_human_review { return Ok(IntentResult { success: false, intent_type: IntentType::Action, message: format!( "This action requires approval:\n{}", compiled.risk_assessment.review_reason.unwrap_or_default() ), created_resources: Vec::new(), app_url: None, task_id: Some(compiled.id), schedule_id: None, tool_triggers: Vec::new(), next_steps: vec!["Approve the action to proceed".to_string()], error: None, }); } // Execute low-risk actions immediately // In production, this would run the BASIC program Ok(IntentResult { success: true, intent_type: IntentType::Action, message: format!( "Executing: {}\nSteps: {}", compiled.plan.name, compiled.plan.steps.len() ), created_resources: Vec::new(), app_url: None, task_id: Some(compiled.id), schedule_id: None, tool_triggers: Vec::new(), next_steps: vec!["Action is being executed".to_string()], error: None, }) } fn handle_schedule( &self, classification: &ClassifiedIntent, session: &UserSession, ) -> Result> { info!("Handling SCHEDULE intent"); let schedule_name = classification .suggested_name .clone() .unwrap_or_else(|| "scheduled-task".to_string()) .to_lowercase() .replace(' ', "-"); let time_spec = classification .entities .time_spec .as_ref() .map(|ts| { format!( "{} at {}", match ts.schedule_type { ScheduleType::Daily => "Every day", ScheduleType::Weekly => "Every week", ScheduleType::Monthly => "Every month", _ => "Once", }, ts.time.as_deref().unwrap_or("9:00 AM") ) }) .unwrap_or_else(|| "Every day at 9:00 AM".to_string()); // Generate scheduler BASIC code let scheduler_file = format!("{schedule_name}.bas"); let basic_code = format!( r#"' Scheduler: {schedule_name} ' Schedule: {time_spec} ' Created: {} SET SCHEDULE "{time_spec}" ' Task logic from: {} TALK "Running scheduled task: {schedule_name}" ' Add your automation logic here END SCHEDULE "#, Utc::now().format("%Y-%m-%d %H:%M"), classification.original_text ); // Save to .gbdialog/schedulers/ let scheduler_path = format!(".gbdialog/schedulers/{scheduler_file}"); self.save_basic_file(session.bot_id, &scheduler_path, &basic_code)?; let schedule_id = Uuid::new_v4(); Ok(IntentResult { success: true, intent_type: IntentType::Schedule, message: format!("Scheduler created: {scheduler_file}\nSchedule: {time_spec}"), created_resources: vec![CreatedResource { resource_type: "scheduler".to_string(), name: scheduler_file, path: Some(scheduler_path), }], app_url: None, task_id: None, schedule_id: Some(schedule_id.to_string()), tool_triggers: Vec::new(), next_steps: vec![format!("The task will run {time_spec}")], error: None, }) } fn handle_goal( &self, classification: &ClassifiedIntent, session: &UserSession, ) -> Result> { info!("Handling GOAL intent"); let goal_name = classification .suggested_name .clone() .unwrap_or_else(|| "goal".to_string()); let target = classification .entities .target_value .clone() .unwrap_or_else(|| "unspecified".to_string()); let _goal_id = Uuid::new_v4(); // Goals are more complex - they create a monitoring + action loop let basic_code = format!( r#"' Goal: {goal_name} ' Target: {target} ' Created: {} ' This goal runs as an autonomous loop SET GOAL "{goal_name}" TARGET = "{target}" ' Check current metrics current = GET_METRIC "{goal_name}" ' LLM analyzes progress and suggests actions analysis = LLM "Analyze progress toward {target}. Current: " + current ' Execute suggested improvements IF analysis.has_action THEN EXECUTE analysis.action END IF ' Report progress TALK "Goal progress: " + current + " / " + TARGET END GOAL "#, Utc::now().format("%Y-%m-%d %H:%M") ); // Save to .gbdialog/goals/ let goal_file = format!("{}.bas", goal_name.to_lowercase().replace(' ', "-")); let goal_path = format!(".gbdialog/goals/{goal_file}"); self.save_basic_file(session.bot_id, &goal_path, &basic_code)?; Ok(IntentResult { success: true, intent_type: IntentType::Goal, message: format!("Goal created: {goal_name}\nTarget: {target}"), created_resources: vec![CreatedResource { resource_type: "goal".to_string(), name: goal_name, path: Some(goal_path), }], app_url: None, task_id: None, schedule_id: None, tool_triggers: Vec::new(), next_steps: vec![ "The system will work toward this goal autonomously".to_string(), "Check progress in the Goals dashboard".to_string(), ], error: None, }) } fn handle_tool( &self, classification: &ClassifiedIntent, session: &UserSession, ) -> Result> { info!("Handling TOOL intent"); let tool_name = classification .suggested_name .clone() .unwrap_or_else(|| "custom-command".to_string()) .to_lowercase() .replace(' ', "-"); let triggers = if classification.entities.trigger_phrases.is_empty() { vec![tool_name.clone()] } else { classification.entities.trigger_phrases.clone() }; let triggers_str = triggers .iter() .map(|t| format!("\"{}\"", t)) .collect::>() .join(", "); // Generate tool BASIC code let tool_file = format!("{tool_name}.bas"); let basic_code = format!( r#"' Tool: {tool_name} ' Triggers: {triggers_str} ' Created: {} TRIGGER {triggers_str} ' Command logic from: {} TALK "Running command: {tool_name}" ' Add your command logic here END TRIGGER "#, Utc::now().format("%Y-%m-%d %H:%M"), classification.original_text ); // Save to .gbdialog/tools/ let tool_path = format!(".gbdialog/tools/{tool_file}"); self.save_basic_file(session.bot_id, &tool_path, &basic_code)?; Ok(IntentResult { success: true, intent_type: IntentType::Tool, message: format!( "Command created: {tool_file}\nTriggers: {}", triggers.join(", ") ), created_resources: vec![CreatedResource { resource_type: "tool".to_string(), name: tool_file, path: Some(tool_path), }], app_url: None, task_id: None, schedule_id: None, tool_triggers: triggers, next_steps: vec!["Say any of the trigger phrases to use the command".to_string()], error: None, }) } fn handle_unknown( classification: &ClassifiedIntent, ) -> Result> { info!("Handling UNKNOWN intent - requesting clarification"); 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 .alternative_types .iter() .map(|a| format!("- {}: {}", a.intent_type, a.reason)) .collect::>() .join("\n") }; Ok(IntentResult { success: false, intent_type: IntentType::Unknown, message: format!( "I'm not sure what you'd like me to do. Could you clarify?\n\nPossible interpretations:\n{}", suggestions ), created_resources: Vec::new(), app_url: None, task_id: None, schedule_id: None, tool_triggers: Vec::new(), next_steps: vec!["Provide more details about what you want".to_string()], error: None, }) } // ========================================================================= // HELPER METHODS // ========================================================================= /// Call LLM for classification async fn call_llm( &self, prompt: &str, bot_id: Uuid, ) -> Result> { trace!("Calling LLM for intent classification"); #[cfg(feature = "llm")] { // Get model and key from bot configuration let config_manager = ConfigManager::new(self.state.conn.clone()); let model = config_manager .get_config(&bot_id, "llm-model", None) .unwrap_or_else(|_| { config_manager .get_config(&Uuid::nil(), "llm-model", None) .unwrap_or_else(|_| "gpt-4".to_string()) }); let key = config_manager .get_config(&bot_id, "llm-key", None) .unwrap_or_else(|_| { config_manager .get_config(&Uuid::nil(), "llm-key", None) .unwrap_or_default() }); let llm_config = serde_json::json!({ "temperature": 0.3, "max_tokens": 1000 }); let response = self .state .llm_provider .generate(prompt, &llm_config, &model, &key) .await?; return Ok(response); } #[cfg(not(feature = "llm"))] { warn!("LLM feature not enabled, using heuristic classification"); Ok("{}".to_string()) } } /// Save a BASIC file to the bot's directory fn save_basic_file( &self, bot_id: Uuid, path: &str, content: &str, ) -> Result<(), Box> { let site_path = self .state .config .as_ref() .map(|c| c.site_path.clone()) .unwrap_or_else(|| "./botserver-stack/sites".to_string()); let full_path = format!("{}/{}.gbai/{}", site_path, bot_id, path); // Create directory if needed if let Some(dir) = std::path::Path::new(&full_path).parent() { if !dir.exists() { std::fs::create_dir_all(dir)?; } } std::fs::write(&full_path, content)?; info!("Saved BASIC file: {full_path}"); Ok(()) } /// Store classification for analytics fn store_classification( &self, classification: &ClassifiedIntent, session: &UserSession, ) -> Result<(), Box> { let mut conn = self.state.conn.get()?; sql_query( "INSERT INTO intent_classifications (id, bot_id, session_id, original_text, intent_type, confidence, entities, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW()) ON CONFLICT DO NOTHING", ) .bind::(Uuid::parse_str(&classification.id)?) .bind::(session.bot_id) .bind::(session.id) .bind::(&classification.original_text) .bind::(&classification.intent_type.to_string()) .bind::(classification.confidence) .bind::(serde_json::to_string(&classification.entities)?) .execute(&mut conn) .ok(); // Ignore errors - analytics shouldn't break the flow Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_intent_type_from_str() { assert_eq!(IntentType::from("APP_CREATE"), IntentType::AppCreate); assert_eq!(IntentType::from("app"), IntentType::AppCreate); assert_eq!(IntentType::from("TODO"), IntentType::Todo); assert_eq!(IntentType::from("reminder"), IntentType::Todo); assert_eq!(IntentType::from("MONITOR"), IntentType::Monitor); assert_eq!(IntentType::from("SCHEDULE"), IntentType::Schedule); assert_eq!(IntentType::from("daily"), IntentType::Schedule); assert_eq!(IntentType::from("unknown_value"), IntentType::Unknown); } #[test] fn test_intent_type_display() { assert_eq!(IntentType::AppCreate.to_string(), "APP_CREATE"); assert_eq!(IntentType::Todo.to_string(), "TODO"); assert_eq!(IntentType::Monitor.to_string(), "MONITOR"); } }