//! Pipeline actions module //! //! Built-in actions that can be used in pipelines. mod llm; mod parallel; mod render; mod export; mod http; mod skill; mod hand; mod orchestration; pub use llm::*; pub use parallel::*; pub use render::*; pub use export::*; pub use http::*; pub use skill::*; pub use hand::*; pub use orchestration::*; use std::collections::HashMap; use std::sync::Arc; use serde_json::Value; use async_trait::async_trait; use crate::types::ExportFormat; /// Action execution error #[derive(Debug, thiserror::Error)] pub enum ActionError { #[error("LLM error: {0}")] Llm(String), #[error("Skill error: {0}")] Skill(String), #[error("Hand error: {0}")] Hand(String), #[error("Render error: {0}")] Render(String), #[error("Export error: {0}")] Export(String), #[error("HTTP error: {0}")] Http(String), #[error("IO error: {0}")] Io(#[from] std::io::Error), #[error("JSON error: {0}")] Json(#[from] serde_json::Error), #[error("Template not found: {0}")] TemplateNotFound(String), #[error("Invalid input: {0}")] InvalidInput(String), #[error("Orchestration error: {0}")] Orchestration(String), } /// Action registry - holds references to all action executors pub struct ActionRegistry { /// LLM driver (injected from runtime) llm_driver: Option>, /// Skill registry (injected from kernel) skill_registry: Option>, /// Hand registry (injected from kernel) hand_registry: Option>, /// Orchestration driver (injected from kernel) orchestration_driver: Option>, /// Template directory template_dir: Option, } impl ActionRegistry { /// Create a new action registry pub fn new() -> Self { Self { llm_driver: None, skill_registry: None, hand_registry: None, orchestration_driver: None, template_dir: None, } } /// Set LLM driver pub fn with_llm_driver(mut self, driver: Arc) -> Self { self.llm_driver = Some(driver); self } /// Set skill registry pub fn with_skill_registry(mut self, registry: Arc) -> Self { self.skill_registry = Some(registry); self } /// Set hand registry pub fn with_hand_registry(mut self, registry: Arc) -> Self { self.hand_registry = Some(registry); self } /// Set orchestration driver pub fn with_orchestration_driver(mut self, driver: Arc) -> Self { self.orchestration_driver = Some(driver); self } /// Set template directory pub fn with_template_dir(mut self, dir: std::path::PathBuf) -> Self { self.template_dir = Some(dir); self } /// Execute LLM generation pub async fn execute_llm( &self, template: &str, input: HashMap, model: Option, temperature: Option, max_tokens: Option, json_mode: bool, ) -> Result { println!("[DEBUG execute_llm] Called with template length: {}", template.len()); println!("[DEBUG execute_llm] Input HashMap contents:"); for (k, v) in &input { println!(" {} => {:?}", k, v); } if let Some(driver) = &self.llm_driver { // Load template if it's a file path let prompt = if template.ends_with(".md") || template.contains('/') { self.load_template(template)? } else { template.to_string() }; println!("[DEBUG execute_llm] Calling driver.generate with prompt length: {}", prompt.len()); driver.generate(prompt, input, model, temperature, max_tokens, json_mode) .await .map_err(ActionError::Llm) } else { Err(ActionError::Llm("LLM driver not configured".to_string())) } } /// Execute a skill pub async fn execute_skill( &self, skill_id: &str, input: HashMap, ) -> Result { if let Some(registry) = &self.skill_registry { registry.execute(skill_id, input) .await .map_err(ActionError::Skill) } else { Err(ActionError::Skill("Skill registry not configured".to_string())) } } /// Execute a hand action pub async fn execute_hand( &self, hand_id: &str, action: &str, params: HashMap, ) -> Result { if let Some(registry) = &self.hand_registry { registry.execute(hand_id, action, params) .await .map_err(ActionError::Hand) } else { Err(ActionError::Hand("Hand registry not configured".to_string())) } } /// Execute a skill orchestration pub async fn execute_orchestration( &self, graph_id: Option<&str>, graph: Option<&Value>, input: HashMap, ) -> Result { if let Some(driver) = &self.orchestration_driver { driver.execute(graph_id, graph, input) .await .map_err(ActionError::Orchestration) } else { Err(ActionError::Orchestration("Orchestration driver not configured".to_string())) } } /// Render classroom pub async fn render_classroom(&self, data: &Value) -> Result { // This will integrate with the classroom renderer // For now, return the data as-is Ok(data.clone()) } /// Export files pub async fn export_files( &self, formats: &[ExportFormat], data: &Value, output_dir: Option<&str>, ) -> Result { let mut paths = Vec::new(); let dir = output_dir .map(std::path::PathBuf::from) .unwrap_or_else(|| std::env::temp_dir()); for format in formats { let path = self.export_single(format, data, &dir).await?; paths.push(path); } Ok(serde_json::to_value(paths).unwrap_or(Value::Null)) } async fn export_single( &self, format: &ExportFormat, data: &Value, dir: &std::path::Path, ) -> Result { let filename = format!("output_{}.{}", chrono::Utc::now().format("%Y%m%d_%H%M%S"), format.extension()); let path = dir.join(&filename); match format { ExportFormat::Json => { let content = serde_json::to_string_pretty(data)?; tokio::fs::write(&path, content).await?; } ExportFormat::Markdown => { let content = self.render_markdown(data)?; tokio::fs::write(&path, content).await?; } ExportFormat::Html => { let content = self.render_html(data)?; tokio::fs::write(&path, content).await?; } ExportFormat::Pptx => { // Will integrate with pptx exporter return Err(ActionError::Export("PPTX export not yet implemented".to_string())); } ExportFormat::Pdf => { return Err(ActionError::Export("PDF export not yet implemented".to_string())); } } Ok(path.to_string_lossy().to_string()) } /// Make HTTP request pub async fn http_request( &self, url: &str, method: &str, headers: &HashMap, body: Option<&Value>, ) -> Result { let client = reqwest::Client::new(); let mut request = match method.to_uppercase().as_str() { "GET" => client.get(url), "POST" => client.post(url), "PUT" => client.put(url), "DELETE" => client.delete(url), "PATCH" => client.patch(url), _ => return Err(ActionError::Http(format!("Unsupported HTTP method: {}", method))), }; for (key, value) in headers { request = request.header(key, value); } if let Some(body) = body { request = request.json(body); } let response = request.send() .await .map_err(|e| ActionError::Http(e.to_string()))?; let status = response.status(); let body = response.text() .await .map_err(|e| ActionError::Http(e.to_string()))?; Ok(serde_json::json!({ "status": status.as_u16(), "body": body, })) } /// Load a template file fn load_template(&self, path: &str) -> Result { let template_path = if let Some(dir) = &self.template_dir { dir.join(path) } else { std::path::PathBuf::from(path) }; std::fs::read_to_string(&template_path) .map_err(|_| ActionError::TemplateNotFound(path.to_string())) } /// Render data to markdown fn render_markdown(&self, data: &Value) -> Result { // Simple markdown rendering let mut md = String::new(); if let Some(title) = data.get("title").and_then(|v| v.as_str()) { md.push_str(&format!("# {}\n\n", title)); } if let Some(items) = data.get("items").and_then(|v| v.as_array()) { for item in items { if let Some(text) = item.as_str() { md.push_str(&format!("- {}\n", text)); } } } Ok(md) } /// Render data to HTML fn render_html(&self, data: &Value) -> Result { let mut html = String::from("Export"); if let Some(title) = data.get("title").and_then(|v| v.as_str()) { html.push_str(&format!("

