Some checks failed
BotServer CI / build (push) Failing after 1m34s
Split 20+ files over 1000 lines into focused subdirectories for better maintainability and code organization. All changes maintain backward compatibility through re-export wrappers. Major splits: - attendance/llm_assist.rs (2074→7 modules) - basic/keywords/face_api.rs → face_api/ (7 modules) - basic/keywords/file_operations.rs → file_ops/ (8 modules) - basic/keywords/hear_talk.rs → hearing/ (6 modules) - channels/wechat.rs → wechat/ (10 modules) - channels/youtube.rs → youtube/ (5 modules) - contacts/mod.rs → contacts_api/ (6 modules) - core/bootstrap/mod.rs → bootstrap/ (5 modules) - core/shared/admin.rs → admin_*.rs (5 modules) - designer/canvas.rs → canvas_api/ (6 modules) - designer/mod.rs → designer_api/ (6 modules) - docs/handlers.rs → handlers_api/ (11 modules) - drive/mod.rs → drive_handlers.rs, drive_types.rs - learn/mod.rs → types.rs - main.rs → main_module/ (7 modules) - meet/webinar.rs → webinar_api/ (8 modules) - paper/mod.rs → (10 modules) - security/auth.rs → auth_api/ (7 modules) - security/passkey.rs → (4 modules) - sources/mod.rs → sources_api/ (5 modules) - tasks/mod.rs → task_api/ (5 modules) Stats: 38,040 deletions, 1,315 additions across 318 files Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
949 lines
30 KiB
Rust
949 lines
30 KiB
Rust
|
|
use crate::core::shared::models::UserSession;
|
|
use crate::core::shared::state::AppState;
|
|
#[cfg(feature = "llm")]
|
|
use crate::core::config::ConfigManager;
|
|
use chrono::{DateTime, Utc};
|
|
use diesel::prelude::*;
|
|
use log::{error, info, trace, warn};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::fmt::Write;
|
|
use std::sync::Arc;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CompiledIntent {
|
|
pub id: String,
|
|
pub original_intent: String,
|
|
pub entities: IntentEntities,
|
|
pub plan: ExecutionPlan,
|
|
pub basic_program: String,
|
|
pub confidence: f64,
|
|
pub alternatives: Vec<AlternativeInterpretation>,
|
|
pub risk_assessment: RiskAssessment,
|
|
pub resource_estimate: ResourceEstimate,
|
|
pub compiled_at: DateTime<Utc>,
|
|
pub session_id: String,
|
|
pub bot_id: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct IntentEntities {
|
|
pub action: String,
|
|
pub target: String,
|
|
pub domain: Option<String>,
|
|
pub client: Option<String>,
|
|
pub features: Vec<String>,
|
|
pub constraints: Vec<Constraint>,
|
|
pub technologies: Vec<String>,
|
|
pub data_sources: Vec<String>,
|
|
pub integrations: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Constraint {
|
|
pub constraint_type: ConstraintType,
|
|
pub value: String,
|
|
pub is_hard: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub enum ConstraintType {
|
|
Budget,
|
|
Timeline,
|
|
Technology,
|
|
Security,
|
|
Compliance,
|
|
Performance,
|
|
Scalability,
|
|
Custom(String),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ExecutionPlan {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub description: String,
|
|
pub steps: Vec<PlanStep>,
|
|
pub dependencies: HashMap<String, Vec<String>>,
|
|
pub estimated_duration_minutes: i32,
|
|
pub requires_approval: bool,
|
|
pub approval_levels: Vec<ApprovalLevel>,
|
|
pub rollback_plan: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PlanStep {
|
|
pub id: String,
|
|
pub order: i32,
|
|
pub name: String,
|
|
pub description: String,
|
|
pub keywords: Vec<String>,
|
|
pub basic_code: String,
|
|
pub priority: StepPriority,
|
|
pub risk_level: RiskLevel,
|
|
pub estimated_minutes: i32,
|
|
pub requires_approval: bool,
|
|
pub can_rollback: bool,
|
|
pub dependencies: Vec<String>,
|
|
pub outputs: Vec<String>,
|
|
pub mcp_servers: Vec<String>,
|
|
pub api_calls: Vec<ApiCallSpec>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[derive(Default)]
|
|
pub enum StepPriority {
|
|
Critical,
|
|
High,
|
|
#[default]
|
|
Medium,
|
|
Low,
|
|
Optional,
|
|
}
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
|
#[derive(Default)]
|
|
pub enum RiskLevel {
|
|
None,
|
|
#[default]
|
|
Low,
|
|
Medium,
|
|
High,
|
|
Critical,
|
|
}
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ApiCallSpec {
|
|
pub name: String,
|
|
pub method: String,
|
|
pub url_template: String,
|
|
pub headers: HashMap<String, String>,
|
|
pub body_template: Option<String>,
|
|
pub auth_type: AuthType,
|
|
pub retry_config: RetryConfig,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[derive(Default)]
|
|
pub enum AuthType {
|
|
#[default]
|
|
None,
|
|
ApiKey {
|
|
header: String,
|
|
key_ref: String,
|
|
},
|
|
Bearer {
|
|
token_ref: String,
|
|
},
|
|
Basic {
|
|
user_ref: String,
|
|
pass_ref: String,
|
|
},
|
|
OAuth2 {
|
|
client_id_ref: String,
|
|
client_secret_ref: String,
|
|
},
|
|
}
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RetryConfig {
|
|
pub max_retries: i32,
|
|
pub backoff_ms: i32,
|
|
pub retry_on_status: Vec<i32>,
|
|
}
|
|
|
|
impl Default for RetryConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
max_retries: 3,
|
|
backoff_ms: 1000,
|
|
retry_on_status: vec![429, 500, 502, 503, 504],
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ApprovalLevel {
|
|
pub level: i32,
|
|
pub approver: String,
|
|
pub reason: String,
|
|
pub timeout_minutes: i32,
|
|
pub default_action: DefaultApprovalAction,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[derive(Default)]
|
|
pub enum DefaultApprovalAction {
|
|
Approve,
|
|
Reject,
|
|
Escalate,
|
|
#[default]
|
|
Pause,
|
|
}
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AlternativeInterpretation {
|
|
pub id: String,
|
|
pub description: String,
|
|
pub confidence: f64,
|
|
pub plan_summary: String,
|
|
pub pros: Vec<String>,
|
|
pub cons: Vec<String>,
|
|
pub estimated_cost: Option<f64>,
|
|
pub estimated_time_hours: Option<f64>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RiskAssessment {
|
|
pub overall_risk: RiskLevel,
|
|
pub risks: Vec<IdentifiedRisk>,
|
|
pub mitigations: Vec<RiskMitigation>,
|
|
pub requires_human_review: bool,
|
|
pub review_reason: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct IdentifiedRisk {
|
|
pub id: String,
|
|
pub category: RiskCategory,
|
|
pub description: String,
|
|
pub probability: f64,
|
|
pub impact: RiskLevel,
|
|
pub affected_steps: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum RiskCategory {
|
|
DataLoss,
|
|
SecurityBreach,
|
|
CostOverrun,
|
|
TimelineSlip,
|
|
IntegrationFailure,
|
|
ComplianceViolation,
|
|
PerformanceIssue,
|
|
DependencyFailure,
|
|
Custom(String),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RiskMitigation {
|
|
pub risk_id: String,
|
|
pub strategy: String,
|
|
pub basic_code: Option<String>,
|
|
pub fallback_plan: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ResourceEstimate {
|
|
pub compute_hours: f64,
|
|
pub storage_gb: f64,
|
|
pub api_calls: i32,
|
|
pub llm_tokens: i32,
|
|
pub estimated_cost_usd: f64,
|
|
pub human_hours: f64,
|
|
pub mcp_servers_needed: Vec<String>,
|
|
pub external_services: Vec<String>,
|
|
}
|
|
|
|
impl Default for ResourceEstimate {
|
|
fn default() -> Self {
|
|
Self {
|
|
compute_hours: 0.0,
|
|
storage_gb: 0.0,
|
|
api_calls: 0,
|
|
llm_tokens: 0,
|
|
estimated_cost_usd: 0.0,
|
|
human_hours: 0.0,
|
|
mcp_servers_needed: Vec::new(),
|
|
external_services: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct IntentCompiler {
|
|
state: Arc<AppState>,
|
|
config: IntentCompilerConfig,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct IntentCompilerConfig {
|
|
pub enabled: bool,
|
|
pub model: String,
|
|
pub temperature: f64,
|
|
pub max_tokens: i32,
|
|
pub auto_execute_low_risk: bool,
|
|
pub require_approval_above: RiskLevel,
|
|
pub simulate_before_execute: bool,
|
|
pub max_plan_steps: i32,
|
|
pub available_keywords: Vec<String>,
|
|
pub available_mcp_servers: Vec<String>,
|
|
}
|
|
|
|
impl Default for IntentCompilerConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: true,
|
|
model: "gpt-4".to_string(),
|
|
temperature: 0.3,
|
|
max_tokens: 4000,
|
|
auto_execute_low_risk: false,
|
|
require_approval_above: RiskLevel::Medium,
|
|
simulate_before_execute: true,
|
|
max_plan_steps: 50,
|
|
available_keywords: get_all_keywords(),
|
|
available_mcp_servers: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for IntentCompiler {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("IntentCompiler")
|
|
.field("config", &self.config)
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
impl IntentCompiler {
|
|
pub fn new(state: Arc<AppState>) -> Self {
|
|
Self {
|
|
state,
|
|
config: IntentCompilerConfig::default(),
|
|
}
|
|
}
|
|
|
|
pub fn with_config(state: Arc<AppState>, config: IntentCompilerConfig) -> Self {
|
|
Self { state, config }
|
|
}
|
|
|
|
pub async fn compile(
|
|
&self,
|
|
intent: &str,
|
|
session: &UserSession,
|
|
) -> Result<CompiledIntent, Box<dyn std::error::Error + Send + Sync>> {
|
|
info!(
|
|
"Compiling intent for session {}: {}",
|
|
session.id,
|
|
&intent[..intent.len().min(100)]
|
|
);
|
|
|
|
let entities = self.extract_entities(intent, session.bot_id).await?;
|
|
trace!("Extracted entities: {entities:?}");
|
|
|
|
let plan = self.generate_plan(intent, &entities, session.bot_id).await?;
|
|
trace!("Generated plan with {} steps", plan.steps.len());
|
|
|
|
let basic_program = Self::generate_basic_program(&plan, &entities);
|
|
trace!(
|
|
"Generated BASIC program: {} lines",
|
|
basic_program.lines().count()
|
|
);
|
|
|
|
let risk_assessment = Self::assess_risks(&plan);
|
|
|
|
let resource_estimate = Self::estimate_resources(&plan);
|
|
|
|
let (confidence, alternatives) = Self::check_ambiguity();
|
|
|
|
let compiled = CompiledIntent {
|
|
id: Uuid::new_v4().to_string(),
|
|
original_intent: intent.to_string(),
|
|
entities,
|
|
plan,
|
|
basic_program,
|
|
confidence,
|
|
alternatives,
|
|
risk_assessment,
|
|
resource_estimate,
|
|
compiled_at: Utc::now(),
|
|
session_id: session.id.to_string(),
|
|
bot_id: session.bot_id.to_string(),
|
|
};
|
|
|
|
Self::store_compiled_intent(&compiled, &self.state);
|
|
|
|
Ok(compiled)
|
|
}
|
|
|
|
async fn extract_entities(
|
|
&self,
|
|
intent: &str,
|
|
bot_id: Uuid,
|
|
) -> Result<IntentEntities, Box<dyn std::error::Error + Send + Sync>> {
|
|
let prompt = format!(
|
|
r#"Analyze this user request and extract structured information.
|
|
|
|
User Request: "{intent}"
|
|
|
|
Extract the following as JSON:
|
|
{{
|
|
"action": "primary action (create/update/delete/analyze/report/integrate/automate)",
|
|
"target": "what to create/modify (CRM, website, report, API, etc.)",
|
|
"domain": "industry/domain if mentioned (financial, healthcare, retail, etc.) or null",
|
|
"client": "client/company name if mentioned or null",
|
|
"features": ["list of specific features requested"],
|
|
"constraints": [
|
|
{{"type": "budget|timeline|technology|security|compliance|performance", "value": "constraint value", "is_hard": true/false}}
|
|
],
|
|
"technologies": ["specific technologies/tools mentioned"],
|
|
"data_sources": ["data sources mentioned"],
|
|
"integrations": ["external systems to integrate with"]
|
|
}}
|
|
|
|
Respond ONLY with valid JSON, no explanation."#
|
|
);
|
|
|
|
let response = self.call_llm(&prompt, bot_id).await?;
|
|
let entities: IntentEntities = serde_json::from_str(&response).unwrap_or_else(|e| {
|
|
warn!("Failed to parse entity extraction response: {e}");
|
|
IntentEntities {
|
|
action: "create".to_string(),
|
|
target: intent.to_string(),
|
|
..Default::default()
|
|
}
|
|
});
|
|
|
|
Ok(entities)
|
|
}
|
|
|
|
async fn generate_plan(
|
|
&self,
|
|
intent: &str,
|
|
entities: &IntentEntities,
|
|
bot_id: Uuid,
|
|
) -> Result<ExecutionPlan, Box<dyn std::error::Error + Send + Sync>> {
|
|
let keywords_list = self.config.available_keywords.join(", ");
|
|
let mcp_servers_list = self.config.available_mcp_servers.join(", ");
|
|
|
|
let prompt = format!(
|
|
r#"Generate an execution plan for this task.
|
|
|
|
Original Request: "{intent}"
|
|
|
|
Extracted Information:
|
|
- Action: {}
|
|
- Target: {}
|
|
- Domain: {}
|
|
- Client: {}
|
|
- Features: {:?}
|
|
- Technologies: {:?}
|
|
- Integrations: {:?}
|
|
|
|
Available BASIC Keywords: {keywords_list}
|
|
Available MCP Servers: {mcp_servers_list}
|
|
|
|
Generate a detailed execution plan as JSON:
|
|
{{
|
|
"name": "short plan name",
|
|
"description": "brief description",
|
|
"steps": [
|
|
{{
|
|
"id": "step-1",
|
|
"order": 1,
|
|
"name": "Step name",
|
|
"description": "What this step does",
|
|
"keywords": ["BASIC keywords this step uses"],
|
|
"priority": "CRITICAL|HIGH|MEDIUM|LOW|OPTIONAL",
|
|
"risk_level": "NONE|LOW|MEDIUM|HIGH|CRITICAL",
|
|
"estimated_minutes": 5,
|
|
"requires_approval": false,
|
|
"can_rollback": true,
|
|
"dependencies": [],
|
|
"outputs": ["variables/resources produced"],
|
|
"mcp_servers": ["MCP servers needed"],
|
|
"api_calls": []
|
|
}}
|
|
],
|
|
"requires_approval": true/false,
|
|
"estimated_duration_minutes": 60,
|
|
"rollback_plan": "how to undo if needed"
|
|
}}
|
|
|
|
Maximum {} steps. Focus on practical, executable steps.
|
|
Respond ONLY with valid JSON."#,
|
|
entities.action,
|
|
entities.target,
|
|
entities.domain.as_deref().unwrap_or("general"),
|
|
entities.client.as_deref().unwrap_or("none"),
|
|
entities.features,
|
|
entities.technologies,
|
|
entities.integrations,
|
|
self.config.max_plan_steps
|
|
);
|
|
|
|
let response = self.call_llm(&prompt, bot_id).await?;
|
|
|
|
#[derive(Deserialize)]
|
|
struct PlanResponse {
|
|
name: String,
|
|
description: String,
|
|
steps: Vec<PlanStepResponse>,
|
|
requires_approval: Option<bool>,
|
|
estimated_duration_minutes: Option<i32>,
|
|
rollback_plan: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct PlanStepResponse {
|
|
id: String,
|
|
order: i32,
|
|
name: String,
|
|
description: String,
|
|
keywords: Vec<String>,
|
|
priority: Option<String>,
|
|
risk_level: Option<String>,
|
|
estimated_minutes: Option<i32>,
|
|
requires_approval: Option<bool>,
|
|
can_rollback: Option<bool>,
|
|
dependencies: Option<Vec<String>>,
|
|
outputs: Option<Vec<String>>,
|
|
mcp_servers: Option<Vec<String>>,
|
|
api_calls: Option<Vec<ApiCallSpec>>,
|
|
}
|
|
|
|
let plan_response: PlanResponse = serde_json::from_str(&response)?;
|
|
|
|
let steps: Vec<PlanStep> = plan_response
|
|
.steps
|
|
.into_iter()
|
|
.map(|s| PlanStep {
|
|
id: s.id,
|
|
order: s.order,
|
|
name: s.name,
|
|
description: s.description,
|
|
keywords: s.keywords,
|
|
basic_code: String::new(),
|
|
priority: match s.priority.as_deref() {
|
|
Some("CRITICAL") => StepPriority::Critical,
|
|
Some("HIGH") => StepPriority::High,
|
|
Some("LOW") => StepPriority::Low,
|
|
Some("OPTIONAL") => StepPriority::Optional,
|
|
Some("MEDIUM" | _) | None => StepPriority::Medium,
|
|
},
|
|
risk_level: match s.risk_level.as_deref() {
|
|
Some("NONE") => RiskLevel::None,
|
|
Some("MEDIUM") => RiskLevel::Medium,
|
|
Some("HIGH") => RiskLevel::High,
|
|
Some("CRITICAL") => RiskLevel::Critical,
|
|
Some("LOW" | _) | None => RiskLevel::Low,
|
|
},
|
|
estimated_minutes: s.estimated_minutes.unwrap_or(5),
|
|
requires_approval: s.requires_approval.unwrap_or(false),
|
|
can_rollback: s.can_rollback.unwrap_or(true),
|
|
dependencies: s.dependencies.unwrap_or_default(),
|
|
outputs: s.outputs.unwrap_or_default(),
|
|
mcp_servers: s.mcp_servers.unwrap_or_default(),
|
|
api_calls: s.api_calls.unwrap_or_default(),
|
|
})
|
|
.collect();
|
|
|
|
let mut dependencies: HashMap<String, Vec<String>> = HashMap::new();
|
|
for step in &steps {
|
|
dependencies.insert(step.id.clone(), step.dependencies.clone());
|
|
}
|
|
|
|
let approval_levels = Self::determine_approval_levels(&steps);
|
|
|
|
Ok(ExecutionPlan {
|
|
id: Uuid::new_v4().to_string(),
|
|
name: plan_response.name,
|
|
description: plan_response.description,
|
|
steps,
|
|
dependencies,
|
|
estimated_duration_minutes: plan_response.estimated_duration_minutes.unwrap_or(60),
|
|
requires_approval: plan_response.requires_approval.unwrap_or(false),
|
|
approval_levels,
|
|
rollback_plan: plan_response.rollback_plan,
|
|
})
|
|
}
|
|
|
|
fn generate_basic_program(plan: &ExecutionPlan, entities: &IntentEntities) -> String {
|
|
let mut program = String::new();
|
|
|
|
let _ = writeln!(
|
|
program,
|
|
"' ============================================================================="
|
|
);
|
|
let _ = writeln!(program, "' AUTO-GENERATED BASIC PROGRAM");
|
|
let _ = writeln!(program, "' Plan: {}", plan.name);
|
|
let _ = writeln!(program, "' Description: {}", plan.description);
|
|
let _ = writeln!(
|
|
program,
|
|
"' Generated: {}",
|
|
Utc::now().format("%Y-%m-%d %H:%M:%S")
|
|
);
|
|
let _ = writeln!(
|
|
program,
|
|
"' =============================================================================\n"
|
|
);
|
|
|
|
let _ = writeln!(
|
|
program,
|
|
"PLAN_START \"{}\", \"{}\"",
|
|
plan.name, plan.description
|
|
);
|
|
|
|
for step in &plan.steps {
|
|
let priority_str = match step.priority {
|
|
StepPriority::Critical => "CRITICAL",
|
|
StepPriority::High => "HIGH",
|
|
StepPriority::Medium => "MEDIUM",
|
|
StepPriority::Low => "LOW",
|
|
StepPriority::Optional => "OPTIONAL",
|
|
};
|
|
let _ = writeln!(
|
|
program,
|
|
" STEP {}, \"{}\", {}",
|
|
step.order, step.name, priority_str
|
|
);
|
|
}
|
|
let _ = writeln!(program, "PLAN_END\n");
|
|
|
|
let _ = writeln!(program, "' Initialize context variables");
|
|
let _ = writeln!(program, "SET action = \"{}\"", entities.action);
|
|
let _ = writeln!(program, "SET target = \"{}\"", entities.target);
|
|
if let Some(ref client) = entities.client {
|
|
let _ = writeln!(program, "SET client = \"{client}\"");
|
|
}
|
|
if let Some(ref domain) = entities.domain {
|
|
let _ = writeln!(program, "SET domain = \"{domain}\"");
|
|
}
|
|
program.push('\n');
|
|
|
|
let _ = writeln!(program, "' Set LLM context");
|
|
let _ = writeln!(
|
|
program,
|
|
"SET CONTEXT \"Task: {} {} for {}\"\n",
|
|
entities.action,
|
|
entities.target,
|
|
entities.client.as_deref().unwrap_or("general use")
|
|
);
|
|
|
|
for step in &plan.steps {
|
|
let _ = writeln!(
|
|
program,
|
|
"' -----------------------------------------------------------------------------"
|
|
);
|
|
let _ = writeln!(program, "' STEP {}: {}", step.order, step.name);
|
|
let _ = writeln!(program, "' {}", step.description);
|
|
let _ = writeln!(
|
|
program,
|
|
"' Risk: {:?}, Approval Required: {}",
|
|
step.risk_level, step.requires_approval
|
|
);
|
|
let _ = writeln!(
|
|
program,
|
|
"' -----------------------------------------------------------------------------"
|
|
);
|
|
|
|
let step_code = Self::generate_step_code(step);
|
|
let _ = writeln!(program, "{step_code}");
|
|
}
|
|
|
|
let _ = writeln!(program, "' Task completed");
|
|
let _ = writeln!(program, "TALK \"Task completed successfully!\"");
|
|
let _ = writeln!(
|
|
program,
|
|
"AUDIT_LOG \"plan-complete\", \"{}\", \"success\"",
|
|
plan.id
|
|
);
|
|
|
|
program
|
|
}
|
|
|
|
fn generate_step_code(step: &PlanStep) -> String {
|
|
if !step.basic_code.is_empty() {
|
|
return step.basic_code.clone();
|
|
}
|
|
|
|
let mut code = String::new();
|
|
let _ = writeln!(
|
|
code,
|
|
"TALK \"Executing step {}: {}\"",
|
|
step.order, step.name
|
|
);
|
|
code
|
|
}
|
|
|
|
async fn call_llm(
|
|
&self,
|
|
_prompt: &str,
|
|
_bot_id: Uuid,
|
|
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
|
trace!("Calling LLM with prompt length: {}", _prompt.len());
|
|
|
|
#[cfg(feature = "llm")]
|
|
{
|
|
let prompt = _prompt;
|
|
let bot_id = _bot_id;
|
|
// 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(|_| self.config.model.clone())
|
|
});
|
|
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": self.config.temperature,
|
|
"max_tokens": self.config.max_tokens
|
|
});
|
|
let response = self
|
|
.state
|
|
.llm_provider
|
|
.generate(prompt, &llm_config, &model, &key)
|
|
.await?;
|
|
Ok(response)
|
|
}
|
|
|
|
#[cfg(not(feature = "llm"))]
|
|
{
|
|
warn!("LLM feature not enabled, returning fallback response");
|
|
let response = serde_json::json!({
|
|
"action": "create",
|
|
"target": "system",
|
|
"domain": null,
|
|
"client": null,
|
|
"features": [],
|
|
"constraints": [],
|
|
"technologies": [],
|
|
"data_sources": [],
|
|
"integrations": []
|
|
});
|
|
Ok(response.to_string())
|
|
}
|
|
}
|
|
|
|
fn assess_risks(plan: &ExecutionPlan) -> RiskAssessment {
|
|
let mut risks = Vec::new();
|
|
let mut overall_risk = RiskLevel::Low;
|
|
|
|
for step in &plan.steps {
|
|
if step.risk_level >= RiskLevel::High {
|
|
overall_risk = step.risk_level;
|
|
risks.push(IdentifiedRisk {
|
|
id: format!("risk-{}", step.id),
|
|
category: RiskCategory::DependencyFailure,
|
|
description: format!("Step '{}' has high risk level", step.name),
|
|
probability: 0.3,
|
|
impact: step.risk_level,
|
|
affected_steps: vec![step.id.clone()],
|
|
});
|
|
}
|
|
}
|
|
|
|
RiskAssessment {
|
|
overall_risk,
|
|
risks,
|
|
mitigations: Vec::new(),
|
|
requires_human_review: overall_risk >= RiskLevel::High,
|
|
review_reason: if overall_risk >= RiskLevel::High {
|
|
Some("High risk steps detected".to_string())
|
|
} else {
|
|
None
|
|
},
|
|
}
|
|
}
|
|
|
|
fn estimate_resources(plan: &ExecutionPlan) -> ResourceEstimate {
|
|
let mut estimate = ResourceEstimate::default();
|
|
|
|
for step in &plan.steps {
|
|
estimate.compute_hours += f64::from(step.estimated_minutes) / 60.0;
|
|
estimate.api_calls += step.api_calls.len() as i32;
|
|
|
|
for keyword in &step.keywords {
|
|
if keyword == "LLM" {
|
|
estimate.llm_tokens += 1000;
|
|
}
|
|
}
|
|
|
|
for mcp in &step.mcp_servers {
|
|
if !estimate.mcp_servers_needed.contains(mcp) {
|
|
estimate.mcp_servers_needed.push(mcp.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
let llm_cost = f64::from(estimate.llm_tokens).mul_add(0.00002, 0.0);
|
|
estimate.estimated_cost_usd = estimate
|
|
.compute_hours
|
|
.mul_add(0.10, f64::from(estimate.api_calls) * 0.001)
|
|
+ llm_cost;
|
|
|
|
estimate
|
|
}
|
|
|
|
fn check_ambiguity() -> (f64, Vec<AlternativeInterpretation>) {
|
|
(0.85, Vec::new())
|
|
}
|
|
|
|
fn store_compiled_intent(compiled: &CompiledIntent, state: &Arc<AppState>) {
|
|
info!("Storing compiled intent: {}", compiled.id);
|
|
|
|
// Store in task_manifests cache for quick access
|
|
if let Ok(mut manifests) = state.task_manifests.write() {
|
|
use crate::auto_task::task_manifest::{TaskManifest, ManifestStatus, CurrentStatus, ProcessingStats};
|
|
|
|
let manifest = TaskManifest {
|
|
id: compiled.id.clone(),
|
|
app_name: compiled.entities.action.clone(),
|
|
description: compiled.original_intent.clone(),
|
|
created_at: compiled.compiled_at,
|
|
updated_at: compiled.compiled_at,
|
|
status: ManifestStatus::Ready,
|
|
current_status: CurrentStatus {
|
|
title: "Compiled".to_string(),
|
|
current_action: Some("Ready for execution".to_string()),
|
|
decision_point: None,
|
|
},
|
|
sections: Vec::new(),
|
|
total_steps: compiled.plan.steps.len() as u32,
|
|
completed_steps: 0,
|
|
runtime_seconds: 0,
|
|
estimated_seconds: (compiled.resource_estimate.compute_hours * 3600.0) as u64,
|
|
terminal_output: Vec::new(),
|
|
processing_stats: ProcessingStats::default(),
|
|
};
|
|
manifests.insert(compiled.id.clone(), manifest);
|
|
info!("Compiled intent {} stored in manifest cache", compiled.id);
|
|
}
|
|
|
|
// Also persist to database for durability
|
|
match state.conn.get() {
|
|
Ok(mut conn) => {
|
|
let compiled_json = serde_json::to_value(compiled).unwrap_or_default();
|
|
let insert_sql = format!(
|
|
"INSERT INTO compiled_intents (id, bot_id, session_id, original_intent, basic_program, confidence, compiled_at, data)
|
|
VALUES ('{}', '{}', '{}', '{}', '{}', {}, '{}', '{}')
|
|
ON CONFLICT (id) DO UPDATE SET
|
|
basic_program = EXCLUDED.basic_program,
|
|
confidence = EXCLUDED.confidence,
|
|
data = EXCLUDED.data,
|
|
compiled_at = EXCLUDED.compiled_at",
|
|
compiled.id,
|
|
compiled.bot_id,
|
|
compiled.session_id,
|
|
compiled.original_intent.replace('\'', "''"),
|
|
compiled.basic_program.replace('\'', "''"),
|
|
compiled.confidence,
|
|
compiled.compiled_at.to_rfc3339(),
|
|
compiled_json.to_string().replace('\'', "''")
|
|
);
|
|
|
|
match diesel::sql_query(&insert_sql).execute(&mut conn) {
|
|
Ok(_) => info!("Compiled intent {} persisted to database", compiled.id),
|
|
Err(e) => {
|
|
// Table might not exist yet - this is okay, cache is primary storage
|
|
trace!("Could not persist compiled intent to database (table may not exist): {}", e);
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to get database connection for storing compiled intent: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn _store_compiled_intent_simple(compiled: &CompiledIntent) {
|
|
// Simple version without state - just log
|
|
info!("Storing compiled intent (no state): {}", compiled.id);
|
|
}
|
|
|
|
fn determine_approval_levels(steps: &[PlanStep]) -> Vec<ApprovalLevel> {
|
|
let mut levels = Vec::new();
|
|
|
|
let has_high_risk = steps.iter().any(|s| s.risk_level >= RiskLevel::High);
|
|
|
|
if has_high_risk {
|
|
levels.push(ApprovalLevel {
|
|
level: 1,
|
|
approver: "admin".to_string(),
|
|
reason: "High risk steps require approval".to_string(),
|
|
timeout_minutes: 60,
|
|
default_action: DefaultApprovalAction::Pause,
|
|
});
|
|
}
|
|
|
|
levels
|
|
}
|
|
}
|
|
|
|
fn get_all_keywords() -> Vec<String> {
|
|
vec![
|
|
"ADD BOT".to_string(),
|
|
"ADD MEMBER".to_string(),
|
|
"ADD SUGGESTION".to_string(),
|
|
"ADD TOOL".to_string(),
|
|
"AUDIT_LOG".to_string(),
|
|
"BOOK".to_string(),
|
|
"CLEAR KB".to_string(),
|
|
"CLEAR TOOLS".to_string(),
|
|
"CREATE DRAFT".to_string(),
|
|
"CREATE SITE".to_string(),
|
|
"CREATE_TASK".to_string(),
|
|
"DELETE".to_string(),
|
|
"DELETE HTTP".to_string(),
|
|
"DOWNLOAD".to_string(),
|
|
"FILL".to_string(),
|
|
"FILTER".to_string(),
|
|
"FIND".to_string(),
|
|
"FIRST".to_string(),
|
|
"GET".to_string(),
|
|
"GRAPHQL".to_string(),
|
|
"HEAR".to_string(),
|
|
"INSERT".to_string(),
|
|
"JOIN".to_string(),
|
|
"LAST".to_string(),
|
|
"LIST".to_string(),
|
|
"LLM".to_string(),
|
|
"MAP".to_string(),
|
|
"MERGE".to_string(),
|
|
"PATCH".to_string(),
|
|
"PIVOT".to_string(),
|
|
"POST".to_string(),
|
|
"PRINT".to_string(),
|
|
"PUT".to_string(),
|
|
"REMEMBER".to_string(),
|
|
"REQUIRE_APPROVAL".to_string(),
|
|
"RUN_BASH".to_string(),
|
|
"RUN_JAVASCRIPT".to_string(),
|
|
"RUN_PYTHON".to_string(),
|
|
"SAVE".to_string(),
|
|
"SEND_MAIL".to_string(),
|
|
"SEND_TEMPLATE".to_string(),
|
|
"SET".to_string(),
|
|
"SET CONTEXT".to_string(),
|
|
"SET SCHEDULE".to_string(),
|
|
"SET USER".to_string(),
|
|
"SIMULATE_IMPACT".to_string(),
|
|
"SMS".to_string(),
|
|
"SOAP".to_string(),
|
|
"TALK".to_string(),
|
|
"UPDATE".to_string(),
|
|
"UPLOAD".to_string(),
|
|
"USE KB".to_string(),
|
|
"USE MODEL".to_string(),
|
|
"USE TOOL".to_string(),
|
|
"USE WEBSITE".to_string(),
|
|
"USE_MCP".to_string(),
|
|
"WAIT".to_string(),
|
|
"WEATHER".to_string(),
|
|
"WEBHOOK".to_string(),
|
|
]
|
|
}
|