yamlgraph 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of yamlgraph might be problematic. Click here for more details.

Files changed (111) hide show
  1. examples/__init__.py +1 -0
  2. examples/storyboard/__init__.py +1 -0
  3. examples/storyboard/generate_videos.py +335 -0
  4. examples/storyboard/nodes/__init__.py +10 -0
  5. examples/storyboard/nodes/animated_character_node.py +248 -0
  6. examples/storyboard/nodes/animated_image_node.py +138 -0
  7. examples/storyboard/nodes/character_node.py +162 -0
  8. examples/storyboard/nodes/image_node.py +118 -0
  9. examples/storyboard/nodes/replicate_tool.py +238 -0
  10. examples/storyboard/retry_images.py +118 -0
  11. tests/__init__.py +1 -0
  12. tests/conftest.py +178 -0
  13. tests/integration/__init__.py +1 -0
  14. tests/integration/test_animated_storyboard.py +63 -0
  15. tests/integration/test_cli_commands.py +242 -0
  16. tests/integration/test_map_demo.py +50 -0
  17. tests/integration/test_memory_demo.py +281 -0
  18. tests/integration/test_pipeline_flow.py +105 -0
  19. tests/integration/test_providers.py +163 -0
  20. tests/integration/test_resume.py +75 -0
  21. tests/unit/__init__.py +1 -0
  22. tests/unit/test_agent_nodes.py +200 -0
  23. tests/unit/test_checkpointer.py +212 -0
  24. tests/unit/test_cli.py +121 -0
  25. tests/unit/test_cli_package.py +81 -0
  26. tests/unit/test_compile_graph_map.py +132 -0
  27. tests/unit/test_conditions_routing.py +253 -0
  28. tests/unit/test_config.py +93 -0
  29. tests/unit/test_conversation_memory.py +270 -0
  30. tests/unit/test_database.py +145 -0
  31. tests/unit/test_deprecation.py +104 -0
  32. tests/unit/test_executor.py +60 -0
  33. tests/unit/test_executor_async.py +179 -0
  34. tests/unit/test_export.py +150 -0
  35. tests/unit/test_expressions.py +178 -0
  36. tests/unit/test_format_prompt.py +145 -0
  37. tests/unit/test_generic_report.py +200 -0
  38. tests/unit/test_graph_commands.py +327 -0
  39. tests/unit/test_graph_loader.py +299 -0
  40. tests/unit/test_graph_schema.py +193 -0
  41. tests/unit/test_inline_schema.py +151 -0
  42. tests/unit/test_issues.py +164 -0
  43. tests/unit/test_jinja2_prompts.py +85 -0
  44. tests/unit/test_langsmith.py +319 -0
  45. tests/unit/test_llm_factory.py +109 -0
  46. tests/unit/test_llm_factory_async.py +118 -0
  47. tests/unit/test_loops.py +403 -0
  48. tests/unit/test_map_node.py +144 -0
  49. tests/unit/test_no_backward_compat.py +56 -0
  50. tests/unit/test_node_factory.py +225 -0
  51. tests/unit/test_prompts.py +166 -0
  52. tests/unit/test_python_nodes.py +198 -0
  53. tests/unit/test_reliability.py +298 -0
  54. tests/unit/test_result_export.py +234 -0
  55. tests/unit/test_router.py +296 -0
  56. tests/unit/test_sanitize.py +99 -0
  57. tests/unit/test_schema_loader.py +295 -0
  58. tests/unit/test_shell_tools.py +229 -0
  59. tests/unit/test_state_builder.py +331 -0
  60. tests/unit/test_state_builder_map.py +104 -0
  61. tests/unit/test_state_config.py +197 -0
  62. tests/unit/test_template.py +190 -0
  63. tests/unit/test_tool_nodes.py +129 -0
  64. yamlgraph/__init__.py +35 -0
  65. yamlgraph/builder.py +110 -0
  66. yamlgraph/cli/__init__.py +139 -0
  67. yamlgraph/cli/__main__.py +6 -0
  68. yamlgraph/cli/commands.py +232 -0
  69. yamlgraph/cli/deprecation.py +92 -0
  70. yamlgraph/cli/graph_commands.py +382 -0
  71. yamlgraph/cli/validators.py +37 -0
  72. yamlgraph/config.py +67 -0
  73. yamlgraph/constants.py +66 -0
  74. yamlgraph/error_handlers.py +226 -0
  75. yamlgraph/executor.py +275 -0
  76. yamlgraph/executor_async.py +122 -0
  77. yamlgraph/graph_loader.py +337 -0
  78. yamlgraph/map_compiler.py +138 -0
  79. yamlgraph/models/__init__.py +36 -0
  80. yamlgraph/models/graph_schema.py +141 -0
  81. yamlgraph/models/schemas.py +124 -0
  82. yamlgraph/models/state_builder.py +236 -0
  83. yamlgraph/node_factory.py +240 -0
  84. yamlgraph/routing.py +87 -0
  85. yamlgraph/schema_loader.py +160 -0
  86. yamlgraph/storage/__init__.py +17 -0
  87. yamlgraph/storage/checkpointer.py +72 -0
  88. yamlgraph/storage/database.py +320 -0
  89. yamlgraph/storage/export.py +269 -0
  90. yamlgraph/tools/__init__.py +1 -0
  91. yamlgraph/tools/agent.py +235 -0
  92. yamlgraph/tools/nodes.py +124 -0
  93. yamlgraph/tools/python_tool.py +178 -0
  94. yamlgraph/tools/shell.py +205 -0
  95. yamlgraph/utils/__init__.py +47 -0
  96. yamlgraph/utils/conditions.py +157 -0
  97. yamlgraph/utils/expressions.py +111 -0
  98. yamlgraph/utils/langsmith.py +308 -0
  99. yamlgraph/utils/llm_factory.py +118 -0
  100. yamlgraph/utils/llm_factory_async.py +105 -0
  101. yamlgraph/utils/logging.py +127 -0
  102. yamlgraph/utils/prompts.py +116 -0
  103. yamlgraph/utils/sanitize.py +98 -0
  104. yamlgraph/utils/template.py +102 -0
  105. yamlgraph/utils/validators.py +181 -0
  106. yamlgraph-0.1.1.dist-info/METADATA +854 -0
  107. yamlgraph-0.1.1.dist-info/RECORD +111 -0
  108. yamlgraph-0.1.1.dist-info/WHEEL +5 -0
  109. yamlgraph-0.1.1.dist-info/entry_points.txt +2 -0
  110. yamlgraph-0.1.1.dist-info/licenses/LICENSE +21 -0
  111. yamlgraph-0.1.1.dist-info/top_level.txt +3 -0
