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