{}

", title)); } if let Some(items) = data.get("items").and_then(|v| v.as_array()) { html.push_str("
    "); for item in items { if let Some(text) = item.as_str() { html.push_str(&format!("
  • {}
  • ", text)); } } html.push_str("
"); } html.push_str(""); Ok(html) } } impl ExportFormat { fn extension(&self) -> &'static str { match self { ExportFormat::Pptx => "pptx", ExportFormat::Html => "html", ExportFormat::Pdf => "pdf", ExportFormat::Markdown => "md", ExportFormat::Json => "json", } } } impl Default for ActionRegistry { fn default() -> Self { Self::new() } } /// LLM action driver trait #[async_trait] pub trait LlmActionDriver: Send + Sync { async fn generate( &self, prompt: String, input: HashMap, model: Option, temperature: Option, max_tokens: Option, json_mode: bool, ) -> Result; } /// Skill action driver trait #[async_trait] pub trait SkillActionDriver: Send + Sync { async fn execute( &self, skill_id: &str, input: HashMap, ) -> Result; } /// Hand action driver trait #[async_trait] pub trait HandActionDriver: Send + Sync { async fn execute( &self, hand_id: &str, action: &str, params: HashMap, ) -> Result; } /// Orchestration action driver trait #[async_trait] pub trait OrchestrationActionDriver: Send + Sync { async fn execute( &self, graph_id: Option<&str>, graph: Option<&Value>, input: HashMap, ) -> Result; }