@@ -0,0 +1,116 @@
1
+ """Unified prompt loading and path resolution.
2
+
3
+ This module consolidates prompt loading logic used by executor.py
4
+ and node_factory.py into a single, testable module.
5
+
6
+ Search order for prompts:
7
+ 1. {prompts_dir}/{prompt_name}.yaml (standard location)
8
+ 2. {parent}/prompts/{basename}.yaml (external examples like examples/storyboard/...)
9
+ """
10
+
11
+ from pathlib import Path
12
+
13
+ import yaml
14
+
15
+ from yamlgraph.config import PROMPTS_DIR
16
+
17
+
18
+ def resolve_prompt_path(
19
+ prompt_name: str,
20
+ prompts_dir: Path | None = None,
21
+ ) -> Path:
22
+ """Resolve a prompt name to its full YAML file path.
23
+
24
+ Search order:
25
+ 1. prompts_dir/{prompt_name}.yaml (default: prompts/)
26
+ 2. {parent}/prompts/{basename}.yaml (for external examples)
27
+
28
+ Args:
29
+ prompt_name: Prompt name like "greet" or "examples/storyboard/expand_story"
30
+ prompts_dir: Base prompts directory (defaults to PROMPTS_DIR from config)
31
+
32
+ Returns:
33
+ Path to the YAML file
34
+
35
+ Raises:
36
+ FileNotFoundError: If prompt file doesn't exist
37
+
38
+ Examples:
39
+ >>> resolve_prompt_path("greet")
40
+ PosixPath('/path/to/prompts/greet.yaml')
41
+
42
+ >>> resolve_prompt_path("map-demo/generate_ideas")
43
+ PosixPath('/path/to/prompts/map-demo/generate_ideas.yaml')
44
+ """
45
+ if prompts_dir is None:
46
+ prompts_dir = PROMPTS_DIR
47
+
48
+ prompts_dir = Path(prompts_dir)
49
+
50
+ # Try standard location first: prompts_dir/{prompt_name}.yaml
51
+ yaml_path = prompts_dir / f"{prompt_name}.yaml"
52
+ if yaml_path.exists():
53
+ return yaml_path
54
+
55
+ # Try external example location: {parent}/prompts/{basename}.yaml
56
+ # e.g., "examples/storyboard/expand_story" -> "examples/storyboard/prompts/expand_story.yaml"
57
+ parts = prompt_name.rsplit("/", 1)
58
+ if len(parts) == 2:
59
+ parent_dir, basename = parts
60
+ alt_path = Path(parent_dir) / "prompts" / f"{basename}.yaml"
61
+ if alt_path.exists():
62
+ return alt_path
63
+
64
+ raise FileNotFoundError(f"Prompt not found: {yaml_path}")
65
+
66
+
67
+ def load_prompt(
68
+ prompt_name: str,
69
+ prompts_dir: Path | None = None,
70
+ ) -> dict:
71
+ """Load a YAML prompt template.
72
+
73
+ Args:
74
+ prompt_name: Name of the prompt file (without .yaml extension)
75
+ prompts_dir: Optional prompts directory override
76
+
77
+ Returns:
78
+ Dictionary with prompt content (typically 'system' and 'user' keys)
79
+
80
+ Raises:
81
+ FileNotFoundError: If prompt file doesn't exist
82
+ """
83
+ path = resolve_prompt_path(prompt_name, prompts_dir)
84
+
85
+ with open(path) as f:
86
+ return yaml.safe_load(f)
87
+
88
+
89
+ def load_prompt_path(
90
+ prompt_name: str,
91
+ prompts_dir: Path | None = None,
92
+ ) -> tuple[Path, dict]:
93
+ """Load a prompt and return both path and content.
94
+
95
+ Useful when you need both the file path (for schema loading)
96
+ and the content (for prompt execution).
97
+
98
+ Args:
99
+ prompt_name: Name of the prompt file (without .yaml extension)
100
+ prompts_dir: Optional prompts directory override
101
+
102
+ Returns:
103
+ Tuple of (path, content_dict)
104
+
105
+ Raises:
106
+ FileNotFoundError: If prompt file doesn't exist
107
+ """
108
+ path = resolve_prompt_path(prompt_name, prompts_dir)
109
+
110
+ with open(path) as f:
111
+ content = yaml.safe_load(f)
112
+
113
+ return path, content
114
+
115
+
116
+ __all__ = ["resolve_prompt_path", "load_prompt", "load_prompt_path"]
@@ -0,0 +1,98 @@
1
+ """Input sanitization utilities.
2
+
3
+ Provides functions for validating and sanitizing user input
4
+ to prevent prompt injection and other security issues.
5
+ """
6
+
7
+ import re
8
+ from typing import NamedTuple
9
+
10
+ from yamlgraph.config import DANGEROUS_PATTERNS, MAX_TOPIC_LENGTH
11
+
12
+
13
+ class SanitizationResult(NamedTuple):
14
+ """Result of input sanitization."""
15
+
16
+ value: str
17
+ is_safe: bool
18
+ warnings: list[str]
19
+
20
+
21
+ def sanitize_topic(topic: str) -> SanitizationResult:
22
+ """Sanitize a topic string for use in prompts.
23
+
24
+ Checks for:
25
+ - Length limits
26
+ - Potential prompt injection patterns
27
+ - Control characters
28
+
29
+ Args:
30
+ topic: The raw topic string
31
+
32
+ Returns:
33
+ SanitizationResult with cleaned value and safety status
34
+
35
+ Example:
36
+ >>> result = sanitize_topic("machine learning")
37
+ >>> result.is_safe
38
+ True
39
+ >>> result = sanitize_topic("ignore previous instructions")
40
+ >>> result.is_safe
41
+ False
42
+ """
43
+ warnings = []
44
+ cleaned = topic.strip()
45
+
46
+ # Check length
47
+ if len(cleaned) > MAX_TOPIC_LENGTH:
48
+ cleaned = cleaned[:MAX_TOPIC_LENGTH]
49
+ warnings.append(f"Topic truncated to {MAX_TOPIC_LENGTH} characters")
50
+
51
+ # Check for empty
52
+ if not cleaned:
53
+ return SanitizationResult(
54
+ value="",
55
+ is_safe=False,
56
+ warnings=["Topic cannot be empty"],
57
+ )
58
+
59
+ # Remove control characters (except newlines)
60
+ cleaned = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", "", cleaned)
61
+
62
+ # Check for dangerous patterns (case-insensitive)
63
+ topic_lower = cleaned.lower()
64
+ for pattern in DANGEROUS_PATTERNS:
65
+ if pattern.lower() in topic_lower:
66
+ return SanitizationResult(
67
+ value=cleaned,
68
+ is_safe=False,
69
+ warnings=[f"Topic contains potentially unsafe pattern: '{pattern}'"],
70
+ )
71
+
72
+ return SanitizationResult(
73
+ value=cleaned,
74
+ is_safe=True,
75
+ warnings=warnings,
76
+ )
77
+
78
+
79
+ def sanitize_variables(variables: dict) -> dict:
80
+ """Sanitize a dictionary of template variables.
81
+
82
+ Args:
83
+ variables: Dictionary of variable name -> value
84
+
85
+ Returns:
86
+ Sanitized dictionary with cleaned values
87
+ """
88
+ sanitized = {}
89
+
90
+ for key, value in variables.items():
91
+ if isinstance(value, str):
92
+ # Remove control characters but preserve newlines
93
+ cleaned = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", "", value)
94
+ sanitized[key] = cleaned
95
+ else:
96
+ sanitized[key] = value
97
+
98
+ return sanitized
@@ -0,0 +1,102 @@
1
+ """Template utilities - Variable extraction and validation.
2
+
3
+ This module provides functions to extract required variables from
4
+ prompt templates and validate that all required variables are provided
5
+ before execution.
6
+
7
+ Supports both simple {variable} placeholders and Jinja2 templates.
8
+ """
9
+
10
+ import logging
11
+ import re
12
+ from typing import Any
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def extract_variables(template: str) -> set[str]:
18
+ """Extract all variable names required by a template.
19
+
20
+ Handles both simple {var} and Jinja2 {{ var }}, {% for x in var %} syntax.
21
+
22
+ Args:
23
+ template: Template string with placeholders
24
+
25
+ Returns:
26
+ Set of variable names required by the template
27
+
28
+ Examples:
29
+ >>> extract_variables("Hello {name}")
30
+ {'name'}
31
+
32
+ >>> extract_variables("{% for item in items %}{{ item }}{% endfor %}")
33
+ {'items'}
34
+ """
35
+ variables: set[str] = set()
36
+
37
+ # Simple format: {var} - but NOT {{ (Jinja2)
38
+ # Match {word} but not {{word}} - use negative lookbehind/lookahead
39
+ simple_pattern = r"(?<!\{)\{(\w+)\}(?!\})"
40
+ variables.update(re.findall(simple_pattern, template))
41
+
42
+ # Jinja2 variable: {{ var }} or {{ var.field }}
43
+ jinja_var_pattern = r"\{\{\s*(\w+)"
44
+ variables.update(re.findall(jinja_var_pattern, template))
45
+
46
+ # Jinja2 loop: {% for x in var %}
47
+ jinja_loop_pattern = r"\{%\s*for\s+\w+\s+in\s+(\w+)"
48
+ variables.update(re.findall(jinja_loop_pattern, template))
49
+
50
+ # Jinja2 condition: {% if var %} or {% if var.field %}
51
+ jinja_if_pattern = r"\{%\s*if\s+(\w+)"
52
+ variables.update(re.findall(jinja_if_pattern, template))
53
+
54
+ # Remove loop iteration variables (they're not inputs)
55
+ # e.g., in "{% for item in items %}", "item" is not required
56
+ loop_iter_pattern = r"\{%\s*for\s+(\w+)\s+in"
57
+ loop_vars = set(re.findall(loop_iter_pattern, template))
58
+ variables -= loop_vars
59
+
60
+ # Remove common non-input variables
61
+ # - state: injected by node_factory
62
+ # - loop: Jinja2 loop context
63
+ # - range: Jinja2 builtin function
64
+ excluded = {"state", "loop", "range", "true", "false", "none"}
65
+ variables -= excluded
66
+
67
+ return variables
68
+
69
+
70
+ def validate_variables(
71
+ template: str,
72
+ provided: dict[str, Any],
73
+ prompt_name: str,
74
+ ) -> None:
75
+ """Validate that all required template variables are provided.
76
+
77
+ Raises ValueError with helpful message listing all missing variables.
78
+
79
+ Args:
80
+ template: Template string with placeholders
81
+ provided: Dictionary of provided variable values
82
+ prompt_name: Name of the prompt (for error messages)
83
+
84
+ Raises:
85
+ ValueError: If any required variables are missing
86
+
87
+ Examples:
88
+ >>> validate_variables("Hello {name}", {"name": "World"}, "greet")
89
+ # No error
90
+
91
+ >>> validate_variables("Hello {name}", {}, "greet")
92
+ ValueError: Missing required variable(s) for prompt 'greet': name
93
+ """
94
+ required = extract_variables(template)
95
+ provided_keys = set(provided.keys())
96
+ missing = required - provided_keys
97
+
98
+ if missing:
99
+ raise ValueError(
100
+ f"Missing required variable(s) for prompt '{prompt_name}': "
101
+ f"{', '.join(sorted(missing))}"
102
+ )
@@ -0,0 +1,181 @@
1
+ """Graph configuration validators.
2
+
3
+ Validation functions for YAML graph configuration structures.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from yamlgraph.constants import ErrorHandler, NodeType
9
+
10
+
11
+ def validate_required_sections(config: dict[str, Any]) -> None:
12
+ """Validate required top-level sections exist.
13
+
14
+ Args:
15
+ config: Parsed YAML configuration dictionary
16
+
17
+ Raises:
18
+ ValueError: If required sections are missing
19
+ """
20
+ if not config.get("nodes"):
21
+ raise ValueError("Graph config missing required 'nodes' section")
22
+ if not config.get("edges"):
23
+ raise ValueError("Graph config missing required 'edges' section")
24
+
25
+
26
+ def validate_node_prompt(node_name: str, node_config: dict[str, Any]) -> None:
27
+ """Validate node has required prompt if applicable.
28
+
29
+ Args:
30
+ node_name: Name of the node
31
+ node_config: Node configuration dictionary
32
+
33
+ Raises:
34
+ ValueError: If prompt is required but missing
35
+ """
36
+ node_type = node_config.get("type", NodeType.LLM)
37
+ # Only llm and router nodes require prompts
38
+ # tool, python, agent, and map nodes don't require prompts
39
+ if NodeType.requires_prompt(node_type) and not node_config.get("prompt"):
40
+ raise ValueError(f"Node '{node_name}' missing required 'prompt' field")
41
+
42
+
43
+ def validate_router_node(
44
+ node_name: str, node_config: dict[str, Any], all_nodes: dict[str, Any]
45
+ ) -> None:
46
+ """Validate router node has routes pointing to valid nodes.
47
+
48
+ Args:
49
+ node_name: Name of the node
50
+ node_config: Node configuration dictionary
51
+ all_nodes: All nodes in the graph for target validation
52
+
53
+ Raises:
54
+ ValueError: If router configuration is invalid
55
+ """
56
+ if node_config.get("type") != NodeType.ROUTER:
57
+ return
58
+
59
+ if not node_config.get("routes"):
60
+ raise ValueError(f"Router node '{node_name}' missing required 'routes' field")
61
+
62
+ for route_key, target_node in node_config["routes"].items():
63
+ if target_node not in all_nodes:
64
+ raise ValueError(
65
+ f"Router node '{node_name}' route '{route_key}' points to "
66
+ f"nonexistent node '{target_node}'"
67
+ )
68
+
69
+
70
+ def validate_edges(edges: list[dict[str, Any]]) -> None:
71
+ """Validate each edge has required from/to fields.
72
+
73
+ Args:
74
+ edges: List of edge configurations
75
+
76
+ Raises:
77
+ ValueError: If edge is missing required fields
78
+ """
79
+ for i, edge in enumerate(edges):
80
+ if "from" not in edge:
81
+ raise ValueError(f"Edge {i} missing required 'from' field")
82
+ if "to" not in edge:
83
+ raise ValueError(f"Edge {i} missing required 'to' field")
84
+
85
+ # Validate condition expressions at load time
86
+ if "condition" in edge:
87
+ validate_condition_expression(edge["condition"], i)
88
+
89
+
90
+ def validate_condition_expression(condition: str, edge_index: int) -> None:
91
+ """Validate a condition expression has valid syntax.
92
+
93
+ Performs compile-time validation of condition expressions to catch
94
+ syntax errors early rather than at runtime.
95
+
96
+ Supports expression conditions like "score < 0.8", "a.b >= 1 and c == 'done'"
97
+
98
+ Args:
99
+ condition: Condition expression like "score < 0.8"
100
+ edge_index: Edge index for error messages
101
+
102
+ Raises:
103
+ ValueError: If condition has invalid syntax
104
+ """
105
+ import re
106
+
107
+ # Expression syntax check - must match comparison pattern
108
+ # Valid: "score < 0.8", "a.b >= 1", "x == 'done'"
109
+ # Also valid: compound expressions "a > 1 and b < 2"
110
+ comparison_pattern = r"[a-zA-Z_][\w.]*\s*(<=|>=|==|!=|<|>)\s*.+"
111
+ compound_pattern = r"\s+(and|or)\s+"
112
+
113
+ # Split by and/or and validate each part
114
+ parts = re.split(compound_pattern, condition, flags=re.IGNORECASE)
115
+ # parts includes the 'and'/'or' tokens, so filter to just comparisons
116
+ comparisons = [p.strip() for p in parts if p.strip().lower() not in ("and", "or")]
117
+
118
+ for part in comparisons:
119
+ if not re.match(comparison_pattern, part.strip()):
120
+ raise ValueError(
121
+ f"Edge {edge_index} has invalid condition syntax: '{condition}'. "
122
+ f"Expected format: 'field <op> value' (e.g., 'score < 0.8')"
123
+ )
124
+
125
+
126
+ def validate_on_error(node_name: str, node_config: dict[str, Any]) -> None:
127
+ """Validate on_error value is valid.
128
+
129
+ Args:
130
+ node_name: Name of the node
131
+ node_config: Node configuration dictionary
132
+
133
+ Raises:
134
+ ValueError: If on_error value is invalid
135
+ """
136
+ on_error = node_config.get("on_error")
137
+ if on_error and on_error not in ErrorHandler.all_values():
138
+ raise ValueError(
139
+ f"Node '{node_name}' has invalid on_error value '{on_error}'. "
140
+ f"Valid values: {', '.join(ErrorHandler.all_values())}"
141
+ )
142
+
143
+
144
+ def validate_map_node(node_name: str, node_config: dict[str, Any]) -> None:
145
+ """Validate map node has required fields.
146
+
147
+ Args:
148
+ node_name: Name of the node
149
+ node_config: Node configuration dictionary
150
+
151
+ Raises:
152
+ ValueError: If map node configuration is invalid
153
+ """
154
+ if node_config.get("type") != NodeType.MAP:
155
+ return
156
+
157
+ required_fields = ["over", "as", "node", "collect"]
158
+ for field in required_fields:
159
+ if field not in node_config:
160
+ raise ValueError(f"Map node '{node_name}' missing required '{field}' field")
161
+
162
+
163
+ def validate_config(config: dict[str, Any]) -> None:
164
+ """Validate YAML configuration structure.
165
+
166
+ Args:
167
+ config: Parsed YAML dictionary
168
+
169
+ Raises:
170
+ ValueError: If required fields are missing or invalid
171
+ """
172
+ validate_required_sections(config)
173
+
174
+ nodes = config["nodes"]
175
+ for node_name, node_config in nodes.items():
176
+ validate_node_prompt(node_name, node_config)
177
+ validate_router_node(node_name, node_config, nodes)
178
+ validate_on_error(node_name, node_config)
179
+ validate_map_node(node_name, node_config)
180
+
181
+ validate_edges(config["edges"])