1use anyhow::{Result, bail};
12use async_trait::async_trait;
13use serde_json::{Value, json};
14
15use crate::tools::{Tool, ToolDefinition};
16use crate::workflow::{
17 EdgeDef, InputDef, NodeDef, NodeType, OutputDef,
18 parser::{parse_workflow, parse_workflow_from_file, to_yaml},
19};
20use std::fs;
21use std::path::PathBuf;
22
23fn get_workflow_dir(location: &str) -> Result<PathBuf> {
25 match location {
26 "project" => {
27 let cwd = std::env::current_dir()
28 .map_err(|e| anyhow::anyhow!("Failed to get current directory: {}", e))?;
29 Ok(cwd.join(".matrix").join("workflows"))
30 }
31 "user" => dirs::home_dir()
32 .ok_or_else(|| anyhow::anyhow!("Failed to get home directory"))
33 .map(|p| p.join(".matrix").join("workflows")),
34 _ => bail!("Invalid location: {}. Use 'project' or 'user'", location),
35 }
36}
37
38pub struct WorkflowCreateTool;
40
41#[async_trait]
42impl Tool for WorkflowCreateTool {
43 fn definition(&self) -> ToolDefinition {
44 ToolDefinition {
45 name: "workflow_create".to_string(),
46 description: "创建和编辑 workflow YAML 文件,支持迭代修改。
47
48【核心功能】
49此工具是 workflow 制作的核心工具,支持完整的创建-编辑-验证循环。
50
51【适用场景】
52- 创建新的 workflow
53- 修改现有 workflow(增删改节点、调整连接、修改参数)
54- 获取预定义模板作为起点
55- 验证 workflow 结构合法性
56- **多次迭代调整**:用户可能反复修改直到满意
57
58【功能模式】
59- **create**: 创建新 workflow(首次创建)
60- **edit**: 编辑现有 workflow(精确修改节点、边、参数)
61- **template**: 获取模板(快速开始)
62- **validate**: 验证结构(检查错误)
63- **info**: 查看 workflow 详情(查看节点、边、参数)
64
65【迭代修改流程】
66典型使用流程:
671. template → 获取模板作为起点
682. create → 创建初始版本
693. edit → 多次调整(修改节点名称、添加节点、调整连接)
704. validate → 验证最终版本
71
72【edit 模式操作类型】
73- add_node: 添加节点
74- remove_node: 删除节点
75- update_node: 更新节点属性(name, task, params, on_failure)
76- add_edge: 添加连接
77- remove_edge: 删除连接
78- add_input: 添加输入参数
79- remove_input: 删除输入参数
80- update_input: 更新输入参数
81- add_output: 添加输出参数
82- remove_output: 删除输出参数
83- update_output: 更新输出参数
84- update_metadata: 更新元数据(name, description, version)
85
86【参数说明】
87- mode: 操作模式 (create/edit/template/validate/info)
88- workflow_id: 目标 workflow ID (edit/info 模式必需)
89- workflow: workflow 定义 (create 模式必需)
90- yaml_content: YAML 内容字符串 (可选,用于直接提供 YAML)
91- edit_operation: 编辑操作类型 (edit 模式必需)
92- edit_target: 编辑目标 (节点ID、边ID等)
93- edit_value: 新值 (更新操作必需)
94- location: 保存位置 (project/user,默认 project)
95- template_type: 模板类型 (template 模式使用)
96
97【使用示例】
98创建 workflow:
99{
100 \"mode\": \"create\",
101 \"workflow\": {\"id\": \"my-workflow\", \"name\": \"My Workflow\", ...}
102}
103
104添加节点:
105{
106 \"mode\": \"edit\",
107 \"workflow_id\": \"my-workflow\",
108 \"edit_operation\": \"add_node\",
109 \"edit_value\": {\"id\": \"new_task\", \"type\": \"task\", \"name\": \"New Task\", \"task\": \"process\"}
110}
111
112修改节点名称:
113{
114 \"mode\": \"edit\",
115 \"workflow_id\": \"my-workflow\",
116 \"edit_operation\": \"update_node\",
117 \"edit_target\": \"task1\",
118 \"edit_value\": {\"name\": \"Updated Task Name\"}
119}
120
121添加连接:
122{
123 \"mode\": \"edit\",
124 \"workflow_id\": \"my-workflow\",
125 \"edit_operation\": \"add_edge\",
126 \"edit_value\": {\"from\": \"task1\", \"to\": \"new_task\"}
127}
128
129查看详情:
130{
131 \"mode\": \"info\",
132 \"workflow_id\": \"my-workflow\"
133}".to_string(),
134 parameters: json!({
135 "type": "object",
136 "properties": {
137 "mode": {
138 "type": "string",
139 "enum": ["create", "edit", "template", "validate", "info"],
140 "description": "操作模式",
141 "default": "create"
142 },
143 "workflow_id": {
144 "type": "string",
145 "description": "目标 workflow ID (edit/info 模式必需)"
146 },
147 "workflow": {
148 "type": "object",
149 "description": "完整 workflow 定义 (create 模式)"
150 },
151 "yaml_content": {
152 "type": "string",
153 "description": "YAML 内容字符串 (可选)"
154 },
155 "edit_operation": {
156 "type": "string",
157 "enum": [
158 "add_node", "remove_node", "update_node",
159 "add_edge", "remove_edge",
160 "add_input", "remove_input", "update_input",
161 "add_output", "remove_output", "update_output",
162 "update_metadata"
163 ],
164 "description": "编辑操作类型 (edit 模式)"
165 },
166 "edit_target": {
167 "type": "string",
168 "description": "编辑目标:节点ID、边ID、输入参数名等"
169 },
170 "edit_value": {
171 "type": "object",
172 "description": "新值:节点定义、边定义、参数定义等"
173 },
174 "location": {
175 "type": "string",
176 "enum": ["project", "user"],
177 "description": "保存位置",
178 "default": "project"
179 },
180 "template_type": {
181 "type": "string",
182 "enum": ["simple", "parallel", "condition", "research", "batch"],
183 "description": "模板类型 (template 模式)"
184 },
185 "overwrite": {
186 "type": "boolean",
187 "description": "是否覆盖已存在文件",
188 "default": false
189 }
190 },
191 "required": ["mode"]
192 }),
193 is_priority: false,
194 }
195 }
196
197 async fn execute(&self, params: Value) -> Result<String> {
198 let mode = params["mode"].as_str().unwrap_or("create");
199
200 match mode {
201 "create" => self.handle_create(params).await,
202 "edit" => self.handle_edit(params).await,
203 "template" => self.handle_template(params).await,
204 "validate" => self.handle_validate(params).await,
205 "info" => self.handle_info(params).await,
206 _ => bail!(
207 "Invalid mode: {}. Supported modes: create, edit, template, validate, info",
208 mode
209 ),
210 }
211 }
212}
213
214impl WorkflowCreateTool {
215 async fn handle_create(&self, params: Value) -> Result<String> {
217 let workflow_def = if let Some(yaml_str) = params["yaml_content"].as_str() {
219 parse_workflow(yaml_str).map_err(|e| anyhow::anyhow!("Failed to parse YAML: {}", e))?
221 } else if let Some(workflow_json) = params.get("workflow") {
222 serde_json::from_value(workflow_json.clone())
224 .map_err(|e| anyhow::anyhow!("Failed to parse workflow JSON: {}", e))?
225 } else {
226 bail!(
227 "Missing workflow definition. Provide either 'workflow' (JSON) or 'yaml_content' (YAML string)"
228 );
229 };
230
231 workflow_def
233 .validate()
234 .map_err(|e| anyhow::anyhow!("Workflow validation failed: {}", e))?;
235
236 let location = params["location"].as_str().unwrap_or("project");
238 let overwrite = params["overwrite"].as_bool().unwrap_or(false);
239
240 let save_dir = get_workflow_dir(location)?;
241
242 fs::create_dir_all(&save_dir)
244 .map_err(|e| anyhow::anyhow!("Failed to create directory {:?}: {}", save_dir, e))?;
245
246 let file_path = save_dir.join(format!("{}.yaml", workflow_def.id));
248 if file_path.exists() && !overwrite {
249 bail!(
250 "Workflow file already exists: {:?}\nSet overwrite=true to replace it.",
251 file_path
252 );
253 }
254
255 let yaml_content = to_yaml(&workflow_def)
257 .map_err(|e| anyhow::anyhow!("Failed to convert to YAML: {}", e))?;
258
259 fs::write(&file_path, &yaml_content)
261 .map_err(|e| anyhow::anyhow!("Failed to write file {:?}: {}", file_path, e))?;
262
263 Ok(format!(
264 "✓ Workflow created successfully!\n\n📄 File: {:?}\n📁 ID: {}\n📝 Name: {}\n\n```yaml\n{}\n```\n\nUse 'workflow_run' to execute this workflow.",
265 file_path, workflow_def.id, workflow_def.name, yaml_content
266 ))
267 }
268
269 async fn handle_template(&self, params: Value) -> Result<String> {
271 let template_type = params["template_type"].as_str().unwrap_or("simple");
272
273 let template = match template_type {
274 "simple" => self.get_simple_template(),
275 "parallel" => self.get_parallel_template(),
276 "condition" => self.get_condition_template(),
277 "research" => self.get_research_template(),
278 "batch" => self.get_batch_template(),
279 _ => bail!(
280 "Unknown template type: {}. Available: simple, parallel, condition, research, batch",
281 template_type
282 ),
283 };
284
285 Ok(format!(
286 "📋 Workflow Template: {}\n\n```yaml\n{}\n```\n\n💡 Tips:\n- Copy this template and modify as needed\n- Use 'workflow_create' with mode='create' to save it\n- Each workflow must have a start node and an end node",
287 template_type, template
288 ))
289 }
290
291 async fn handle_validate(&self, params: Value) -> Result<String> {
293 let workflow_def = if let Some(yaml_str) = params["yaml_content"].as_str() {
294 parse_workflow(yaml_str).map_err(|e| anyhow::anyhow!("YAML parsing failed: {}", e))?
295 } else if let Some(workflow_json) = params.get("workflow") {
296 serde_json::from_value(workflow_json.clone())
297 .map_err(|e| anyhow::anyhow!("JSON parsing failed: {}", e))?
298 } else {
299 bail!(
300 "Missing workflow definition. Provide either 'workflow' (JSON) or 'yaml_content' (YAML string)"
301 );
302 };
303
304 match workflow_def.validate() {
306 Ok(()) => Ok(format!(
307 "✓ Workflow validation passed!\n\n📁 ID: {}\n📝 Name: {}\n📊 Nodes: {}\n🔗 Edges: {}\n📥 Inputs: {}\n📤 Outputs: {}",
308 workflow_def.id,
309 workflow_def.name,
310 workflow_def.nodes.len(),
311 workflow_def.edges.len(),
312 workflow_def.inputs.len(),
313 workflow_def.outputs.len()
314 )),
315 Err(e) => bail!("Workflow validation failed: {}", e),
316 }
317 }
318
319 async fn handle_edit(&self, params: Value) -> Result<String> {
321 let workflow_id = params["workflow_id"]
323 .as_str()
324 .ok_or_else(|| anyhow::anyhow!("Missing 'workflow_id' parameter"))?;
325
326 let location = params["location"].as_str().unwrap_or("project");
328 let save_dir = get_workflow_dir(location)?;
329 let file_path = save_dir.join(format!("{}.yaml", workflow_id));
330
331 if !file_path.exists() {
333 bail!("Workflow '{}' not found at {:?}", workflow_id, file_path);
334 }
335
336 let mut workflow = parse_workflow_from_file(&file_path)
337 .map_err(|e| anyhow::anyhow!("Failed to load workflow: {}", e))?;
338
339 let edit_operation = params["edit_operation"]
341 .as_str()
342 .ok_or_else(|| anyhow::anyhow!("Missing 'edit_operation' parameter"))?;
343
344 let edit_target = params["edit_target"].as_str();
345 let edit_value = params.get("edit_value").cloned();
346
347 match edit_operation {
349 "add_node" => {
351 let node_json = edit_value
352 .ok_or_else(|| anyhow::anyhow!("Missing 'edit_value' for add_node"))?;
353 let new_node: NodeDef = serde_json::from_value(node_json)
354 .map_err(|e| anyhow::anyhow!("Invalid node definition: {}", e))?;
355
356 if workflow.nodes.iter().any(|n| n.id == new_node.id) {
358 bail!("Node '{}' already exists", new_node.id);
359 }
360
361 workflow.nodes.push(new_node);
362 }
363
364 "remove_node" => {
365 let node_id = edit_target
366 .ok_or_else(|| anyhow::anyhow!("Missing 'edit_target' for remove_node"))?;
367 let idx = workflow
368 .nodes
369 .iter()
370 .position(|n| n.id == node_id)
371 .ok_or_else(|| anyhow::anyhow!("Node '{}' not found", node_id))?;
372
373 let node = &workflow.nodes[idx];
375 if node.node_type == NodeType::Start || node.node_type == NodeType::End {
376 bail!("Cannot remove start/end nodes");
377 }
378
379 workflow
381 .edges
382 .retain(|e| e.from != node_id && e.to != node_id);
383 workflow.nodes.remove(idx);
384 }
385
386 "update_node" => {
387 let node_id = edit_target
388 .ok_or_else(|| anyhow::anyhow!("Missing 'edit_target' for update_node"))?;
389 let node = workflow
390 .nodes
391 .iter_mut()
392 .find(|n| n.id == node_id)
393 .ok_or_else(|| anyhow::anyhow!("Node '{}' not found", node_id))?;
394
395 let updates = edit_value
396 .ok_or_else(|| anyhow::anyhow!("Missing 'edit_value' for update_node"))?;
397
398 if let Some(name) = updates.get("name").and_then(|v| v.as_str()) {
400 node.name = name.to_string();
401 }
402 if let Some(task) = updates.get("task").and_then(|v| v.as_str()) {
403 node.task = Some(task.to_string());
404 }
405 if let Some(params_val) = updates.get("params") {
406 node.params = serde_json::from_value(params_val.clone())
407 .map_err(|e| anyhow::anyhow!("Invalid params: {}", e))?;
408 }
409 if let Some(timeout) = updates.get("timeout_ms").and_then(|v| v.as_u64()) {
410 node.timeout_ms = Some(timeout);
411 }
412 }
413
414 "add_edge" => {
416 let edge_json = edit_value
417 .ok_or_else(|| anyhow::anyhow!("Missing 'edit_value' for add_edge"))?;
418 let new_edge: EdgeDef = serde_json::from_value(edge_json)
419 .map_err(|e| anyhow::anyhow!("Invalid edge definition: {}", e))?;
420
421 if !workflow.nodes.iter().any(|n| n.id == new_edge.from) {
423 bail!("Source node '{}' not found", new_edge.from);
424 }
425 if !workflow.nodes.iter().any(|n| n.id == new_edge.to) {
426 bail!("Target node '{}' not found", new_edge.to);
427 }
428
429 workflow.edges.push(new_edge);
430 }
431
432 "remove_edge" => {
433 let from = edit_target.ok_or_else(|| {
434 anyhow::anyhow!("Missing 'edit_target' (from node) for remove_edge")
435 })?;
436 let to = params["to"]
437 .as_str()
438 .ok_or_else(|| anyhow::anyhow!("Missing 'to' parameter for remove_edge"))?;
439
440 workflow.edges.retain(|e| e.from != from || e.to != to);
441 }
442
443 "add_input" => {
445 let input_json = edit_value
446 .ok_or_else(|| anyhow::anyhow!("Missing 'edit_value' for add_input"))?;
447 let new_input: InputDef = serde_json::from_value(input_json)
448 .map_err(|e| anyhow::anyhow!("Invalid input definition: {}", e))?;
449
450 workflow.inputs.push(new_input);
451 }
452
453 "remove_input" => {
454 let input_name = edit_target
455 .ok_or_else(|| anyhow::anyhow!("Missing 'edit_target' for remove_input"))?;
456 workflow.inputs.retain(|i| i.name != input_name);
457 }
458
459 "update_input" => {
460 let input_name = edit_target
461 .ok_or_else(|| anyhow::anyhow!("Missing 'edit_target' for update_input"))?;
462 let input = workflow
463 .inputs
464 .iter_mut()
465 .find(|i| i.name == input_name)
466 .ok_or_else(|| anyhow::anyhow!("Input '{}' not found", input_name))?;
467
468 let updates = edit_value
469 .ok_or_else(|| anyhow::anyhow!("Missing 'edit_value' for update_input"))?;
470
471 if let Some(desc) = updates.get("description").and_then(|v| v.as_str()) {
472 input.description = Some(desc.to_string());
473 }
474 if let Some(required) = updates.get("required").and_then(|v| v.as_bool()) {
475 input.required = required;
476 }
477 if let Some(default) = updates.get("default") {
478 input.default = Some(default.clone());
479 }
480 }
481
482 "add_output" => {
484 let output_json = edit_value
485 .ok_or_else(|| anyhow::anyhow!("Missing 'edit_value' for add_output"))?;
486 let new_output: OutputDef = serde_json::from_value(output_json)
487 .map_err(|e| anyhow::anyhow!("Invalid output definition: {}", e))?;
488
489 workflow.outputs.push(new_output);
490 }
491
492 "remove_output" => {
493 let output_name = edit_target
494 .ok_or_else(|| anyhow::anyhow!("Missing 'edit_target' for remove_output"))?;
495 workflow.outputs.retain(|o| o.name != output_name);
496 }
497
498 "update_output" => {
499 let output_name = edit_target
500 .ok_or_else(|| anyhow::anyhow!("Missing 'edit_target' for update_output"))?;
501 let output = workflow
502 .outputs
503 .iter_mut()
504 .find(|o| o.name == output_name)
505 .ok_or_else(|| anyhow::anyhow!("Output '{}' not found", output_name))?;
506
507 let updates = edit_value
508 .ok_or_else(|| anyhow::anyhow!("Missing 'edit_value' for update_output"))?;
509
510 if let Some(value) = updates.get("value").and_then(|v| v.as_str()) {
511 output.value = value.to_string();
512 }
513 if let Some(desc) = updates.get("description").and_then(|v| v.as_str()) {
514 output.description = Some(desc.to_string());
515 }
516 }
517
518 "update_metadata" => {
520 let updates = edit_value
521 .ok_or_else(|| anyhow::anyhow!("Missing 'edit_value' for update_metadata"))?;
522
523 if let Some(name) = updates.get("name").and_then(|v| v.as_str()) {
524 workflow.name = name.to_string();
525 }
526 if let Some(desc) = updates.get("description").and_then(|v| v.as_str()) {
527 workflow.description = Some(desc.to_string());
528 }
529 if let Some(version) = updates.get("version").and_then(|v| v.as_str()) {
530 workflow.version = version.to_string();
531 }
532 }
533
534 _ => bail!("Unknown edit operation: {}", edit_operation),
535 }
536
537 workflow
539 .validate()
540 .map_err(|e| anyhow::anyhow!("Workflow validation failed after edit: {}", e))?;
541
542 let yaml_content =
544 to_yaml(&workflow).map_err(|e| anyhow::anyhow!("Failed to convert to YAML: {}", e))?;
545 fs::write(&file_path, &yaml_content)
546 .map_err(|e| anyhow::anyhow!("Failed to write file {:?}: {}", file_path, e))?;
547
548 Ok(format!(
549 "✓ Workflow '{}' updated successfully!\n\nOperation: {}\n📄 File: {:?}\n\n```yaml\n{}\n```\n\nUse 'info' mode to see detailed structure.",
550 workflow_id, edit_operation, file_path, yaml_content
551 ))
552 }
553
554 async fn handle_info(&self, params: Value) -> Result<String> {
556 let workflow_id = params["workflow_id"]
557 .as_str()
558 .ok_or_else(|| anyhow::anyhow!("Missing 'workflow_id' parameter"))?;
559
560 let location = params["location"].as_str().unwrap_or("project");
562 let save_dir = get_workflow_dir(location)?;
563 let file_path = save_dir.join(format!("{}.yaml", workflow_id));
564
565 if !file_path.exists() {
567 bail!("Workflow '{}' not found at {:?}", workflow_id, file_path);
568 }
569
570 let workflow = parse_workflow_from_file(&file_path)
571 .map_err(|e| anyhow::anyhow!("Failed to load workflow: {}", e))?;
572
573 let mut info = format!(
574 "📋 Workflow: {}\n\n📁 ID: {}\n📝 Name: {}\n📌 Version: {}\n",
575 workflow_id, workflow.id, workflow.name, workflow.version
576 );
577
578 if let Some(ref desc) = workflow.description {
579 info.push_str(&format!("📖 Description: {}\n", desc));
580 }
581
582 info.push_str("\n📊 Nodes:\n");
584 for node in &workflow.nodes {
585 let node_type = match node.node_type {
586 NodeType::Start => "start",
587 NodeType::End => "end",
588 NodeType::Task => "task",
589 NodeType::Condition => "condition",
590 NodeType::Parallel => "parallel",
591 NodeType::Pipeline => "pipeline",
592 NodeType::Wait => "wait",
593 NodeType::Approval => "approval",
594 NodeType::SubWorkflow => "subworkflow",
595 };
596 info.push_str(&format!(" - {} [{}] {}\n", node.id, node_type, node.name));
597
598 if let Some(ref task) = node.task {
599 info.push_str(&format!(" Task: {}\n", task));
600 }
601 }
602
603 info.push_str("\n🔗 Edges:\n");
605 for edge in &workflow.edges {
606 let label = edge
607 .label
608 .as_ref()
609 .map(|l| format!(" ({})", l))
610 .unwrap_or_default();
611 info.push_str(&format!(" {} → {}{}\n", edge.from, edge.to, label));
612 }
613
614 if !workflow.inputs.is_empty() {
616 info.push_str("\n📥 Inputs:\n");
617 for input in &workflow.inputs {
618 let required = if input.required { " (required)" } else { "" };
619 info.push_str(&format!(
620 " - {} [{}]{}\n",
621 input.name, input.input_type, required
622 ));
623 if let Some(ref desc) = input.description {
624 info.push_str(&format!(" {}\n", desc));
625 }
626 }
627 }
628
629 if !workflow.outputs.is_empty() {
631 info.push_str("\n📤 Outputs:\n");
632 for output in &workflow.outputs {
633 info.push_str(&format!(" - {}: {}\n", output.name, output.value));
634 }
635 }
636
637 Ok(info)
638 }
639
640 fn get_simple_template(&self) -> String {
642 r#"id: my-workflow
643name: My Workflow
644version: "1.0.0"
645description: A simple workflow example
646
647inputs:
648 - name: param1
649 type: string
650 required: true
651 description: First parameter
652
653outputs:
654 - name: result
655 value: "{{nodes.end.output}}"
656
657nodes:
658 - id: start
659 type: start
660 name: Start
661
662 - id: task1
663 type: task
664 name: First Task
665 task: example_task
666 params:
667 input: "{{inputs.param1}}"
668 on_failure:
669 type: abort
670
671 - id: end
672 type: end
673 name: End
674
675edges:
676 - from: start
677 to: task1
678 - from: task1
679 to: end
680"#
681 .to_string()
682 }
683
684 fn get_parallel_template(&self) -> String {
686 r#"id: parallel-workflow
687name: Parallel Workflow
688version: "1.0.0"
689description: A workflow with parallel execution
690
691inputs:
692 - name: data
693 type: string
694 required: true
695 description: Input data to process
696
697nodes:
698 - id: start
699 type: start
700 name: Start
701
702 - id: parallel1
703 type: parallel
704 name: Parallel Processing
705 parallel_branches:
706 - name: Branch A
707 nodes:
708 - id: task_a
709 type: task
710 name: Task A
711 task: process_a
712 params:
713 data: "{{inputs.data}}"
714 - name: Branch B
715 nodes:
716 - id: task_b
717 type: task
718 name: Task B
719 task: process_b
720 params:
721 data: "{{inputs.data}}"
722
723 - id: merge
724 type: task
725 name: Merge Results
726 task: merge_results
727 params:
728 result_a: "{{nodes.task_a.output}}"
729 result_b: "{{nodes.task_b.output}}"
730
731 - id: end
732 type: end
733 name: End
734
735edges:
736 - from: start
737 to: parallel1
738 - from: parallel1
739 to: merge
740 - from: merge
741 to: end
742"#
743 .to_string()
744 }
745
746 fn get_condition_template(&self) -> String {
748 r#"id: condition-workflow
749name: Conditional Workflow
750version: "1.0.0"
751description: A workflow with conditional branches
752
753inputs:
754 - name: value
755 type: number
756 required: true
757 description: Value to check
758
759nodes:
760 - id: start
761 type: start
762 name: Start
763
764 - id: check_value
765 type: condition
766 name: Check Value
767 branches:
768 - name: Positive
769 condition: "{{inputs.value}} > 0"
770 target: positive_task
771 - name: Negative
772 condition: "{{inputs.value}} < 0"
773 target: negative_task
774 - name: Zero
775 condition: "{{inputs.value}} == 0"
776 target: zero_task
777
778 - id: positive_task
779 type: task
780 name: Handle Positive
781 task: handle_positive
782
783 - id: negative_task
784 type: task
785 name: Handle Negative
786 task: handle_negative
787
788 - id: zero_task
789 type: task
790 name: Handle Zero
791 task: handle_zero
792
793 - id: end
794 type: end
795 name: End
796
797edges:
798 - from: start
799 to: check_value
800 - from: positive_task
801 to: end
802 - from: negative_task
803 to: end
804 - from: zero_task
805 to: end
806"#
807 .to_string()
808 }
809
810 fn get_research_template(&self) -> String {
812 r#"id: research-workflow
813name: Research Workflow
814version: "1.0.0"
815description: Automated research and report generation
816
817inputs:
818 - name: topic
819 type: string
820 required: true
821 description: Research topic
822 - name: depth
823 type: string
824 required: false
825 default: "medium"
826 description: Research depth (shallow/medium/deep)
827
828outputs:
829 - name: report
830 value: "{{nodes.generate_report.output}}"
831
832nodes:
833 - id: start
834 type: start
835 name: Start
836
837 - id: search_web
838 type: task
839 name: Search Web
840 task: websearch
841 params:
842 query: "{{inputs.topic}}"
843 max_results: 10
844 on_failure:
845 type: retry
846 max_attempts: 3
847
848 - id: analyze_results
849 type: task
850 name: Analyze Results
851 task: analyze
852 params:
853 data: "{{nodes.search_web.output}}"
854 depth: "{{inputs.depth}}"
855
856 - id: generate_report
857 type: task
858 name: Generate Report
859 task: content_generation
860 params:
861 topic: "{{inputs.topic}}"
862 research_data: "{{nodes.analyze_results.output}}"
863 style: "professional"
864
865 - id: end
866 type: end
867 name: End
868
869edges:
870 - from: start
871 to: search_web
872 - from: search_web
873 to: analyze_results
874 - from: analyze_results
875 to: generate_report
876 - from: generate_report
877 to: end
878"#
879 .to_string()
880 }
881
882 fn get_batch_template(&self) -> String {
884 r#"id: batch-workflow
885name: Batch Processing Workflow
886version: "1.0.0"
887description: Process multiple items in batch
888
889inputs:
890 - name: items
891 type: array
892 required: true
893 description: Array of items to process
894 - name: operation
895 type: string
896 required: true
897 description: Operation to perform on each item
898
899nodes:
900 - id: start
901 type: start
902 name: Start
903
904 - id: prepare_batch
905 type: task
906 name: Prepare Batch
907 task: prepare_batch
908 params:
909 items: "{{inputs.items}}"
910
911 - id: process_items
912 type: parallel
913 name: Process Items
914 parallel_branches:
915 - name: Process Chunk 1
916 nodes:
917 - id: chunk1
918 type: task
919 name: Process Chunk 1
920 task: "{{inputs.operation}}"
921 params:
922 items: "{{nodes.prepare_batch.chunk1}}"
923 - name: Process Chunk 2
924 nodes:
925 - id: chunk2
926 type: task
927 name: Process Chunk 2
928 task: "{{inputs.operation}}"
929 params:
930 items: "{{nodes.prepare_batch.chunk2}}"
931
932 - id: aggregate_results
933 type: task
934 name: Aggregate Results
935 task: aggregate
936 params:
937 results:
938 - "{{nodes.chunk1.output}}"
939 - "{{nodes.chunk2.output}}"
940
941 - id: end
942 type: end
943 name: End
944
945edges:
946 - from: start
947 to: prepare_batch
948 - from: prepare_batch
949 to: process_items
950 - from: process_items
951 to: aggregate_results
952 - from: aggregate_results
953 to: end
954"#
955 .to_string()
956 }
957}
958
959#[cfg(test)]
960mod tests {
961 use super::*;
962 use serde_json::json;
963
964 #[tokio::test]
965 async fn test_template_simple() {
966 let tool = WorkflowCreateTool;
967 let result = tool
968 .execute(json!({
969 "mode": "template",
970 "template_type": "simple"
971 }))
972 .await
973 .unwrap();
974
975 assert!(result.contains("simple"));
976 assert!(result.contains("id:"));
977 assert!(result.contains("nodes:"));
978 assert!(result.contains("edges:"));
979 }
980
981 #[tokio::test]
982 async fn test_template_research() {
983 let tool = WorkflowCreateTool;
984 let result = tool
985 .execute(json!({
986 "mode": "template",
987 "template_type": "research"
988 }))
989 .await
990 .unwrap();
991
992 assert!(result.contains("research"));
993 assert!(result.contains("websearch"));
994 assert!(result.contains("generate_report"));
995 }
996
997 #[tokio::test]
998 async fn test_validate_valid_workflow() {
999 let tool = WorkflowCreateTool;
1000 let result = tool.execute(json!({
1001 "mode": "validate",
1002 "yaml_content": "id: test\nname: Test\nnodes:\n - id: start\n type: start\n name: Start\n - id: end\n type: end\n name: End\nedges:\n - from: start\n to: end"
1003 })).await.unwrap();
1004
1005 assert!(result.contains("validation passed"));
1006 assert!(result.contains("Nodes: 2"));
1007 assert!(result.contains("Edges: 1"));
1008 }
1009
1010 #[tokio::test]
1011 async fn test_validate_invalid_workflow() {
1012 let tool = WorkflowCreateTool;
1013 let result = tool.execute(json!({
1014 "mode": "validate",
1015 "yaml_content": "id: test\nname: Test\nnodes:\n - id: task1\n type: task\n name: Task"
1016 })).await;
1017
1018 assert!(result.is_err());
1020 assert!(
1021 result
1022 .unwrap_err()
1023 .to_string()
1024 .contains("validation failed")
1025 );
1026 }
1027
1028 #[tokio::test]
1029 async fn test_info_mode() {
1030 let tool = WorkflowCreateTool;
1031
1032 let create_result = tool.execute(json!({
1034 "mode": "create",
1035 "workflow": {
1036 "id": "test-info-workflow",
1037 "name": "Test Info Workflow",
1038 "version": "1.0.0",
1039 "description": "A test workflow for info mode",
1040 "nodes": [
1041 {"id": "start", "type": "start", "name": "Start"},
1042 {"id": "task1", "type": "task", "name": "Task 1", "task": "process"},
1043 {"id": "end", "type": "end", "name": "End"}
1044 ],
1045 "edges": [
1046 {"from": "start", "to": "task1"},
1047 {"from": "task1", "to": "end"}
1048 ],
1049 "inputs": [
1050 {"name": "data", "type": "string", "required": true, "description": "Input data"}
1051 ],
1052 "outputs": [
1053 {"name": "result", "value": "{{nodes.task1.output}}"}
1054 ]
1055 },
1056 "location": "project",
1057 "overwrite": true
1058 })).await.unwrap();
1059
1060 assert!(create_result.contains("created successfully"));
1061
1062 let info_result = tool
1064 .execute(json!({
1065 "mode": "info",
1066 "workflow_id": "test-info-workflow"
1067 }))
1068 .await
1069 .unwrap();
1070
1071 assert!(info_result.contains("test-info-workflow"));
1072 assert!(info_result.contains("Test Info Workflow"));
1073 assert!(info_result.contains("Task 1"));
1074 assert!(info_result.contains("start → task1"));
1075 assert!(info_result.contains("data [string] (required)"));
1076 }
1077
1078 #[tokio::test]
1079 async fn test_edit_update_node() {
1080 let tool = WorkflowCreateTool;
1081
1082 tool.execute(json!({
1084 "mode": "create",
1085 "workflow": {
1086 "id": "test-edit-workflow",
1087 "name": "Test Edit Workflow",
1088 "version": "1.0.0",
1089 "nodes": [
1090 {"id": "start", "type": "start", "name": "Start"},
1091 {"id": "task1", "type": "task", "name": "Old Name", "task": "old_task"},
1092 {"id": "end", "type": "end", "name": "End"}
1093 ],
1094 "edges": [
1095 {"from": "start", "to": "task1"},
1096 {"from": "task1", "to": "end"}
1097 ]
1098 },
1099 "location": "project",
1100 "overwrite": true
1101 }))
1102 .await
1103 .unwrap();
1104
1105 let edit_result = tool
1107 .execute(json!({
1108 "mode": "edit",
1109 "workflow_id": "test-edit-workflow",
1110 "edit_operation": "update_node",
1111 "edit_target": "task1",
1112 "edit_value": {
1113 "name": "New Name",
1114 "task": "new_task"
1115 }
1116 }))
1117 .await
1118 .unwrap();
1119
1120 assert!(edit_result.contains("updated successfully"));
1121 assert!(edit_result.contains("update_node"));
1122
1123 let info_result = tool
1125 .execute(json!({
1126 "mode": "info",
1127 "workflow_id": "test-edit-workflow"
1128 }))
1129 .await
1130 .unwrap();
1131
1132 assert!(info_result.contains("New Name"));
1133 assert!(info_result.contains("new_task"));
1134 assert!(!info_result.contains("Old Name"));
1135 }
1136
1137 #[tokio::test]
1138 async fn test_edit_add_node() {
1139 let tool = WorkflowCreateTool;
1140
1141 tool.execute(json!({
1143 "mode": "create",
1144 "workflow": {
1145 "id": "test-add-node-workflow",
1146 "name": "Test Add Node",
1147 "version": "1.0.0",
1148 "nodes": [
1149 {"id": "start", "type": "start", "name": "Start"},
1150 {"id": "end", "type": "end", "name": "End"}
1151 ],
1152 "edges": [
1153 {"from": "start", "to": "end"}
1154 ]
1155 },
1156 "location": "project",
1157 "overwrite": true
1158 }))
1159 .await
1160 .unwrap();
1161
1162 let edit_result = tool
1164 .execute(json!({
1165 "mode": "edit",
1166 "workflow_id": "test-add-node-workflow",
1167 "edit_operation": "add_node",
1168 "edit_value": {
1169 "id": "new_task",
1170 "type": "task",
1171 "name": "New Task",
1172 "task": "process"
1173 }
1174 }))
1175 .await
1176 .unwrap();
1177
1178 assert!(edit_result.contains("updated successfully"));
1179
1180 let edge_result = tool
1182 .execute(json!({
1183 "mode": "edit",
1184 "workflow_id": "test-add-node-workflow",
1185 "edit_operation": "add_edge",
1186 "edit_value": {
1187 "from": "start",
1188 "to": "new_task"
1189 }
1190 }))
1191 .await
1192 .unwrap();
1193
1194 assert!(edge_result.contains("updated successfully"));
1195
1196 tool.execute(json!({
1198 "mode": "edit",
1199 "workflow_id": "test-add-node-workflow",
1200 "edit_operation": "add_edge",
1201 "edit_value": {
1202 "from": "new_task",
1203 "to": "end"
1204 }
1205 }))
1206 .await
1207 .unwrap();
1208
1209 let info_result = tool
1211 .execute(json!({
1212 "mode": "info",
1213 "workflow_id": "test-add-node-workflow"
1214 }))
1215 .await
1216 .unwrap();
1217
1218 assert!(info_result.contains("new_task"));
1219 assert!(info_result.contains("New Task"));
1220 assert!(info_result.contains("start → new_task"));
1221 assert!(info_result.contains("new_task → end"));
1222 }
1223
1224 #[tokio::test]
1225 async fn test_invalid_mode() {
1226 let tool = WorkflowCreateTool;
1227 let result = tool
1228 .execute(json!({
1229 "mode": "invalid"
1230 }))
1231 .await;
1232
1233 assert!(result.is_err());
1234 }
1235
1236 #[test]
1237 fn test_definition() {
1238 let tool = WorkflowCreateTool;
1239 let def = tool.definition();
1240
1241 assert_eq!(def.name, "workflow_create");
1242 assert!(def.description.contains("创建")); assert!(def.parameters["properties"]["mode"].is_object());
1244 }
1245}