2025-10-18 18:20:02 -03:00
|
|
|
use crate::shared::state::AppState;
|
2025-11-02 20:57:53 -03:00
|
|
|
use crate::basic::keywords::set_schedule::execute_set_schedule;
|
2025-10-18 18:20:02 -03:00
|
|
|
use log::{debug, info, warn};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::error::Error;
|
|
|
|
|
use std::fs;
|
|
|
|
|
use std::path::Path;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Represents a PARAM declaration in BASIC
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ParamDeclaration {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub param_type: String,
|
|
|
|
|
pub example: Option<String>,
|
|
|
|
|
pub description: String,
|
|
|
|
|
pub required: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Represents a BASIC tool definition
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ToolDefinition {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub description: String,
|
|
|
|
|
pub parameters: Vec<ParamDeclaration>,
|
|
|
|
|
pub source_file: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// MCP tool format (Model Context Protocol)
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct MCPTool {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub description: String,
|
|
|
|
|
pub input_schema: MCPInputSchema,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct MCPInputSchema {
|
|
|
|
|
#[serde(rename = "type")]
|
|
|
|
|
pub schema_type: String,
|
|
|
|
|
pub properties: HashMap<String, MCPProperty>,
|
|
|
|
|
pub required: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct MCPProperty {
|
|
|
|
|
#[serde(rename = "type")]
|
|
|
|
|
pub prop_type: String,
|
|
|
|
|
pub description: String,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub example: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// OpenAI tool format
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct OpenAITool {
|
|
|
|
|
#[serde(rename = "type")]
|
|
|
|
|
pub tool_type: String,
|
|
|
|
|
pub function: OpenAIFunction,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct OpenAIFunction {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub description: String,
|
|
|
|
|
pub parameters: OpenAIParameters,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct OpenAIParameters {
|
|
|
|
|
#[serde(rename = "type")]
|
|
|
|
|
pub param_type: String,
|
|
|
|
|
pub properties: HashMap<String, OpenAIProperty>,
|
|
|
|
|
pub required: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct OpenAIProperty {
|
|
|
|
|
#[serde(rename = "type")]
|
|
|
|
|
pub prop_type: String,
|
|
|
|
|
pub description: String,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub example: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// BASIC Compiler
|
|
|
|
|
pub struct BasicCompiler {
|
|
|
|
|
state: Arc<AppState>,
|
2025-11-02 20:57:53 -03:00
|
|
|
bot_id: uuid::Uuid,
|
2025-10-18 18:20:02 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl BasicCompiler {
|
2025-11-02 20:57:53 -03:00
|
|
|
pub fn new(state: Arc<AppState>, bot_id: uuid::Uuid) -> Self {
|
|
|
|
|
Self { state, bot_id }
|
2025-10-18 18:20:02 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Compile a BASIC file to AST and generate tool definitions
|
|
|
|
|
pub fn compile_file(
|
|
|
|
|
&self,
|
|
|
|
|
source_path: &str,
|
|
|
|
|
output_dir: &str,
|
|
|
|
|
) -> Result<CompilationResult, Box<dyn Error + Send + Sync>> {
|
|
|
|
|
info!("Compiling BASIC file: {}", source_path);
|
|
|
|
|
|
|
|
|
|
// Read source file
|
|
|
|
|
let source_content = fs::read_to_string(source_path)
|
|
|
|
|
.map_err(|e| format!("Failed to read source file: {}", e))?;
|
|
|
|
|
|
|
|
|
|
// Parse tool definition from source
|
|
|
|
|
let tool_def = self.parse_tool_definition(&source_content, source_path)?;
|
|
|
|
|
|
|
|
|
|
// Extract base name without extension
|
|
|
|
|
let file_name = Path::new(source_path)
|
|
|
|
|
.file_stem()
|
|
|
|
|
.and_then(|s| s.to_str())
|
|
|
|
|
.ok_or("Invalid file name")?;
|
|
|
|
|
|
|
|
|
|
// Generate AST path
|
|
|
|
|
let ast_path = format!("{}/{}.ast", output_dir, file_name);
|
|
|
|
|
|
|
|
|
|
// Generate AST (using Rhai compilation would happen here)
|
|
|
|
|
// For now, we'll store the preprocessed script
|
2025-11-02 20:57:53 -03:00
|
|
|
let ast_content = self.preprocess_basic(&source_content, source_path, self.bot_id)?;
|
2025-10-18 18:20:02 -03:00
|
|
|
fs::write(&ast_path, &ast_content)
|
|
|
|
|
.map_err(|e| format!("Failed to write AST file: {}", e))?;
|
|
|
|
|
|
|
|
|
|
info!("AST generated: {}", ast_path);
|
|
|
|
|
|
|
|
|
|
// Generate tool definitions if PARAM and DESCRIPTION found
|
|
|
|
|
let (mcp_json, tool_json) = if !tool_def.parameters.is_empty() {
|
|
|
|
|
let mcp = self.generate_mcp_tool(&tool_def)?;
|
|
|
|
|
let openai = self.generate_openai_tool(&tool_def)?;
|
|
|
|
|
|
|
|
|
|
let mcp_path = format!("{}/{}.mcp.json", output_dir, file_name);
|
|
|
|
|
let tool_path = format!("{}/{}.tool.json", output_dir, file_name);
|
|
|
|
|
|
|
|
|
|
// Write MCP JSON
|
|
|
|
|
let mcp_json_str = serde_json::to_string_pretty(&mcp)?;
|
|
|
|
|
fs::write(&mcp_path, mcp_json_str)
|
|
|
|
|
.map_err(|e| format!("Failed to write MCP JSON: {}", e))?;
|
|
|
|
|
|
|
|
|
|
// Write OpenAI tool JSON
|
|
|
|
|
let tool_json_str = serde_json::to_string_pretty(&openai)?;
|
|
|
|
|
fs::write(&tool_path, tool_json_str)
|
|
|
|
|
.map_err(|e| format!("Failed to write tool JSON: {}", e))?;
|
|
|
|
|
|
|
|
|
|
info!("Tool definitions generated: {} and {}", mcp_path, tool_path);
|
|
|
|
|
|
|
|
|
|
(Some(mcp), Some(openai))
|
|
|
|
|
} else {
|
|
|
|
|
debug!("No tool parameters found in {}", source_path);
|
|
|
|
|
(None, None)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(CompilationResult {
|
|
|
|
|
mcp_tool: mcp_json,
|
|
|
|
|
openai_tool: tool_json,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Parse tool definition from BASIC source
|
2025-11-04 23:11:33 -03:00
|
|
|
pub fn parse_tool_definition(
|
2025-10-18 18:20:02 -03:00
|
|
|
&self,
|
|
|
|
|
source: &str,
|
|
|
|
|
source_path: &str,
|
|
|
|
|
) -> Result<ToolDefinition, Box<dyn Error + Send + Sync>> {
|
|
|
|
|
let mut params = Vec::new();
|
|
|
|
|
let mut description = String::new();
|
|
|
|
|
|
|
|
|
|
let lines: Vec<&str> = source.lines().collect();
|
|
|
|
|
let mut i = 0;
|
|
|
|
|
|
|
|
|
|
while i < lines.len() {
|
|
|
|
|
let line = lines[i].trim();
|
|
|
|
|
|
|
|
|
|
// Parse PARAM declarations
|
|
|
|
|
if line.starts_with("PARAM ") {
|
|
|
|
|
if let Some(param) = self.parse_param_line(line)? {
|
|
|
|
|
params.push(param);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse DESCRIPTION
|
|
|
|
|
if line.starts_with("DESCRIPTION ") {
|
|
|
|
|
let desc_start = line.find('"').unwrap_or(0);
|
|
|
|
|
let desc_end = line.rfind('"').unwrap_or(line.len());
|
|
|
|
|
if desc_start < desc_end {
|
|
|
|
|
description = line[desc_start + 1..desc_end].to_string();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let tool_name = Path::new(source_path)
|
|
|
|
|
.file_stem()
|
|
|
|
|
.and_then(|s| s.to_str())
|
|
|
|
|
.unwrap_or("unknown")
|
|
|
|
|
.to_string();
|
|
|
|
|
|
|
|
|
|
Ok(ToolDefinition {
|
|
|
|
|
name: tool_name,
|
|
|
|
|
description,
|
|
|
|
|
parameters: params,
|
|
|
|
|
source_file: source_path.to_string(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Parse a PARAM line
|
|
|
|
|
/// Format: PARAM name AS type LIKE "example" DESCRIPTION "description"
|
|
|
|
|
fn parse_param_line(
|
|
|
|
|
&self,
|
|
|
|
|
line: &str,
|
|
|
|
|
) -> Result<Option<ParamDeclaration>, Box<dyn Error + Send + Sync>> {
|
|
|
|
|
let line = line.trim();
|
|
|
|
|
if !line.starts_with("PARAM ") {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract parts
|
|
|
|
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
|
|
|
if parts.len() < 4 {
|
|
|
|
|
warn!("Invalid PARAM line: {}", line);
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let name = parts[1].to_string();
|
|
|
|
|
|
|
|
|
|
// Find AS keyword
|
|
|
|
|
let as_index = parts.iter().position(|&p| p == "AS");
|
|
|
|
|
let param_type = if let Some(idx) = as_index {
|
|
|
|
|
if idx + 1 < parts.len() {
|
|
|
|
|
parts[idx + 1].to_lowercase()
|
|
|
|
|
} else {
|
|
|
|
|
"string".to_string()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
"string".to_string()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Extract LIKE value (example)
|
|
|
|
|
let example = if let Some(like_pos) = line.find("LIKE") {
|
|
|
|
|
let rest = &line[like_pos + 4..].trim();
|
|
|
|
|
if let Some(start) = rest.find('"') {
|
|
|
|
|
if let Some(end) = rest[start + 1..].find('"') {
|
|
|
|
|
Some(rest[start + 1..start + 1 + end].to_string())
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Extract DESCRIPTION
|
|
|
|
|
let description = if let Some(desc_pos) = line.find("DESCRIPTION") {
|
|
|
|
|
let rest = &line[desc_pos + 11..].trim();
|
|
|
|
|
if let Some(start) = rest.find('"') {
|
|
|
|
|
if let Some(end) = rest[start + 1..].rfind('"') {
|
|
|
|
|
rest[start + 1..start + 1 + end].to_string()
|
|
|
|
|
} else {
|
|
|
|
|
"".to_string()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
"".to_string()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
"".to_string()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(Some(ParamDeclaration {
|
|
|
|
|
name,
|
|
|
|
|
param_type: self.normalize_type(¶m_type),
|
|
|
|
|
example,
|
|
|
|
|
description,
|
|
|
|
|
required: true, // Default to required
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Normalize BASIC types to JSON schema types
|
|
|
|
|
fn normalize_type(&self, basic_type: &str) -> String {
|
|
|
|
|
match basic_type.to_lowercase().as_str() {
|
|
|
|
|
"string" | "text" => "string".to_string(),
|
|
|
|
|
"integer" | "int" | "number" => "integer".to_string(),
|
|
|
|
|
"float" | "double" | "decimal" => "number".to_string(),
|
|
|
|
|
"boolean" | "bool" => "boolean".to_string(),
|
|
|
|
|
"date" | "datetime" => "string".to_string(), // Dates as strings
|
|
|
|
|
"array" | "list" => "array".to_string(),
|
|
|
|
|
"object" | "map" => "object".to_string(),
|
|
|
|
|
_ => "string".to_string(), // Default to string
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generate MCP tool format
|
|
|
|
|
fn generate_mcp_tool(
|
|
|
|
|
&self,
|
|
|
|
|
tool_def: &ToolDefinition,
|
|
|
|
|
) -> Result<MCPTool, Box<dyn Error + Send + Sync>> {
|
|
|
|
|
let mut properties = HashMap::new();
|
|
|
|
|
let mut required = Vec::new();
|
|
|
|
|
|
|
|
|
|
for param in &tool_def.parameters {
|
|
|
|
|
properties.insert(
|
|
|
|
|
param.name.clone(),
|
|
|
|
|
MCPProperty {
|
|
|
|
|
prop_type: param.param_type.clone(),
|
|
|
|
|
description: param.description.clone(),
|
|
|
|
|
example: param.example.clone(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if param.required {
|
|
|
|
|
required.push(param.name.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(MCPTool {
|
|
|
|
|
name: tool_def.name.clone(),
|
|
|
|
|
description: tool_def.description.clone(),
|
|
|
|
|
input_schema: MCPInputSchema {
|
|
|
|
|
schema_type: "object".to_string(),
|
|
|
|
|
properties,
|
|
|
|
|
required,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generate OpenAI tool format
|
|
|
|
|
fn generate_openai_tool(
|
|
|
|
|
&self,
|
|
|
|
|
tool_def: &ToolDefinition,
|
|
|
|
|
) -> Result<OpenAITool, Box<dyn Error + Send + Sync>> {
|
|
|
|
|
let mut properties = HashMap::new();
|
|
|
|
|
let mut required = Vec::new();
|
|
|
|
|
|
|
|
|
|
for param in &tool_def.parameters {
|
|
|
|
|
properties.insert(
|
|
|
|
|
param.name.clone(),
|
|
|
|
|
OpenAIProperty {
|
|
|
|
|
prop_type: param.param_type.clone(),
|
|
|
|
|
description: param.description.clone(),
|
|
|
|
|
example: param.example.clone(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if param.required {
|
|
|
|
|
required.push(param.name.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(OpenAITool {
|
|
|
|
|
tool_type: "function".to_string(),
|
|
|
|
|
function: OpenAIFunction {
|
|
|
|
|
name: tool_def.name.clone(),
|
|
|
|
|
description: tool_def.description.clone(),
|
|
|
|
|
parameters: OpenAIParameters {
|
|
|
|
|
param_type: "object".to_string(),
|
|
|
|
|
properties,
|
|
|
|
|
required,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Preprocess BASIC script (basic transformations)
|
2025-11-02 20:57:53 -03:00
|
|
|
fn preprocess_basic(&self, source: &str, source_path: &str, bot_id: uuid::Uuid) -> Result<String, Box<dyn Error + Send + Sync>> {
|
2025-10-18 18:20:02 -03:00
|
|
|
let mut result = String::new();
|
|
|
|
|
|
|
|
|
|
for line in source.lines() {
|
|
|
|
|
let trimmed = line.trim();
|
|
|
|
|
|
|
|
|
|
// Skip empty lines and comments
|
|
|
|
|
if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("REM") {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-02 20:57:53 -03:00
|
|
|
// Handle SET_SCHEDULE keyword during preprocessing
|
|
|
|
|
if trimmed.starts_with("SET_SCHEDULE") {
|
|
|
|
|
// Expected format: SET_SCHEDULE "cron_expression"
|
|
|
|
|
// Extract the quoted cron expression
|
|
|
|
|
let parts: Vec<&str> = trimmed.split('"').collect();
|
|
|
|
|
if parts.len() >= 3 {
|
|
|
|
|
let cron = parts[1];
|
|
|
|
|
|
|
|
|
|
// Get the current script's name (file stem)
|
|
|
|
|
use std::path::Path;
|
|
|
|
|
let script_name = Path::new(source_path)
|
|
|
|
|
.file_stem()
|
|
|
|
|
.and_then(|s| s.to_str())
|
|
|
|
|
.unwrap_or("unknown")
|
|
|
|
|
.to_string();
|
|
|
|
|
|
|
|
|
|
let mut conn = self.state.conn.lock().unwrap();
|
|
|
|
|
if let Err(e) = execute_set_schedule(&mut *conn, cron, &script_name, bot_id) {
|
|
|
|
|
log::error!("Failed to schedule SET_SCHEDULE during preprocessing: {}", e);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
log::warn!("Malformed SET_SCHEDULE line ignored: {}", trimmed);
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-18 18:20:02 -03:00
|
|
|
// Skip PARAM and DESCRIPTION lines (metadata)
|
|
|
|
|
if trimmed.starts_with("PARAM ") || trimmed.starts_with("DESCRIPTION ") {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.push_str(trimmed);
|
|
|
|
|
result.push('\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(result)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Result of compilation
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct CompilationResult {
|
|
|
|
|
pub mcp_tool: Option<MCPTool>,
|
|
|
|
|
pub openai_tool: Option<OpenAITool>,
|
|
|
|
|
}
|