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,197 @@
1
+ """Tests for YAML state configuration parsing.
2
+
3
+ Tests the parse_state_config function and state: section handling
4
+ in build_state_class.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from yamlgraph.models.state_builder import (
10
+ TYPE_MAP,
11
+ build_state_class,
12
+ parse_state_config,
13
+ )
14
+
15
+
16
+ class TestParseStateConfig:
17
+ """Tests for parse_state_config function."""
18
+
19
+ def test_empty_config(self):
20
+ """Empty state config returns empty dict."""
21
+ result = parse_state_config({})
22
+ assert result == {}
23
+
24
+ def test_simple_str_type(self):
25
+ """Parse 'str' type."""
26
+ result = parse_state_config({"concept": "str"})
27
+ assert result == {"concept": str}
28
+
29
+ def test_simple_int_type(self):
30
+ """Parse 'int' type."""
31
+ result = parse_state_config({"count": "int"})
32
+ assert result == {"count": int}
33
+
34
+ def test_simple_float_type(self):
35
+ """Parse 'float' type."""
36
+ result = parse_state_config({"score": "float"})
37
+ assert result == {"score": float}
38
+
39
+ def test_simple_bool_type(self):
40
+ """Parse 'bool' type."""
41
+ result = parse_state_config({"enabled": "bool"})
42
+ assert result == {"enabled": bool}
43
+
44
+ def test_simple_list_type(self):
45
+ """Parse 'list' type."""
46
+ result = parse_state_config({"items": "list"})
47
+ assert result == {"items": list}
48
+
49
+ def test_simple_dict_type(self):
50
+ """Parse 'dict' type."""
51
+ result = parse_state_config({"metadata": "dict"})
52
+ assert result == {"metadata": dict}
53
+
54
+ def test_any_type(self):
55
+ """Parse 'any' type."""
56
+ result = parse_state_config({"data": "any"})
57
+ assert result == {"data": Any}
58
+
59
+ def test_type_aliases(self):
60
+ """Type aliases like 'string', 'integer', 'boolean' work."""
61
+ result = parse_state_config(
62
+ {
63
+ "name": "string",
64
+ "age": "integer",
65
+ "active": "boolean",
66
+ }
67
+ )
68
+ assert result == {"name": str, "age": int, "active": bool}
69
+
70
+ def test_case_insensitive(self):
71
+ """Type names are case-insensitive."""
72
+ result = parse_state_config(
73
+ {
74
+ "a": "STR",
75
+ "b": "Int",
76
+ "c": "FLOAT",
77
+ }
78
+ )
79
+ assert result == {"a": str, "b": int, "c": float}
80
+
81
+ def test_multiple_fields(self):
82
+ """Parse multiple fields."""
83
+ result = parse_state_config(
84
+ {
85
+ "concept": "str",
86
+ "count": "int",
87
+ "score": "float",
88
+ }
89
+ )
90
+ assert result == {"concept": str, "count": int, "score": float}
91
+
92
+ def test_unknown_type_defaults_to_any(self):
93
+ """Unknown type strings default to Any."""
94
+ result = parse_state_config({"custom": "unknown_type"})
95
+ assert result == {"custom": Any}
96
+
97
+ def test_non_string_value_defaults_to_any(self):
98
+ """Non-string values default to Any."""
99
+ result = parse_state_config(
100
+ {
101
+ "nested": {"type": "str"}, # Dict value, not string
102
+ "number": 123, # Int value, not string
103
+ }
104
+ )
105
+ assert result == {"nested": Any, "number": Any}
106
+
107
+
108
+ class TestTypeMap:
109
+ """Tests for TYPE_MAP constant."""
110
+
111
+ def test_all_basic_types_present(self):
112
+ """TYPE_MAP contains all basic Python types."""
113
+ assert "str" in TYPE_MAP
114
+ assert "int" in TYPE_MAP
115
+ assert "float" in TYPE_MAP
116
+ assert "bool" in TYPE_MAP
117
+ assert "list" in TYPE_MAP
118
+ assert "dict" in TYPE_MAP
119
+ assert "any" in TYPE_MAP
120
+
121
+ def test_aliases_present(self):
122
+ """TYPE_MAP contains common aliases."""
123
+ assert "string" in TYPE_MAP
124
+ assert "integer" in TYPE_MAP
125
+ assert "boolean" in TYPE_MAP
126
+
127
+
128
+ class TestBuildStateClassWithStateConfig:
129
+ """Tests for build_state_class with state: section."""
130
+
131
+ def test_state_section_adds_fields(self):
132
+ """State section fields are included in generated class."""
133
+ config = {
134
+ "state": {"concept": "str", "count": "int"},
135
+ "nodes": {},
136
+ "edges": [],
137
+ }
138
+ state_class = build_state_class(config)
139
+ annotations = state_class.__annotations__
140
+
141
+ assert "concept" in annotations
142
+ assert "count" in annotations
143
+
144
+ def test_state_section_empty(self):
145
+ """Empty state section doesn't break build."""
146
+ config = {
147
+ "state": {},
148
+ "nodes": {},
149
+ "edges": [],
150
+ }
151
+ state_class = build_state_class(config)
152
+ # Should still have base fields
153
+ assert "thread_id" in state_class.__annotations__
154
+
155
+ def test_state_section_missing(self):
156
+ """Missing state section is handled."""
157
+ config = {
158
+ "nodes": {},
159
+ "edges": [],
160
+ }
161
+ state_class = build_state_class(config)
162
+ # Should still have base fields
163
+ assert "thread_id" in state_class.__annotations__
164
+
165
+ def test_custom_field_overrides_common(self):
166
+ """Custom state field can override common field type."""
167
+ config = {
168
+ "state": {"topic": "int"}, # Override str default
169
+ "nodes": {},
170
+ "edges": [],
171
+ }
172
+ state_class = build_state_class(config)
173
+ # The custom field should be present
174
+ assert "topic" in state_class.__annotations__
175
+
176
+ def test_storyboard_example(self):
177
+ """Test storyboard-style config with concept field."""
178
+ config = {
179
+ "state": {"concept": "str"},
180
+ "nodes": {
181
+ "expand_story": {
182
+ "type": "llm",
183
+ "state_key": "story",
184
+ }
185
+ },
186
+ "edges": [],
187
+ }
188
+ state_class = build_state_class(config)
189
+ annotations = state_class.__annotations__
190
+
191
+ # Custom field from state section
192
+ assert "concept" in annotations
193
+ # Output field from node
194
+ assert "story" in annotations
195
+ # Base infrastructure fields
196
+ assert "thread_id" in annotations
197
+ assert "errors" in annotations
@@ -0,0 +1,190 @@
1
+ """Tests for yamlgraph.utils.template module - Variable extraction and validation."""
2
+
3
+ import pytest
4
+
5
+
6
+ class TestExtractVariables:
7
+ """Tests for extract_variables function."""
8
+
9
+ def test_extract_simple_variables(self):
10
+ """Should extract {var} placeholders."""
11
+ from yamlgraph.utils.template import extract_variables
12
+
13
+ template = "Hello {name}, your style is {style}."
14
+ variables = extract_variables(template)
15
+ assert variables == {"name", "style"}
16
+
17
+ def test_extract_single_variable(self):
18
+ """Should extract a single variable."""
19
+ from yamlgraph.utils.template import extract_variables
20
+
21
+ template = "Welcome {user}!"
22
+ variables = extract_variables(template)
23
+ assert variables == {"user"}
24
+
25
+ def test_extract_no_variables(self):
26
+ """Should return empty set when no variables."""
27
+ from yamlgraph.utils.template import extract_variables
28
+
29
+ template = "No variables here"
30
+ variables = extract_variables(template)
31
+ assert variables == set()
32
+
33
+ def test_extract_duplicate_variables(self):
34
+ """Should deduplicate variables."""
35
+ from yamlgraph.utils.template import extract_variables
36
+
37
+ template = "{name} and {name} again"
38
+ variables = extract_variables(template)
39
+ assert variables == {"name"}
40
+
41
+ def test_extract_jinja2_variable(self):
42
+ """Should extract {{ var }} Jinja2 variables."""
43
+ from yamlgraph.utils.template import extract_variables
44
+
45
+ template = "Hello {{ name }}!"
46
+ variables = extract_variables(template)
47
+ assert "name" in variables
48
+
49
+ def test_extract_jinja2_variable_with_field_access(self):
50
+ """Should extract base variable from {{ var.field }}."""
51
+ from yamlgraph.utils.template import extract_variables
52
+
53
+ template = "User: {{ user.name }}"
54
+ variables = extract_variables(template)
55
+ assert "user" in variables
56
+
57
+ def test_extract_jinja2_loop_variable(self):
58
+ """Should extract iterable from {% for x in items %}."""
59
+ from yamlgraph.utils.template import extract_variables
60
+
61
+ template = "{% for item in items %}{{ item.name }}{% endfor %}"
62
+ variables = extract_variables(template)
63
+ assert "items" in variables
64
+ # 'item' is a loop variable, not a required input
65
+ assert "item" not in variables
66
+
67
+ def test_extract_jinja2_if_variable(self):
68
+ """Should extract variable from {% if condition %}."""
69
+ from yamlgraph.utils.template import extract_variables
70
+
71
+ template = "{% if show_details %}Details here{% endif %}"
72
+ variables = extract_variables(template)
73
+ assert "show_details" in variables
74
+
75
+ def test_exclude_state_variable(self):
76
+ """State is injected by framework, not a required input."""
77
+ from yamlgraph.utils.template import extract_variables
78
+
79
+ template = "{{ state.topic }}"
80
+ variables = extract_variables(template)
81
+ # state is excluded - it's injected by node_factory
82
+ assert "state" not in variables
83
+
84
+ def test_exclude_jinja2_builtins(self):
85
+ """Should exclude Jinja2 builtins like loop, range."""
86
+ from yamlgraph.utils.template import extract_variables
87
+
88
+ template = "{% for i in range(10) %}{{ loop.index }}{% endfor %}"
89
+ variables = extract_variables(template)
90
+ assert "range" not in variables
91
+ assert "loop" not in variables
92
+
93
+ def test_mixed_simple_and_jinja2(self):
94
+ """Should handle templates mixing {var} and {{ var }}."""
95
+ from yamlgraph.utils.template import extract_variables
96
+
97
+ template = "Simple {name} and Jinja2 {{ topic }}"
98
+ variables = extract_variables(template)
99
+ assert "name" in variables
100
+ assert "topic" in variables
101
+
102
+
103
+ class TestValidateVariables:
104
+ """Tests for validate_variables function."""
105
+
106
+ def test_validate_all_provided(self):
107
+ """Should not raise when all variables provided."""
108
+ from yamlgraph.utils.template import validate_variables
109
+
110
+ template = "Hello {name}, style: {style}"
111
+ # Should not raise
112
+ validate_variables(template, {"name": "World", "style": "formal"}, "greet")
113
+
114
+ def test_validate_missing_single_variable(self):
115
+ """Should raise ValueError for single missing variable."""
116
+ from yamlgraph.utils.template import validate_variables
117
+
118
+ template = "Hello {name}, style: {style}"
119
+ with pytest.raises(ValueError, match="Missing required variable.*name"):
120
+ validate_variables(template, {"style": "formal"}, "greet")
121
+
122
+ def test_validate_missing_multiple_variables(self):
123
+ """Should list ALL missing variables in error."""
124
+ from yamlgraph.utils.template import validate_variables
125
+
126
+ template = "Hello {name}, style: {style}"
127
+ with pytest.raises(ValueError) as exc_info:
128
+ validate_variables(template, {}, "greet")
129
+ error_msg = str(exc_info.value)
130
+ assert "name" in error_msg
131
+ assert "style" in error_msg
132
+
133
+ def test_validate_extra_variables_ok(self):
134
+ """Should not raise when extra variables provided."""
135
+ from yamlgraph.utils.template import validate_variables
136
+
137
+ template = "Hello {name}"
138
+ # Should not raise - extra vars are fine
139
+ validate_variables(template, {"name": "World", "extra": "ignored"}, "greet")
140
+
141
+ def test_validate_prompt_name_in_error(self):
142
+ """Error message should include prompt name."""
143
+ from yamlgraph.utils.template import validate_variables
144
+
145
+ template = "Hello {name}"
146
+ with pytest.raises(ValueError, match="greet"):
147
+ validate_variables(template, {}, "greet")
148
+
149
+ def test_validate_empty_template(self):
150
+ """Should not raise for template without variables."""
151
+ from yamlgraph.utils.template import validate_variables
152
+
153
+ template = "No variables here"
154
+ # Should not raise
155
+ validate_variables(template, {}, "static")
156
+
157
+ def test_validate_jinja2_template(self):
158
+ """Should validate Jinja2 templates correctly."""
159
+ from yamlgraph.utils.template import validate_variables
160
+
161
+ template = "{% for item in items %}{{ item }}{% endfor %}"
162
+ with pytest.raises(ValueError, match="items"):
163
+ validate_variables(template, {}, "list_template")
164
+
165
+
166
+ class TestExecutePromptValidation:
167
+ """Integration tests for validation in execute_prompt."""
168
+
169
+ def test_execute_prompt_raises_on_missing_variable(self):
170
+ """Should raise clear error when required variable is missing."""
171
+ from yamlgraph.executor import execute_prompt
172
+
173
+ with pytest.raises(ValueError, match="Missing required variable.*name"):
174
+ execute_prompt(
175
+ prompt_name="greet",
176
+ variables={"style": "formal"}, # Missing 'name'
177
+ )
178
+
179
+ def test_execute_prompt_lists_all_missing_variables(self):
180
+ """Error should list ALL missing variables, not just first."""
181
+ from yamlgraph.executor import execute_prompt
182
+
183
+ with pytest.raises(ValueError) as exc_info:
184
+ execute_prompt(
185
+ prompt_name="greet",
186
+ variables={}, # Missing both 'name' and 'style'
187
+ )
188
+ error_msg = str(exc_info.value)
189
+ assert "name" in error_msg
190
+ assert "style" in error_msg
@@ -0,0 +1,129 @@
1
+ """Tests for tool nodes (type: tool)."""
2
+
3
+ import pytest
4
+
5
+ from yamlgraph.tools.nodes import create_tool_node
6
+ from yamlgraph.tools.shell import ShellToolConfig
7
+
8
+
9
+ class TestCreateToolNode:
10
+ """Tests for create_tool_node function."""
11
+
12
+ def test_executes_named_tool(self):
13
+ """Node runs correct tool from registry."""
14
+ tools = {
15
+ "echo_tool": ShellToolConfig(command="echo hello"),
16
+ "other_tool": ShellToolConfig(command="echo other"),
17
+ }
18
+ node_config = {"tool": "echo_tool"}
19
+
20
+ node_fn = create_tool_node("test_node", node_config, tools)
21
+ result = node_fn({})
22
+
23
+ assert result["test_node"].strip() == "hello"
24
+ assert result["current_step"] == "test_node"
25
+
26
+ def test_resolves_variables_from_state(self):
27
+ """State values passed to tool."""
28
+ tools = {
29
+ "greet": ShellToolConfig(command="echo Hello {name}"),
30
+ }
31
+ node_config = {
32
+ "tool": "greet",
33
+ "variables": {"name": "{state.user_name}"},
34
+ }
35
+
36
+ node_fn = create_tool_node("greet_node", node_config, tools)
37
+ result = node_fn({"user_name": "Alice"})
38
+
39
+ assert "Alice" in result["greet_node"]
40
+
41
+ def test_stores_result_in_state_key(self):
42
+ """Tool output saved to custom state_key."""
43
+ tools = {
44
+ "data_tool": ShellToolConfig(command="echo data_value"),
45
+ }
46
+ node_config = {
47
+ "tool": "data_tool",
48
+ "state_key": "my_data",
49
+ }
50
+
51
+ node_fn = create_tool_node("fetch_node", node_config, tools)
52
+ result = node_fn({})
53
+
54
+ assert "my_data" in result
55
+ assert result["my_data"].strip() == "data_value"
56
+
57
+ def test_on_error_skip(self):
58
+ """Failed tool skipped when on_error: skip."""
59
+ tools = {
60
+ "fail_tool": ShellToolConfig(command="exit 1"),
61
+ }
62
+ node_config = {
63
+ "tool": "fail_tool",
64
+ "on_error": "skip",
65
+ }
66
+
67
+ node_fn = create_tool_node("fail_node", node_config, tools)
68
+ result = node_fn({})
69
+
70
+ # Should not raise, should return with error info
71
+ assert result["current_step"] == "fail_node"
72
+ assert "errors" in result or result.get("fail_node") is None
73
+
74
+ def test_on_error_fail_raises(self):
75
+ """Failed tool raises when on_error: fail."""
76
+ tools = {
77
+ "fail_tool": ShellToolConfig(command="exit 1"),
78
+ }
79
+ node_config = {
80
+ "tool": "fail_tool",
81
+ "on_error": "fail",
82
+ }
83
+
84
+ node_fn = create_tool_node("fail_node", node_config, tools)
85
+
86
+ with pytest.raises(RuntimeError):
87
+ node_fn({})
88
+
89
+ def test_nested_state_variable(self):
90
+ """Nested state values like {state.location.lat} resolved."""
91
+ tools = {
92
+ "geo": ShellToolConfig(command="echo {lat},{lon}"),
93
+ }
94
+ node_config = {
95
+ "tool": "geo",
96
+ "variables": {
97
+ "lat": "{state.location.lat}",
98
+ "lon": "{state.location.lon}",
99
+ },
100
+ }
101
+
102
+ node_fn = create_tool_node("geo_node", node_config, tools)
103
+ result = node_fn({"location": {"lat": "37.7749", "lon": "-122.4194"}})
104
+
105
+ assert "37.7749" in result["geo_node"]
106
+ assert "-122.4194" in result["geo_node"]
107
+
108
+ def test_missing_tool_raises(self):
109
+ """Unknown tool name raises error."""
110
+ tools = {}
111
+ node_config = {"tool": "nonexistent"}
112
+
113
+ with pytest.raises(KeyError):
114
+ create_tool_node("bad_node", node_config, tools)
115
+
116
+ def test_json_parse_tool(self):
117
+ """Tool with parse: json returns dict."""
118
+ tools = {
119
+ "json_tool": ShellToolConfig(
120
+ command="echo '{{\"count\": 42}}'",
121
+ parse="json",
122
+ ),
123
+ }
124
+ node_config = {"tool": "json_tool"}
125
+
126
+ node_fn = create_tool_node("json_node", node_config, tools)
127
+ result = node_fn({})
128
+
129
+ assert result["json_node"] == {"count": 42}
yamlgraph/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ """YamlGraph - YAML-first LLM pipeline framework.
2
+
3
+ Framework for building LLM pipelines with YAML configuration.
4
+ State is generated dynamically from graph config.
5
+ """
6
+
7
+ from yamlgraph.builder import build_graph, build_resume_graph, run_pipeline
8
+ from yamlgraph.executor import execute_prompt, get_executor
9
+ from yamlgraph.models import (
10
+ ErrorType,
11
+ GenericReport,
12
+ PipelineError,
13
+ build_state_class,
14
+ create_initial_state,
15
+ )
16
+ from yamlgraph.storage import YamlGraphDB
17
+
18
+ __all__ = [
19
+ # Builder
20
+ "build_graph",
21
+ "build_resume_graph",
22
+ "run_pipeline",
23
+ # Executor
24
+ "execute_prompt",
25
+ "get_executor",
26
+ # Framework models
27
+ "ErrorType",
28
+ "PipelineError",
29
+ "GenericReport",
30
+ # Dynamic state
31
+ "build_state_class",
32
+ "create_initial_state",
33
+ # Storage
34
+ "YamlGraphDB",
35
+ ]
yamlgraph/builder.py ADDED
@@ -0,0 +1,110 @@
1
+ """Graph builders for yamlgraph pipelines.
2
+
3
+ Provides functions to build pipeline graphs from YAML configuration.
4
+
5
+ Pipeline Architecture
6
+ =====================
7
+
8
+ The main pipeline follows this flow:
9
+
10
+ ```mermaid
11
+ graph LR
12
+ A[generate] -->|content| B{should_continue}
13
+ B -->|continue| C[analyze]
14
+ B -->|end| E[END]
15
+ C -->|analysis| D[summarize]
16
+ D --> E[END]
17
+ ```
18
+
19
+ State Flow:
20
+ - generate: Creates structured content from topic
21
+ - analyze: Produces analysis from generated content
22
+ - summarize: Combines all outputs into final_summary
23
+
24
+ Graph Definition:
25
+ - Pipelines are defined in graphs/*.yaml
26
+ - Loaded and compiled via graph_loader module
27
+ """
28
+
29
+ from pathlib import Path
30
+ from typing import Any
31
+
32
+ from langgraph.graph import StateGraph
33
+
34
+ from yamlgraph.config import DEFAULT_GRAPH
35
+ from yamlgraph.graph_loader import load_and_compile
36
+ from yamlgraph.models import create_initial_state
37
+
38
+ # Type alias for dynamic state
39
+ GraphState = dict[str, Any]
40
+
41
+
42
+ def build_graph(
43
+ graph_path: Path | str | None = None,
44
+ checkpointer: Any | None = None,
45
+ ) -> StateGraph:
46
+ """Build a pipeline graph from YAML with optional checkpointer.
47
+
48
+ Args:
49
+ graph_path: Path to YAML graph definition.
50
+ Defaults to graphs/yamlgraph.yaml
51
+ checkpointer: Optional LangGraph checkpointer for state persistence.
52
+ Use get_checkpointer() from storage.checkpointer.
53
+
54
+ Returns:
55
+ StateGraph ready for compilation
56
+ """
57
+ path = Path(graph_path) if graph_path else DEFAULT_GRAPH
58
+ graph = load_and_compile(path)
59
+
60
+ # Checkpointer is applied at compile time
61
+ if checkpointer is not None:
62
+ # Store reference for compile() to use
63
+ graph._checkpointer = checkpointer
64
+
65
+ return graph
66
+
67
+
68
+ def build_resume_graph() -> StateGraph:
69
+ """Build a graph for resuming an interrupted pipeline.
70
+
71
+ This is an alias for build_graph(). Resume works automatically
72
+ because nodes skip execution if their output already exists in state
73
+ (skip_if_exists behavior).
74
+
75
+ To resume:
76
+ 1. Load saved state from database
77
+ 2. Invoke graph with that state
78
+ 3. Nodes with existing outputs are skipped
79
+
80
+ Returns:
81
+ StateGraph for resume (same as main pipeline)
82
+ """
83
+ return build_graph()
84
+
85
+
86
+ def run_pipeline(
87
+ topic: str,
88
+ style: str = "informative",
89
+ word_count: int = 300,
90
+ graph_path: Path | str | None = None,
91
+ ) -> GraphState:
92
+ """Run the complete pipeline with given inputs.
93
+
94
+ Args:
95
+ topic: Topic to generate content about
96
+ style: Writing style
97
+ word_count: Target word count
98
+ graph_path: Optional path to graph YAML
99
+
100
+ Returns:
101
+ Final state with all outputs
102
+ """
103
+ graph = build_graph(graph_path).compile()
104
+ initial_state = create_initial_state(
105
+ topic=topic,
106
+ style=style,
107
+ word_count=word_count,
108
+ )
109
+
110
+ return graph.invoke(initial_state)