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
- 创建 types.ts 定义完整的类型系统 - 重写 DocumentRenderer.tsx 修复语法错误 - 重写 QuizRenderer.tsx 修复语法错误 - 重写 PresentationContainer.tsx 添加类型守卫 - 重写 TypeSwitcher.tsx 修复类型引用 - 更新 index.ts 移除不存在的 ChartRenderer 导出 审计结果: - 类型检查: 通过 - 单元测试: 222 passed - 构建: 成功
430 lines
12 KiB
Rust
430 lines
12 KiB
Rust
//! 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<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>>,
|
|
|
|
/// Orchestration driver (injected from kernel)
|
|
orchestration_driver: Option<Arc<dyn OrchestrationActionDriver>>,
|
|
|
|
/// 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,
|
|
orchestration_driver: 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 orchestration driver
|
|
pub fn with_orchestration_driver(mut self, driver: Arc<dyn OrchestrationActionDriver>) -> 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<String, Value>,
|
|
model: Option<String>,
|
|
temperature: Option<f32>,
|
|
max_tokens: Option<u32>,
|
|
json_mode: bool,
|
|
) -> Result<Value, ActionError> {
|
|
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<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()))
|
|
}
|
|
}
|
|
|
|
/// Execute a skill orchestration
|
|
pub async fn execute_orchestration(
|
|
&self,
|
|
graph_id: Option<&str>,
|
|
graph: Option<&Value>,
|
|
input: HashMap<String, Value>,
|
|
) -> Result<Value, ActionError> {
|
|
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<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>;
|
|
}
|
|
|
|
/// Orchestration action driver trait
|
|
#[async_trait]
|
|
pub trait OrchestrationActionDriver: Send + Sync {
|
|
async fn execute(
|
|
&self,
|
|
graph_id: Option<&str>,
|
|
graph: Option<&Value>,
|
|
input: HashMap<String, Value>,
|
|
) -> Result<Value, String>;
|
|
}
|