feat(pipeline): implement Pipeline DSL system for automated workflows
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Add complete Pipeline DSL system including:
- Rust backend (zclaw-pipeline crate) with parser, executor, and state management
- Frontend components: PipelinesPanel, PipelineResultPreview, ClassroomPreviewer
- Pipeline recommender for Agent conversation integration
- 5 pipeline templates: education, marketing, legal, research, productivity
- Documentation for Pipeline DSL architecture
Pipeline DSL enables declarative workflow definitions with:
- YAML-based configuration
- Expression resolution (${inputs.topic}, ${steps.step1.output})
- LLM integration, parallel execution, file export
- Agent smart recommendations in conversations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
161
crates/zclaw-pipeline/src/actions/export.rs
Normal file
161
crates/zclaw-pipeline/src/actions/export.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
//! File export action
|
||||
|
||||
use std::path::PathBuf;
|
||||
use serde_json::Value;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::types::ExportFormat;
|
||||
use super::ActionError;
|
||||
|
||||
/// Export files in specified formats
|
||||
pub async fn export_files(
|
||||
formats: &[ExportFormat],
|
||||
data: &Value,
|
||||
output_dir: Option<&str>,
|
||||
) -> Result<Value, ActionError> {
|
||||
let dir = output_dir
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| std::env::temp_dir());
|
||||
|
||||
// Ensure directory exists
|
||||
fs::create_dir_all(&dir).await
|
||||
.map_err(|e| ActionError::Export(format!("Failed to create directory: {}", e)))?;
|
||||
|
||||
let mut paths = Vec::new();
|
||||
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
|
||||
|
||||
for format in formats {
|
||||
let filename = format!("output_{}.{}", timestamp, format.extension());
|
||||
let path = dir.join(&filename);
|
||||
|
||||
match format {
|
||||
ExportFormat::Json => {
|
||||
let content = serde_json::to_string_pretty(data)
|
||||
.map_err(|e| ActionError::Export(format!("JSON serialization error: {}", e)))?;
|
||||
fs::write(&path, content).await
|
||||
.map_err(|e| ActionError::Export(format!("Write error: {}", e)))?;
|
||||
}
|
||||
ExportFormat::Markdown => {
|
||||
let content = render_markdown(data);
|
||||
fs::write(&path, content).await
|
||||
.map_err(|e| ActionError::Export(format!("Write error: {}", e)))?;
|
||||
}
|
||||
ExportFormat::Html => {
|
||||
let content = render_html(data);
|
||||
fs::write(&path, content).await
|
||||
.map_err(|e| ActionError::Export(format!("Write error: {}", e)))?;
|
||||
}
|
||||
ExportFormat::Pptx => {
|
||||
// Will integrate with zclaw-kernel export
|
||||
return Err(ActionError::Export("PPTX export requires kernel integration".to_string()));
|
||||
}
|
||||
ExportFormat::Pdf => {
|
||||
return Err(ActionError::Export("PDF export not yet implemented".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
paths.push(serde_json::json!({
|
||||
"format": format.extension(),
|
||||
"path": path.to_string_lossy(),
|
||||
"filename": filename,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(Value::Array(paths))
|
||||
}
|
||||
|
||||
/// Render data to markdown
|
||||
fn render_markdown(data: &Value) -> String {
|
||||
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(description) = data.get("description").and_then(|v| v.as_str()) {
|
||||
md.push_str(&format!("{}\n\n", description));
|
||||
}
|
||||
|
||||
if let Some(outline) = data.get("outline") {
|
||||
md.push_str("## 大纲\n\n");
|
||||
if let Some(items) = outline.get("items").and_then(|v| v.as_array()) {
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if let Some(text) = item.get("title").and_then(|v| v.as_str()) {
|
||||
md.push_str(&format!("{}. {}\n", i + 1, text));
|
||||
}
|
||||
}
|
||||
md.push_str("\n");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(scenes) = data.get("scenes").and_then(|v| v.as_array()) {
|
||||
md.push_str("## 场景\n\n");
|
||||
for scene in scenes {
|
||||
if let Some(title) = scene.get("title").and_then(|v| v.as_str()) {
|
||||
md.push_str(&format!("### {}\n\n", title));
|
||||
}
|
||||
if let Some(content) = scene.get("content").and_then(|v| v.as_str()) {
|
||||
md.push_str(&format!("{}\n\n", content));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
md
|
||||
}
|
||||
|
||||
/// Render data to HTML
|
||||
fn render_html(data: &Value) -> String {
|
||||
let mut html = String::from(r#"<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Export</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
|
||||
h1 { color: #333; }
|
||||
h2 { color: #555; border-bottom: 1px solid #eee; padding-bottom: 10px; }
|
||||
h3 { color: #666; }
|
||||
.scene { margin: 20px 0; padding: 15px; background: #f9f9f9; border-radius: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
"#);
|
||||
|
||||
if let Some(title) = data.get("title").and_then(|v| v.as_str()) {
|
||||
html.push_str(&format!("<h1>{}</h1>", title));
|
||||
}
|
||||
|
||||
if let Some(description) = data.get("description").and_then(|v| v.as_str()) {
|
||||
html.push_str(&format!("<p>{}</p>", description));
|
||||
}
|
||||
|
||||
if let Some(outline) = data.get("outline") {
|
||||
html.push_str("<h2>大纲</h2><ol>");
|
||||
if let Some(items) = outline.get("items").and_then(|v| v.as_array()) {
|
||||
for item in items {
|
||||
if let Some(text) = item.get("title").and_then(|v| v.as_str()) {
|
||||
html.push_str(&format!("<li>{}</li>", text));
|
||||
}
|
||||
}
|
||||
}
|
||||
html.push_str("</ol>");
|
||||
}
|
||||
|
||||
if let Some(scenes) = data.get("scenes").and_then(|v| v.as_array()) {
|
||||
html.push_str("<h2>场景</h2>");
|
||||
for scene in scenes {
|
||||
html.push_str("<div class=\"scene\">");
|
||||
if let Some(title) = scene.get("title").and_then(|v| v.as_str()) {
|
||||
html.push_str(&format!("<h3>{}</h3>", title));
|
||||
}
|
||||
if let Some(content) = scene.get("content").and_then(|v| v.as_str()) {
|
||||
html.push_str(&format!("<p>{}</p>", content));
|
||||
}
|
||||
html.push_str("</div>");
|
||||
}
|
||||
}
|
||||
|
||||
html.push_str("</body></html>");
|
||||
html
|
||||
}
|
||||
21
crates/zclaw-pipeline/src/actions/hand.rs
Normal file
21
crates/zclaw-pipeline/src/actions/hand.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
//! Hand execution action
|
||||
|
||||
use std::collections::HashMap;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::ActionError;
|
||||
|
||||
/// Execute a hand action
|
||||
pub async fn execute_hand(
|
||||
hand_id: &str,
|
||||
action: &str,
|
||||
params: HashMap<String, Value>,
|
||||
) -> Result<Value, ActionError> {
|
||||
// This will be implemented by injecting the hand registry
|
||||
// For now, return an error indicating it needs configuration
|
||||
|
||||
Err(ActionError::Hand(format!(
|
||||
"Hand '{}' action '{}' requires hand registry configuration",
|
||||
hand_id, action
|
||||
)))
|
||||
}
|
||||
61
crates/zclaw-pipeline/src/actions/http.rs
Normal file
61
crates/zclaw-pipeline/src/actions/http.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
//! HTTP request action
|
||||
|
||||
use std::collections::HashMap;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::ActionError;
|
||||
|
||||
/// Execute HTTP request
|
||||
pub async fn http_request(
|
||||
url: &str,
|
||||
method: &str,
|
||||
headers: &HashMap<String, String>,
|
||||
body: Option<&Value>,
|
||||
) -> Result<Value, ActionError> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.map_err(|e| ActionError::Http(e.to_string()))?;
|
||||
|
||||
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),
|
||||
"HEAD" => client.head(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 headers_out: HashMap<String, String> = response.headers()
|
||||
.iter()
|
||||
.filter_map(|(k, v)| Some((k.to_string(), v.to_str().ok()?.to_string())))
|
||||
.collect();
|
||||
|
||||
let body = response.text()
|
||||
.await
|
||||
.map_err(|e| ActionError::Http(e.to_string()))?;
|
||||
|
||||
// Try to parse as JSON, fallback to string
|
||||
let body_value = serde_json::from_str(&body).unwrap_or(Value::String(body));
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"status": status.as_u16(),
|
||||
"status_text": status.canonical_reason().unwrap_or(""),
|
||||
"headers": headers_out,
|
||||
"body": body_value,
|
||||
}))
|
||||
}
|
||||
28
crates/zclaw-pipeline/src/actions/llm.rs
Normal file
28
crates/zclaw-pipeline/src/actions/llm.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
//! LLM generation action
|
||||
|
||||
use std::collections::HashMap;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::ActionError;
|
||||
|
||||
/// Execute LLM generation
|
||||
pub async fn execute_llm_generation(
|
||||
driver: &dyn super::LlmActionDriver,
|
||||
template: &str,
|
||||
input: HashMap<String, Value>,
|
||||
model: Option<String>,
|
||||
temperature: Option<f32>,
|
||||
max_tokens: Option<u32>,
|
||||
json_mode: bool,
|
||||
) -> Result<Value, ActionError> {
|
||||
driver.generate(
|
||||
template.to_string(),
|
||||
input,
|
||||
model,
|
||||
temperature,
|
||||
max_tokens,
|
||||
json_mode,
|
||||
)
|
||||
.await
|
||||
.map_err(ActionError::Llm)
|
||||
}
|
||||
379
crates/zclaw-pipeline/src/actions/mod.rs
Normal file
379
crates/zclaw-pipeline/src/actions/mod.rs
Normal file
@@ -0,0 +1,379 @@
|
||||
//! 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;
|
||||
|
||||
pub use llm::*;
|
||||
pub use parallel::*;
|
||||
pub use render::*;
|
||||
pub use export::*;
|
||||
pub use http::*;
|
||||
pub use skill::*;
|
||||
pub use hand::*;
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
/// Action registry - holds references to all action executors
|
||||
pub struct ActionRegistry {
|
||||
/// LLM driver (injected from runtime)
|
||||
llm_driver: Option<Arc<dyn LlmActionDriver>>,
|
||||
|
||||
/// Skill registry (injected from kernel)
|
||||
skill_registry: Option<Arc<dyn SkillActionDriver>>,
|
||||
|
||||
/// Hand registry (injected from kernel)
|
||||
hand_registry: Option<Arc<dyn HandActionDriver>>,
|
||||
|
||||
/// Template directory
|
||||
template_dir: Option<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
impl ActionRegistry {
|
||||
/// Create a new action registry
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
llm_driver: None,
|
||||
skill_registry: None,
|
||||
hand_registry: None,
|
||||
template_dir: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set LLM driver
|
||||
pub fn with_llm_driver(mut self, driver: Arc<dyn LlmActionDriver>) -> Self {
|
||||
self.llm_driver = Some(driver);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set skill registry
|
||||
pub fn with_skill_registry(mut self, registry: Arc<dyn SkillActionDriver>) -> Self {
|
||||
self.skill_registry = Some(registry);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set hand registry
|
||||
pub fn with_hand_registry(mut self, registry: Arc<dyn HandActionDriver>) -> Self {
|
||||
self.hand_registry = Some(registry);
|
||||
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<String, Value>,
|
||||
model: Option<String>,
|
||||
temperature: Option<f32>,
|
||||
max_tokens: Option<u32>,
|
||||
json_mode: bool,
|
||||
) -> Result<Value, ActionError> {
|
||||
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()
|
||||
};
|
||||
|
||||
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<String, Value>,
|
||||
) -> Result<Value, ActionError> {
|
||||
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<String, Value>,
|
||||
) -> Result<Value, ActionError> {
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Render classroom
|
||||
pub async fn render_classroom(&self, data: &Value) -> Result<Value, ActionError> {
|
||||
// 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<Value, ActionError> {
|
||||
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<String, ActionError> {
|
||||
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<String, String>,
|
||||
body: Option<&Value>,
|
||||
) -> Result<Value, ActionError> {
|
||||
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<String, ActionError> {
|
||||
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<String, ActionError> {
|
||||
// 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<String, ActionError> {
|
||||
let mut html = String::from("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Export</title></head><body>");
|
||||
|
||||
if let Some(title) = data.get("title").and_then(|v| v.as_str()) {
|
||||
html.push_str(&format!("<h1>{}</h1>", title));
|
||||
}
|
||||
|
||||
if let Some(items) = data.get("items").and_then(|v| v.as_array()) {
|
||||
html.push_str("<ul>");
|
||||
for item in items {
|
||||
if let Some(text) = item.as_str() {
|
||||
html.push_str(&format!("<li>{}</li>", text));
|
||||
}
|
||||
}
|
||||
html.push_str("</ul>");
|
||||
}
|
||||
|
||||
html.push_str("</body></html>");
|
||||
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<String, Value>,
|
||||
model: Option<String>,
|
||||
temperature: Option<f32>,
|
||||
max_tokens: Option<u32>,
|
||||
json_mode: bool,
|
||||
) -> Result<Value, String>;
|
||||
}
|
||||
|
||||
/// Skill action driver trait
|
||||
#[async_trait]
|
||||
pub trait SkillActionDriver: Send + Sync {
|
||||
async fn execute(
|
||||
&self,
|
||||
skill_id: &str,
|
||||
input: HashMap<String, Value>,
|
||||
) -> Result<Value, String>;
|
||||
}
|
||||
|
||||
/// Hand action driver trait
|
||||
#[async_trait]
|
||||
pub trait HandActionDriver: Send + Sync {
|
||||
async fn execute(
|
||||
&self,
|
||||
hand_id: &str,
|
||||
action: &str,
|
||||
params: HashMap<String, Value>,
|
||||
) -> Result<Value, String>;
|
||||
}
|
||||
33
crates/zclaw-pipeline/src/actions/parallel.rs
Normal file
33
crates/zclaw-pipeline/src/actions/parallel.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
//! Parallel execution action
|
||||
|
||||
use futures::stream::{self, StreamExt};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::ActionError;
|
||||
|
||||
/// Execute steps in parallel
|
||||
pub async fn execute_parallel<F, Fut>(
|
||||
items: &[Value],
|
||||
max_workers: usize,
|
||||
executor: F,
|
||||
) -> Result<Vec<Value>, ActionError>
|
||||
where
|
||||
F: Fn(Value, usize) -> Fut,
|
||||
Fut: std::future::Future<Output = Result<Value, ActionError>>,
|
||||
{
|
||||
let results: Vec<Result<Value, ActionError>> = stream::iter(items.iter().enumerate())
|
||||
.map(|(index, item)| {
|
||||
let item = item.clone();
|
||||
executor(item, index)
|
||||
})
|
||||
.buffer_unordered(max_workers)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let mut outputs = Vec::new();
|
||||
for result in results {
|
||||
outputs.push(result?);
|
||||
}
|
||||
|
||||
Ok(outputs)
|
||||
}
|
||||
32
crates/zclaw-pipeline/src/actions/render.rs
Normal file
32
crates/zclaw-pipeline/src/actions/render.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
//! Classroom render action
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use super::ActionError;
|
||||
|
||||
/// Render classroom data
|
||||
pub async fn render_classroom(data: &Value) -> Result<Value, ActionError> {
|
||||
// This will integrate with the classroom renderer
|
||||
// For now, validate and pass through
|
||||
|
||||
let title = data.get("title")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| ActionError::Render("Missing 'title' field".to_string()))?;
|
||||
|
||||
let outline = data.get("outline")
|
||||
.ok_or_else(|| ActionError::Render("Missing 'outline' field".to_string()))?;
|
||||
|
||||
let scenes = data.get("scenes")
|
||||
.ok_or_else(|| ActionError::Render("Missing 'scenes' field".to_string()))?;
|
||||
|
||||
// Generate classroom ID
|
||||
let classroom_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"id": classroom_id,
|
||||
"title": title,
|
||||
"outline": outline,
|
||||
"scenes": scenes,
|
||||
"preview_url": format!("/classroom/{}", classroom_id),
|
||||
}))
|
||||
}
|
||||
20
crates/zclaw-pipeline/src/actions/skill.rs
Normal file
20
crates/zclaw-pipeline/src/actions/skill.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
//! Skill execution action
|
||||
|
||||
use std::collections::HashMap;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::ActionError;
|
||||
|
||||
/// Execute a skill by ID
|
||||
pub async fn execute_skill(
|
||||
skill_id: &str,
|
||||
input: HashMap<String, Value>,
|
||||
) -> Result<Value, ActionError> {
|
||||
// This will be implemented by injecting the skill registry
|
||||
// For now, return an error indicating it needs configuration
|
||||
|
||||
Err(ActionError::Skill(format!(
|
||||
"Skill '{}' execution requires skill registry configuration",
|
||||
skill_id
|
||||
)))
|
||||
}
|
||||
Reference in New Issue
Block a user