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::Wait => "wait",
592 NodeType::Approval => "approval",
593 NodeType::SubWorkflow => "subworkflow",
594 };
595 info.push_str(&format!(" - {} [{}] {}\n", node.id, node_type, node.name));
596
597 if let Some(ref task) = node.task {
598 info.push_str(&format!(" Task: {}\n", task));
599 }
600 }
601
602 info.push_str("\n🔗 Edges:\n");
604 for edge in &workflow.edges {
605 let label = edge
606 .label
607 .as_ref()
608 .map(|l| format!(" ({})", l))
609 .unwrap_or_default();
610 info.push_str(&format!(" {} → {}{}\n", edge.from, edge.to, label));
611 }
612
613 if !workflow.inputs.is_empty() {
615 info.push_str("\n📥 Inputs:\n");
616 for input in &workflow.inputs {
617 let required = if input.required { " (required)" } else { "" };
618 info.push_str(&format!(
619 " - {} [{}]{}\n",
620 input.name, input.input_type, required
621 ));
622 if let Some(ref desc) = input.description {
623 info.push_str(&format!(" {}\n", desc));
624 }
625 }
626 }
627
628 if !workflow.outputs.is_empty() {
630 info.push_str("\n📤 Outputs:\n");
631 for output in &workflow.outputs {
632 info.push_str(&format!(" - {}: {}\n", output.name, output.value));
633 }
634 }
635
636 Ok(info)
637 }
638
639 fn get_simple_template(&self) -> String {
641 r#"id: my-workflow
642name: My Workflow
643version: "1.0.0"
644description: A simple workflow example
645
646inputs:
647 - name: param1
648 type: string
649 required: true
650 description: First parameter
651
652outputs:
653 - name: result
654 value: "{{nodes.end.output}}"
655
656nodes:
657 - id: start
658 type: start
659 name: Start
660
661 - id: task1
662 type: task
663 name: First Task
664 task: example_task
665 params:
666 input: "{{inputs.param1}}"
667 on_failure:
668 type: abort
669
670 - id: end
671 type: end
672 name: End
673
674edges:
675 - from: start
676 to: task1
677 - from: task1
678 to: end
679"#
680 .to_string()
681 }
682
683 fn get_parallel_template(&self) -> String {
685 r#"id: parallel-workflow
686name: Parallel Workflow
687version: "1.0.0"
688description: A workflow with parallel execution
689
690inputs:
691 - name: data
692 type: string
693 required: true
694 description: Input data to process
695
696nodes:
697 - id: start
698 type: start
699 name: Start
700
701 - id: parallel1
702 type: parallel
703 name: Parallel Processing
704 parallel_branches:
705 - name: Branch A
706 nodes:
707 - id: task_a
708 type: task
709 name: Task A
710 task: process_a
711 params:
712 data: "{{inputs.data}}"
713 - name: Branch B
714 nodes:
715 - id: task_b
716 type: task
717 name: Task B
718 task: process_b
719 params:
720 data: "{{inputs.data}}"
721
722 - id: merge
723 type: task
724 name: Merge Results
725 task: merge_results
726 params:
727 result_a: "{{nodes.task_a.output}}"
728 result_b: "{{nodes.task_b.output}}"
729
730 - id: end
731 type: end
732 name: End
733
734edges:
735 - from: start
736 to: parallel1
737 - from: parallel1
738 to: merge
739 - from: merge
740 to: end
741"#
742 .to_string()
743 }
744
745 fn get_condition_template(&self) -> String {
747 r#"id: condition-workflow
748name: Conditional Workflow
749version: "1.0.0"
750description: A workflow with conditional branches
751
752inputs:
753 - name: value
754 type: number
755 required: true
756 description: Value to check
757
758nodes:
759 - id: start
760 type: start
761 name: Start
762
763 - id: check_value
764 type: condition
765 name: Check Value
766 branches:
767 - name: Positive
768 condition: "{{inputs.value}} > 0"
769 target: positive_task
770 - name: Negative
771 condition: "{{inputs.value}} < 0"
772 target: negative_task
773 - name: Zero
774 condition: "{{inputs.value}} == 0"
775 target: zero_task
776
777 - id: positive_task
778 type: task
779 name: Handle Positive
780 task: handle_positive
781
782 - id: negative_task
783 type: task
784 name: Handle Negative
785 task: handle_negative
786
787 - id: zero_task
788 type: task
789 name: Handle Zero
790 task: handle_zero
791
792 - id: end
793 type: end
794 name: End
795
796edges:
797 - from: start
798 to: check_value
799 - from: positive_task
800 to: end
801 - from: negative_task
802 to: end
803 - from: zero_task
804 to: end
805"#
806 .to_string()
807 }
808
809 fn get_research_template(&self) -> String {
811 r#"id: research-workflow
812name: Research Workflow
813version: "1.0.0"
814description: Automated research and report generation
815
816inputs:
817 - name: topic
818 type: string
819 required: true
820 description: Research topic
821 - name: depth
822 type: string
823 required: false
824 default: "medium"
825 description: Research depth (shallow/medium/deep)
826
827outputs:
828 - name: report
829 value: "{{nodes.generate_report.output}}"
830
831nodes:
832 - id: start
833 type: start
834 name: Start
835
836 - id: search_web
837 type: task
838 name: Search Web
839 task: websearch
840 params:
841 query: "{{inputs.topic}}"
842 max_results: 10
843 on_failure:
844 type: retry
845 max_attempts: 3
846
847 - id: analyze_results
848 type: task
849 name: Analyze Results
850 task: analyze
851 params:
852 data: "{{nodes.search_web.output}}"
853 depth: "{{inputs.depth}}"
854
855 - id: generate_report
856 type: task
857 name: Generate Report
858 task: content_generation
859 params:
860 topic: "{{inputs.topic}}"
861 research_data: "{{nodes.analyze_results.output}}"
862 style: "professional"
863
864 - id: end
865 type: end
866 name: End
867
868edges:
869 - from: start
870 to: search_web
871 - from: search_web
872 to: analyze_results
873 - from: analyze_results
874 to: generate_report
875 - from: generate_report
876 to: end
877"#
878 .to_string()
879 }
880
881 fn get_batch_template(&self) -> String {
883 r#"id: batch-workflow
884name: Batch Processing Workflow
885version: "1.0.0"
886description: Process multiple items in batch
887
888inputs:
889 - name: items
890 type: array
891 required: true
892 description: Array of items to process
893 - name: operation
894 type: string
895 required: true
896 description: Operation to perform on each item
897
898nodes:
899 - id: start
900 type: start
901 name: Start
902
903 - id: prepare_batch
904 type: task
905 name: Prepare Batch
906 task: prepare_batch
907 params:
908 items: "{{inputs.items}}"
909
910 - id: process_items
911 type: parallel
912 name: Process Items
913 parallel_branches:
914 - name: Process Chunk 1
915 nodes:
916 - id: chunk1
917 type: task
918 name: Process Chunk 1
919 task: "{{inputs.operation}}"
920 params:
921 items: "{{nodes.prepare_batch.chunk1}}"
922 - name: Process Chunk 2
923 nodes:
924 - id: chunk2
925 type: task
926 name: Process Chunk 2
927 task: "{{inputs.operation}}"
928 params:
929 items: "{{nodes.prepare_batch.chunk2}}"
930
931 - id: aggregate_results
932 type: task
933 name: Aggregate Results
934 task: aggregate
935 params:
936 results:
937 - "{{nodes.chunk1.output}}"
938 - "{{nodes.chunk2.output}}"
939
940 - id: end
941 type: end
942 name: End
943
944edges:
945 - from: start
946 to: prepare_batch
947 - from: prepare_batch
948 to: process_items
949 - from: process_items
950 to: aggregate_results
951 - from: aggregate_results
952 to: end
953"#
954 .to_string()
955 }
956}
957
958#[cfg(test)]
959mod tests {
960 use super::*;
961 use serde_json::json;
962
963 #[tokio::test]
964 async fn test_template_simple() {
965 let tool = WorkflowCreateTool;
966 let result = tool
967 .execute(json!({
968 "mode": "template",
969 "template_type": "simple"
970 }))
971 .await
972 .unwrap();
973
974 assert!(result.contains("simple"));
975 assert!(result.contains("id:"));
976 assert!(result.contains("nodes:"));
977 assert!(result.contains("edges:"));
978 }
979
980 #[tokio::test]
981 async fn test_template_research() {
982 let tool = WorkflowCreateTool;
983 let result = tool
984 .execute(json!({
985 "mode": "template",
986 "template_type": "research"
987 }))
988 .await
989 .unwrap();
990
991 assert!(result.contains("research"));
992 assert!(result.contains("websearch"));
993 assert!(result.contains("generate_report"));
994 }
995
996 #[tokio::test]
997 async fn test_validate_valid_workflow() {
998 let tool = WorkflowCreateTool;
999 let result = tool.execute(json!({
1000 "mode": "validate",
1001 "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"
1002 })).await.unwrap();
1003
1004 assert!(result.contains("validation passed"));
1005 assert!(result.contains("Nodes: 2"));
1006 assert!(result.contains("Edges: 1"));
1007 }
1008
1009 #[tokio::test]
1010 async fn test_validate_invalid_workflow() {
1011 let tool = WorkflowCreateTool;
1012 let result = tool.execute(json!({
1013 "mode": "validate",
1014 "yaml_content": "id: test\nname: Test\nnodes:\n - id: task1\n type: task\n name: Task"
1015 })).await;
1016
1017 assert!(result.is_err());
1019 assert!(
1020 result
1021 .unwrap_err()
1022 .to_string()
1023 .contains("validation failed")
1024 );
1025 }
1026
1027 #[tokio::test]
1028 async fn test_info_mode() {
1029 let tool = WorkflowCreateTool;
1030
1031 let create_result = tool.execute(json!({
1033 "mode": "create",
1034 "workflow": {
1035 "id": "test-info-workflow",
1036 "name": "Test Info Workflow",
1037 "version": "1.0.0",
1038 "description": "A test workflow for info mode",
1039 "nodes": [
1040 {"id": "start", "type": "start", "name": "Start"},
1041 {"id": "task1", "type": "task", "name": "Task 1", "task": "process"},
1042 {"id": "end", "type": "end", "name": "End"}
1043 ],
1044 "edges": [
1045 {"from": "start", "to": "task1"},
1046 {"from": "task1", "to": "end"}
1047 ],
1048 "inputs": [
1049 {"name": "data", "type": "string", "required": true, "description": "Input data"}
1050 ],
1051 "outputs": [
1052 {"name": "result", "value": "{{nodes.task1.output}}"}
1053 ]
1054 },
1055 "location": "project",
1056 "overwrite": true
1057 })).await.unwrap();
1058
1059 assert!(create_result.contains("created successfully"));
1060
1061 let info_result = tool
1063 .execute(json!({
1064 "mode": "info",
1065 "workflow_id": "test-info-workflow"
1066 }))
1067 .await
1068 .unwrap();
1069
1070 assert!(info_result.contains("test-info-workflow"));
1071 assert!(info_result.contains("Test Info Workflow"));
1072 assert!(info_result.contains("Task 1"));
1073 assert!(info_result.contains("start → task1"));
1074 assert!(info_result.contains("data [string] (required)"));
1075 }
1076
1077 #[tokio::test]
1078 async fn test_edit_update_node() {
1079 let tool = WorkflowCreateTool;
1080
1081 tool.execute(json!({
1083 "mode": "create",
1084 "workflow": {
1085 "id": "test-edit-workflow",
1086 "name": "Test Edit Workflow",
1087 "version": "1.0.0",
1088 "nodes": [
1089 {"id": "start", "type": "start", "name": "Start"},
1090 {"id": "task1", "type": "task", "name": "Old Name", "task": "old_task"},
1091 {"id": "end", "type": "end", "name": "End"}
1092 ],
1093 "edges": [
1094 {"from": "start", "to": "task1"},
1095 {"from": "task1", "to": "end"}
1096 ]
1097 },
1098 "location": "project",
1099 "overwrite": true
1100 }))
1101 .await
1102 .unwrap();
1103
1104 let edit_result = tool
1106 .execute(json!({
1107 "mode": "edit",
1108 "workflow_id": "test-edit-workflow",
1109 "edit_operation": "update_node",
1110 "edit_target": "task1",
1111 "edit_value": {
1112 "name": "New Name",
1113 "task": "new_task"
1114 }
1115 }))
1116 .await
1117 .unwrap();
1118
1119 assert!(edit_result.contains("updated successfully"));
1120 assert!(edit_result.contains("update_node"));
1121
1122 let info_result = tool
1124 .execute(json!({
1125 "mode": "info",
1126 "workflow_id": "test-edit-workflow"
1127 }))
1128 .await
1129 .unwrap();
1130
1131 assert!(info_result.contains("New Name"));
1132 assert!(info_result.contains("new_task"));
1133 assert!(!info_result.contains("Old Name"));
1134 }
1135
1136 #[tokio::test]
1137 async fn test_edit_add_node() {
1138 let tool = WorkflowCreateTool;
1139
1140 tool.execute(json!({
1142 "mode": "create",
1143 "workflow": {
1144 "id": "test-add-node-workflow",
1145 "name": "Test Add Node",
1146 "version": "1.0.0",
1147 "nodes": [
1148 {"id": "start", "type": "start", "name": "Start"},
1149 {"id": "end", "type": "end", "name": "End"}
1150 ],
1151 "edges": [
1152 {"from": "start", "to": "end"}
1153 ]
1154 },
1155 "location": "project",
1156 "overwrite": true
1157 }))
1158 .await
1159 .unwrap();
1160
1161 let edit_result = tool
1163 .execute(json!({
1164 "mode": "edit",
1165 "workflow_id": "test-add-node-workflow",
1166 "edit_operation": "add_node",
1167 "edit_value": {
1168 "id": "new_task",
1169 "type": "task",
1170 "name": "New Task",
1171 "task": "process"
1172 }
1173 }))
1174 .await
1175 .unwrap();
1176
1177 assert!(edit_result.contains("updated successfully"));
1178
1179 let edge_result = tool
1181 .execute(json!({
1182 "mode": "edit",
1183 "workflow_id": "test-add-node-workflow",
1184 "edit_operation": "add_edge",
1185 "edit_value": {
1186 "from": "start",
1187 "to": "new_task"
1188 }
1189 }))
1190 .await
1191 .unwrap();
1192
1193 assert!(edge_result.contains("updated successfully"));
1194
1195 tool.execute(json!({
1197 "mode": "edit",
1198 "workflow_id": "test-add-node-workflow",
1199 "edit_operation": "add_edge",
1200 "edit_value": {
1201 "from": "new_task",
1202 "to": "end"
1203 }
1204 }))
1205 .await
1206 .unwrap();
1207
1208 let info_result = tool
1210 .execute(json!({
1211 "mode": "info",
1212 "workflow_id": "test-add-node-workflow"
1213 }))
1214 .await
1215 .unwrap();
1216
1217 assert!(info_result.contains("new_task"));
1218 assert!(info_result.contains("New Task"));
1219 assert!(info_result.contains("start → new_task"));
1220 assert!(info_result.contains("new_task → end"));
1221 }
1222
1223 #[tokio::test]
1224 async fn test_invalid_mode() {
1225 let tool = WorkflowCreateTool;
1226 let result = tool
1227 .execute(json!({
1228 "mode": "invalid"
1229 }))
1230 .await;
1231
1232 assert!(result.is_err());
1233 }
1234
1235 #[test]
1236 fn test_definition() {
1237 let tool = WorkflowCreateTool;
1238 let def = tool.definition();
1239
1240 assert_eq!(def.name, "workflow_create");
1241 assert!(def.description.contains("创建并保存"));
1242 assert!(def.parameters["properties"]["mode"].is_object());
1243 }
1244}