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,281 @@
1
+ """Tests for Memory Demo - Multi-turn conversation with persistence.
2
+
3
+ Tests the memory features from Section 6 working together:
4
+ - Checkpointer for state persistence
5
+ - Message accumulation via dynamic state (Annotated reducers)
6
+ - Tool results storage
7
+ - Result export
8
+ """
9
+
10
+ import json
11
+ from pathlib import Path
12
+ from unittest.mock import MagicMock, patch
13
+
14
+ from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
15
+
16
+
17
+ class TestMemoryDemoGraphConfig:
18
+ """Tests for memory-demo.yaml graph configuration."""
19
+
20
+ def test_graph_config_exists(self):
21
+ """Graph config file exists."""
22
+ config_path = Path("graphs/memory-demo.yaml")
23
+ assert config_path.exists(), "graphs/memory-demo.yaml should exist"
24
+
25
+ def test_graph_config_loads(self):
26
+ """Graph config loads without errors."""
27
+ from yamlgraph.graph_loader import load_graph_config
28
+
29
+ config = load_graph_config("graphs/memory-demo.yaml")
30
+ assert config.name == "memory_demo"
31
+
32
+ def test_graph_has_agent_node(self):
33
+ """Graph includes an agent node."""
34
+ from yamlgraph.graph_loader import load_graph_config
35
+
36
+ config = load_graph_config("graphs/memory-demo.yaml")
37
+ assert "review" in config.nodes
38
+ assert config.nodes["review"]["type"] == "agent"
39
+
40
+ def test_graph_has_tools(self):
41
+ """Graph defines git tools."""
42
+ from yamlgraph.graph_loader import load_graph_config
43
+
44
+ config = load_graph_config("graphs/memory-demo.yaml")
45
+ tools = config.tools or {}
46
+ assert "git_log" in tools
47
+ assert "git_diff" in tools
48
+
49
+
50
+ class TestCodeReviewPrompt:
51
+ """Tests for code_review.yaml prompt."""
52
+
53
+ def test_prompt_file_exists(self):
54
+ """Prompt file exists."""
55
+ prompt_path = Path("prompts/code_review.yaml")
56
+ assert prompt_path.exists(), "prompts/code_review.yaml should exist"
57
+
58
+ def test_prompt_loads(self):
59
+ """Prompt loads with system and user templates."""
60
+ from yamlgraph.executor import load_prompt
61
+
62
+ prompt = load_prompt("code_review")
63
+ assert "system" in prompt
64
+ assert "user" in prompt
65
+
66
+
67
+ class TestCheckpointerIntegration:
68
+ """Tests for checkpointer integration with graph builder."""
69
+
70
+ def test_build_graph_accepts_checkpointer(self):
71
+ """build_graph accepts optional checkpointer parameter."""
72
+ import tempfile
73
+
74
+ from yamlgraph.builder import build_graph
75
+ from yamlgraph.storage.checkpointer import get_checkpointer
76
+
77
+ with tempfile.NamedTemporaryFile(suffix=".db") as f:
78
+ checkpointer = get_checkpointer(f.name)
79
+ # Should not raise
80
+ graph = build_graph(checkpointer=checkpointer)
81
+ assert graph is not None
82
+
83
+ def test_graph_with_checkpointer_accepts_thread_id(self):
84
+ """Graph with checkpointer can be invoked with thread_id."""
85
+ import tempfile
86
+
87
+ from yamlgraph.builder import build_graph
88
+ from yamlgraph.storage.checkpointer import get_checkpointer
89
+
90
+ with tempfile.NamedTemporaryFile(suffix=".db") as f:
91
+ checkpointer = get_checkpointer(f.name)
92
+ _graph = build_graph(checkpointer=checkpointer) # noqa: F841
93
+
94
+ # Should accept configurable with thread_id
95
+ config = {"configurable": {"thread_id": "test-123"}}
96
+ # Just verify config structure is valid
97
+ assert "thread_id" in config["configurable"]
98
+
99
+
100
+ class TestCLIThreadFlag:
101
+ """Tests for CLI --thread flag."""
102
+
103
+ def test_graph_run_cli_has_thread_argument(self):
104
+ """Graph run CLI accepts --thread argument."""
105
+ from yamlgraph.cli import create_parser
106
+
107
+ parser = create_parser()
108
+ # Parse with thread flag
109
+ args = parser.parse_args(
110
+ ["graph", "run", "graphs/yamlgraph.yaml", "--thread", "abc123"]
111
+ )
112
+ assert args.thread == "abc123"
113
+
114
+ def test_graph_run_thread_defaults_to_none(self):
115
+ """Thread defaults to None when not specified."""
116
+ from yamlgraph.cli import create_parser
117
+
118
+ parser = create_parser()
119
+ args = parser.parse_args(["graph", "run", "graphs/yamlgraph.yaml"])
120
+ assert args.thread is None
121
+
122
+
123
+ class TestCLIExportFlag:
124
+ """Tests for CLI --export flag."""
125
+
126
+ def test_graph_run_cli_has_export_argument(self):
127
+ """Graph run CLI accepts --export flag."""
128
+ from yamlgraph.cli import create_parser
129
+
130
+ parser = create_parser()
131
+ args = parser.parse_args(["graph", "run", "graphs/yamlgraph.yaml", "--export"])
132
+ assert args.export is True
133
+
134
+ def test_graph_run_export_defaults_to_false(self):
135
+ """Export defaults to False when not specified."""
136
+ from yamlgraph.cli import create_parser
137
+
138
+ parser = create_parser()
139
+ args = parser.parse_args(["graph", "run", "graphs/yamlgraph.yaml"])
140
+ assert args.export is False
141
+
142
+
143
+ class TestMemoryDemoEndToEnd:
144
+ """End-to-end tests for memory demo (mocked LLM)."""
145
+
146
+ def test_single_turn_returns_messages(self):
147
+ """Single turn execution returns messages in state."""
148
+ from yamlgraph.tools.agent import create_agent_node
149
+ from yamlgraph.tools.shell import ShellToolConfig
150
+
151
+ mock_response = AIMessage(content="Here are the recent commits...")
152
+
153
+ mock_llm = MagicMock()
154
+ mock_llm.bind_tools.return_value = mock_llm
155
+ mock_llm.invoke.return_value = mock_response
156
+
157
+ tool_config = ShellToolConfig(
158
+ command="git log --oneline -n {count}",
159
+ description="Get recent commits",
160
+ )
161
+
162
+ with patch("yamlgraph.tools.agent.create_llm", return_value=mock_llm):
163
+ node_fn = create_agent_node(
164
+ "review",
165
+ {
166
+ "tools": ["git_log"],
167
+ "state_key": "response",
168
+ "tool_results_key": "_tool_results",
169
+ },
170
+ {"git_log": tool_config},
171
+ )
172
+ result = node_fn({"input": "Show recent commits"})
173
+
174
+ assert "messages" in result
175
+ assert "response" in result
176
+
177
+ def test_multi_turn_preserves_history(self):
178
+ """Multi-turn conversation preserves message history."""
179
+ from yamlgraph.tools.agent import create_agent_node
180
+
181
+ mock_response = AIMessage(content="Based on our previous discussion...")
182
+
183
+ mock_llm = MagicMock()
184
+ mock_llm.bind_tools.return_value = mock_llm
185
+ mock_llm.invoke.return_value = mock_response
186
+
187
+ # Simulate existing conversation
188
+ existing_messages = [
189
+ SystemMessage(content="You are a code review assistant."),
190
+ HumanMessage(content="Show commits"),
191
+ AIMessage(content="Here are 5 commits..."),
192
+ ]
193
+
194
+ with patch("yamlgraph.tools.agent.create_llm", return_value=mock_llm):
195
+ node_fn = create_agent_node(
196
+ "review",
197
+ {"tools": [], "state_key": "response"},
198
+ {},
199
+ )
200
+ result = node_fn(
201
+ {
202
+ "input": "What about tests?",
203
+ "messages": existing_messages,
204
+ }
205
+ )
206
+
207
+ # New messages should be returned
208
+ assert len(result["messages"]) >= 2 # At least human + AI
209
+
210
+ def test_tool_results_stored_in_state(self):
211
+ """Tool execution results are stored in state."""
212
+ from yamlgraph.tools.agent import create_agent_node
213
+ from yamlgraph.tools.shell import ShellToolConfig
214
+
215
+ tool_response = AIMessage(
216
+ content="",
217
+ tool_calls=[{"name": "git_log", "args": {"count": "5"}, "id": "call_1"}],
218
+ )
219
+ final_response = AIMessage(content="Found 5 commits")
220
+
221
+ mock_llm = MagicMock()
222
+ mock_llm.bind_tools.return_value = mock_llm
223
+ mock_llm.invoke.side_effect = [tool_response, final_response]
224
+
225
+ tool_config = ShellToolConfig(
226
+ command="git log --oneline -n {count}",
227
+ description="Get recent commits",
228
+ )
229
+
230
+ with patch("yamlgraph.tools.agent.create_llm", return_value=mock_llm):
231
+ with patch("yamlgraph.tools.agent.execute_shell_tool") as mock_exec:
232
+ mock_exec.return_value = MagicMock(
233
+ success=True, output="abc123 First commit\ndef456 Second commit"
234
+ )
235
+
236
+ node_fn = create_agent_node(
237
+ "review",
238
+ {
239
+ "tools": ["git_log"],
240
+ "state_key": "response",
241
+ "tool_results_key": "_tool_results",
242
+ },
243
+ {"git_log": tool_config},
244
+ )
245
+ result = node_fn({"input": "Show commits"})
246
+
247
+ assert "_tool_results" in result
248
+ assert len(result["_tool_results"]) == 1
249
+ assert result["_tool_results"][0]["tool"] == "git_log"
250
+
251
+ def test_export_creates_files(self, tmp_path: Path):
252
+ """Export flag creates output files."""
253
+ from yamlgraph.storage.export import export_result
254
+
255
+ state = {
256
+ "thread_id": "demo-123",
257
+ "response": "# Code Review Summary\n\nFound 5 commits.",
258
+ "_tool_results": [
259
+ {"tool": "git_log", "args": {"count": "5"}, "output": "..."}
260
+ ],
261
+ }
262
+
263
+ config = {
264
+ "response": {"format": "markdown", "filename": "review.md"},
265
+ "_tool_results": {"format": "json", "filename": "tool_outputs.json"},
266
+ }
267
+
268
+ paths = export_result(state, config, base_path=tmp_path)
269
+
270
+ assert len(paths) == 2
271
+
272
+ # Check markdown file
273
+ md_path = tmp_path / "demo-123" / "review.md"
274
+ assert md_path.exists()
275
+ assert "Code Review Summary" in md_path.read_text()
276
+
277
+ # Check JSON file
278
+ json_path = tmp_path / "demo-123" / "tool_outputs.json"
279
+ assert json_path.exists()
280
+ data = json.loads(json_path.read_text())
281
+ assert data[0]["tool"] == "git_log"
@@ -0,0 +1,105 @@
1
+ """Integration tests for the complete pipeline flow."""
2
+
3
+ from unittest.mock import patch
4
+
5
+ import pytest
6
+
7
+ from tests.conftest import FixtureAnalysis, FixtureGeneratedContent
8
+ from yamlgraph.builder import build_graph, build_resume_graph, run_pipeline
9
+
10
+
11
+ class TestBuildGraph:
12
+ """Tests for build_graph function."""
13
+
14
+ def test_graph_compiles(self):
15
+ """Graph should compile without errors."""
16
+ graph = build_graph()
17
+ compiled = graph.compile()
18
+ assert compiled is not None
19
+
20
+ def test_graph_has_expected_nodes(self):
21
+ """Graph should have generate, analyze, summarize nodes."""
22
+ graph = build_graph()
23
+ # StateGraph stores nodes internally
24
+ assert "generate" in graph.nodes
25
+ assert "analyze" in graph.nodes
26
+ assert "summarize" in graph.nodes
27
+
28
+
29
+ class TestBuildResumeGraph:
30
+ """Tests for build_resume_graph function.
31
+
32
+ Resume works via skip_if_exists: nodes skip LLM calls if output exists in state.
33
+ """
34
+
35
+ def test_resume_graph_loads_full_pipeline(self):
36
+ """Resume graph loads the full pipeline (same as main graph)."""
37
+ graph = build_resume_graph()
38
+ # All nodes present - skip_if_exists handles resume logic
39
+ assert "generate" in graph.nodes
40
+ assert "analyze" in graph.nodes
41
+ assert "summarize" in graph.nodes
42
+
43
+ def test_resume_graph_same_as_main(self):
44
+ """Resume graph is identical to main graph."""
45
+ main_graph = build_graph()
46
+ resume_graph = build_resume_graph()
47
+
48
+ assert set(main_graph.nodes.keys()) == set(resume_graph.nodes.keys())
49
+
50
+
51
+ class TestRunPipeline:
52
+ """Tests for run_pipeline function with mocked LLM."""
53
+
54
+ @patch("yamlgraph.node_factory.execute_prompt")
55
+ def test_full_pipeline_success(self, mock_execute):
56
+ """Full pipeline should execute all steps."""
57
+ # Setup mock returns for each call
58
+ mock_generated = FixtureGeneratedContent(
59
+ title="Test Title",
60
+ content="Test content " * 50,
61
+ word_count=100,
62
+ tags=["test"],
63
+ )
64
+ mock_analysis = FixtureAnalysis(
65
+ summary="Test summary",
66
+ key_points=["Point 1"],
67
+ sentiment="positive",
68
+ confidence=0.9,
69
+ )
70
+ mock_summary = "Final test summary"
71
+
72
+ mock_execute.side_effect = [mock_generated, mock_analysis, mock_summary]
73
+
74
+ result = run_pipeline(topic="test", style="informative", word_count=100)
75
+
76
+ assert result["generated"] == mock_generated
77
+ assert result["analysis"] == mock_analysis
78
+ assert result["final_summary"] == mock_summary
79
+ assert mock_execute.call_count == 3
80
+
81
+ @patch("yamlgraph.node_factory.execute_prompt")
82
+ def test_pipeline_stops_on_generate_error(self, mock_execute):
83
+ """Pipeline should stop and raise exception on generate failure."""
84
+ mock_execute.side_effect = Exception("API Error")
85
+
86
+ with pytest.raises(Exception) as exc_info:
87
+ run_pipeline(topic="test")
88
+
89
+ assert "API Error" in str(exc_info.value)
90
+
91
+ @patch("yamlgraph.node_factory.execute_prompt")
92
+ def test_pipeline_state_progression(self, mock_execute):
93
+ """Pipeline should update current_step as it progresses."""
94
+ mock_generated = FixtureGeneratedContent(
95
+ title="Test", content="Content", word_count=1, tags=[]
96
+ )
97
+ mock_analysis = FixtureAnalysis(
98
+ summary="Summary", key_points=[], sentiment="neutral", confidence=0.5
99
+ )
100
+ mock_execute.side_effect = [mock_generated, mock_analysis, "Summary"]
101
+
102
+ result = run_pipeline(topic="test")
103
+
104
+ # Final step should be summarize
105
+ assert result["current_step"] == "summarize"
@@ -0,0 +1,163 @@
1
+ """Integration tests for multi-provider LLM support."""
2
+
3
+ import os
4
+ from unittest.mock import patch
5
+
6
+ import pytest
7
+ from pydantic import BaseModel, Field
8
+
9
+ from yamlgraph.executor import execute_prompt, load_prompt
10
+ from yamlgraph.utils.llm_factory import clear_cache
11
+
12
+
13
+ class ProviderTestContent(BaseModel):
14
+ """Test model for provider tests - replaces demo model dependency."""
15
+
16
+ title: str = Field(description="Title of the generated content")
17
+ content: str = Field(description="The main generated text")
18
+ word_count: int = Field(description="Approximate word count")
19
+ tags: list[str] = Field(default_factory=list, description="Relevant tags")
20
+
21
+
22
+ class TestProviderIntegration:
23
+ """Test multi-provider functionality end-to-end."""
24
+
25
+ def setup_method(self):
26
+ """Clear LLM cache before each test."""
27
+ clear_cache()
28
+
29
+ def test_execute_prompt_with_anthropic_provider(self):
30
+ """Should execute prompt with explicit Anthropic provider."""
31
+ result = execute_prompt(
32
+ prompt_name="greet",
33
+ variables={"name": "Alice", "style": "formal"},
34
+ provider="anthropic",
35
+ )
36
+ assert isinstance(result, str)
37
+ assert len(result) > 0
38
+
39
+ @pytest.mark.skipif(
40
+ not os.getenv("MISTRAL_API_KEY"), reason="MISTRAL_API_KEY not set"
41
+ )
42
+ def test_execute_prompt_with_mistral_provider(self):
43
+ """Should execute prompt with Mistral provider."""
44
+ result = execute_prompt(
45
+ prompt_name="greet",
46
+ variables={"name": "Bob", "style": "casual"},
47
+ provider="mistral",
48
+ )
49
+ assert isinstance(result, str)
50
+ assert len(result) > 0
51
+
52
+ @pytest.mark.skipif(
53
+ not os.getenv("OPENAI_API_KEY"), reason="OPENAI_API_KEY not set"
54
+ )
55
+ def test_execute_prompt_with_openai_provider(self):
56
+ """Should execute prompt with OpenAI provider."""
57
+ result = execute_prompt(
58
+ prompt_name="greet",
59
+ variables={"name": "Charlie", "style": "friendly"},
60
+ provider="openai",
61
+ )
62
+ assert isinstance(result, str)
63
+ assert len(result) > 0
64
+
65
+ def test_provider_from_environment_variable(self):
66
+ """Should use provider from PROVIDER env var."""
67
+ with patch.dict(os.environ, {"PROVIDER": "anthropic"}):
68
+ result = execute_prompt(
69
+ prompt_name="greet",
70
+ variables={"name": "Dave", "style": "formal"},
71
+ )
72
+ assert isinstance(result, str)
73
+ assert len(result) > 0
74
+
75
+ def test_provider_in_yaml_metadata(self):
76
+ """Should extract provider from YAML metadata."""
77
+ # greet.yaml doesn't have provider metadata,
78
+ # so this just verifies the load works
79
+ _ = load_prompt("greet")
80
+
81
+ # Even though greet.yaml doesn't have provider,
82
+ # the executor should handle it gracefully
83
+ result = execute_prompt(
84
+ prompt_name="greet",
85
+ variables={"name": "Eve", "style": "casual"},
86
+ )
87
+ assert isinstance(result, str)
88
+
89
+ def test_structured_output_with_different_providers(self):
90
+ """Should work with structured outputs across providers."""
91
+ result = execute_prompt(
92
+ prompt_name="generate",
93
+ variables={
94
+ "topic": "Python testing",
95
+ "style": "technical",
96
+ "word_count": 50,
97
+ },
98
+ output_model=ProviderTestContent,
99
+ provider="anthropic",
100
+ )
101
+ assert isinstance(result, ProviderTestContent)
102
+ assert result.content
103
+ assert isinstance(result.tags, list)
104
+
105
+ def test_temperature_and_provider_together(self):
106
+ """Should handle both temperature and provider parameters."""
107
+ result = execute_prompt(
108
+ prompt_name="greet",
109
+ variables={"name": "Frank", "style": "formal"},
110
+ temperature=0.3,
111
+ provider="anthropic",
112
+ )
113
+ assert isinstance(result, str)
114
+ assert len(result) > 0
115
+
116
+ def test_invalid_provider_raises_error(self):
117
+ """Should raise error for invalid provider."""
118
+ with pytest.raises(ValueError, match="Invalid provider"):
119
+ execute_prompt(
120
+ prompt_name="greet",
121
+ variables={"name": "Greg", "style": "casual"},
122
+ provider="invalid-provider",
123
+ )
124
+
125
+ def test_caching_across_calls_with_same_provider(self):
126
+ """Should reuse LLM instances for same provider/temperature."""
127
+ # First call
128
+ result1 = execute_prompt(
129
+ prompt_name="greet",
130
+ variables={"name": "Harry", "style": "formal"},
131
+ temperature=0.7,
132
+ provider="anthropic",
133
+ )
134
+
135
+ # Second call with same params should use cached LLM
136
+ result2 = execute_prompt(
137
+ prompt_name="greet",
138
+ variables={"name": "Ivy", "style": "casual"},
139
+ temperature=0.7,
140
+ provider="anthropic",
141
+ )
142
+
143
+ # Both should succeed (testing cache doesn't break functionality)
144
+ assert isinstance(result1, str)
145
+ assert isinstance(result2, str)
146
+
147
+
148
+ class TestJinja2WithProviders:
149
+ """Test Jinja2 templates work with different providers."""
150
+
151
+ def setup_method(self):
152
+ """Clear LLM cache before each test."""
153
+ clear_cache()
154
+
155
+ def test_simple_prompt_template_format(self):
156
+ """Should work with simple {variable} templates on any provider."""
157
+ result = execute_prompt(
158
+ prompt_name="greet",
159
+ variables={"name": "TestUser", "style": "formal"},
160
+ provider="anthropic",
161
+ )
162
+ assert isinstance(result, str)
163
+ assert len(result) > 0
@@ -0,0 +1,75 @@
1
+ """Integration tests for pipeline resume functionality."""
2
+
3
+ from unittest.mock import patch
4
+
5
+ from tests.conftest import FixtureAnalysis, FixtureGeneratedContent
6
+ from yamlgraph.builder import build_resume_graph
7
+ from yamlgraph.models import create_initial_state
8
+
9
+
10
+ class TestResumeFromAnalyze:
11
+ """Tests for resuming pipeline with existing generated content."""
12
+
13
+ @patch("yamlgraph.node_factory.execute_prompt")
14
+ def test_resume_with_generated_skips_generate(self, mock_execute):
15
+ """Should skip generate when generated content exists."""
16
+ # Create state with generated content
17
+ state = create_initial_state(topic="test", thread_id="resume1")
18
+ state["generated"] = FixtureGeneratedContent(
19
+ title="Existing Title",
20
+ content="Existing content",
21
+ word_count=10,
22
+ tags=[],
23
+ )
24
+ state["current_step"] = "generate"
25
+
26
+ # Mock returns: analyze, summarize (generate is skipped)
27
+ mock_analysis = FixtureAnalysis(
28
+ summary="Resume summary",
29
+ key_points=["Point"],
30
+ sentiment="neutral",
31
+ confidence=0.7,
32
+ )
33
+ mock_execute.side_effect = [mock_analysis, "Final summary"]
34
+
35
+ graph = build_resume_graph().compile()
36
+ result = graph.invoke(state)
37
+
38
+ assert mock_execute.call_count == 2 # analyze + summarize only
39
+ assert result["analysis"] == mock_analysis
40
+ assert result["final_summary"] == "Final summary"
41
+ assert result["generated"].title == "Existing Title" # Preserved
42
+
43
+
44
+ class TestResumeFromSummarize:
45
+ """Tests for resuming pipeline with existing analysis."""
46
+
47
+ @patch("yamlgraph.node_factory.execute_prompt")
48
+ def test_resume_with_analysis_skips_generate_and_analyze(self, mock_execute):
49
+ """Should skip generate and analyze when both exist."""
50
+ # Create state with generated content and analysis
51
+ state = create_initial_state(topic="test", thread_id="resume2")
52
+ state["generated"] = FixtureGeneratedContent(
53
+ title="Title",
54
+ content="Content",
55
+ word_count=5,
56
+ tags=[],
57
+ )
58
+ state["analysis"] = FixtureAnalysis(
59
+ summary="Existing analysis",
60
+ key_points=["Point"],
61
+ sentiment="positive",
62
+ confidence=0.8,
63
+ )
64
+ state["current_step"] = "analyze"
65
+
66
+ # Mock returns: only summarize (generate and analyze skipped)
67
+ mock_execute.return_value = "Resumed final summary"
68
+
69
+ graph = build_resume_graph().compile()
70
+ result = graph.invoke(state)
71
+
72
+ assert mock_execute.call_count == 1 # summarize only
73
+ assert result["final_summary"] == "Resumed final summary"
74
+ assert result["generated"].title == "Title" # Preserved
75
+ assert result["analysis"].summary == "Existing analysis" # Preserved
tests/unit/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Unit tests for yamlgraph."""