yamlgraph 0.3.9__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.
- examples/__init__.py +1 -0
- examples/codegen/__init__.py +5 -0
- examples/codegen/models/__init__.py +13 -0
- examples/codegen/models/schemas.py +76 -0
- examples/codegen/tests/__init__.py +1 -0
- examples/codegen/tests/test_ai_helpers.py +235 -0
- examples/codegen/tests/test_ast_analysis.py +174 -0
- examples/codegen/tests/test_code_analysis.py +134 -0
- examples/codegen/tests/test_code_context.py +301 -0
- examples/codegen/tests/test_code_nav.py +89 -0
- examples/codegen/tests/test_dependency_tools.py +119 -0
- examples/codegen/tests/test_example_tools.py +185 -0
- examples/codegen/tests/test_git_tools.py +112 -0
- examples/codegen/tests/test_impl_agent_schemas.py +193 -0
- examples/codegen/tests/test_impl_agent_v4_graph.py +94 -0
- examples/codegen/tests/test_jedi_analysis.py +226 -0
- examples/codegen/tests/test_meta_tools.py +250 -0
- examples/codegen/tests/test_plan_discovery_prompt.py +98 -0
- examples/codegen/tests/test_syntax_tools.py +85 -0
- examples/codegen/tests/test_synthesize_prompt.py +94 -0
- examples/codegen/tests/test_template_tools.py +244 -0
- examples/codegen/tools/__init__.py +80 -0
- examples/codegen/tools/ai_helpers.py +420 -0
- examples/codegen/tools/ast_analysis.py +92 -0
- examples/codegen/tools/code_context.py +180 -0
- examples/codegen/tools/code_nav.py +52 -0
- examples/codegen/tools/dependency_tools.py +120 -0
- examples/codegen/tools/example_tools.py +188 -0
- examples/codegen/tools/git_tools.py +151 -0
- examples/codegen/tools/impl_executor.py +614 -0
- examples/codegen/tools/jedi_analysis.py +311 -0
- examples/codegen/tools/meta_tools.py +202 -0
- examples/codegen/tools/syntax_tools.py +26 -0
- examples/codegen/tools/template_tools.py +356 -0
- examples/fastapi_interview.py +167 -0
- examples/npc/api/__init__.py +1 -0
- examples/npc/api/app.py +100 -0
- examples/npc/api/routes/__init__.py +5 -0
- examples/npc/api/routes/encounter.py +182 -0
- examples/npc/api/session.py +330 -0
- examples/npc/demo.py +387 -0
- examples/npc/nodes/__init__.py +5 -0
- examples/npc/nodes/image_node.py +92 -0
- examples/npc/run_encounter.py +230 -0
- examples/shared/__init__.py +0 -0
- examples/shared/replicate_tool.py +238 -0
- examples/storyboard/__init__.py +1 -0
- examples/storyboard/generate_videos.py +335 -0
- examples/storyboard/nodes/__init__.py +12 -0
- examples/storyboard/nodes/animated_character_node.py +248 -0
- examples/storyboard/nodes/animated_image_node.py +138 -0
- examples/storyboard/nodes/character_node.py +162 -0
- examples/storyboard/nodes/image_node.py +118 -0
- examples/storyboard/nodes/replicate_tool.py +49 -0
- examples/storyboard/retry_images.py +118 -0
- scripts/demo_async_executor.py +212 -0
- scripts/demo_interview_e2e.py +200 -0
- scripts/demo_streaming.py +140 -0
- scripts/run_interview_demo.py +94 -0
- scripts/test_interrupt_fix.py +26 -0
- tests/__init__.py +1 -0
- tests/conftest.py +178 -0
- tests/integration/__init__.py +1 -0
- tests/integration/test_animated_storyboard.py +63 -0
- tests/integration/test_cli_commands.py +242 -0
- tests/integration/test_colocated_prompts.py +139 -0
- tests/integration/test_map_demo.py +50 -0
- tests/integration/test_memory_demo.py +283 -0
- tests/integration/test_npc_api/__init__.py +1 -0
- tests/integration/test_npc_api/test_routes.py +357 -0
- tests/integration/test_npc_api/test_session.py +216 -0
- tests/integration/test_pipeline_flow.py +105 -0
- tests/integration/test_providers.py +163 -0
- tests/integration/test_resume.py +75 -0
- tests/integration/test_subgraph_integration.py +295 -0
- tests/integration/test_subgraph_interrupt.py +106 -0
- tests/unit/__init__.py +1 -0
- tests/unit/test_agent_nodes.py +355 -0
- tests/unit/test_async_executor.py +346 -0
- tests/unit/test_checkpointer.py +212 -0
- tests/unit/test_checkpointer_factory.py +212 -0
- tests/unit/test_cli.py +121 -0
- tests/unit/test_cli_package.py +81 -0
- tests/unit/test_compile_graph_map.py +132 -0
- tests/unit/test_conditions_routing.py +253 -0
- tests/unit/test_config.py +93 -0
- tests/unit/test_conversation_memory.py +276 -0
- tests/unit/test_database.py +145 -0
- tests/unit/test_deprecation.py +104 -0
- tests/unit/test_executor.py +172 -0
- tests/unit/test_executor_async.py +179 -0
- tests/unit/test_export.py +149 -0
- tests/unit/test_expressions.py +178 -0
- tests/unit/test_feature_brainstorm.py +194 -0
- tests/unit/test_format_prompt.py +145 -0
- tests/unit/test_generic_report.py +200 -0
- tests/unit/test_graph_commands.py +327 -0
- tests/unit/test_graph_linter.py +627 -0
- tests/unit/test_graph_loader.py +357 -0
- tests/unit/test_graph_schema.py +193 -0
- tests/unit/test_inline_schema.py +151 -0
- tests/unit/test_interrupt_node.py +182 -0
- tests/unit/test_issues.py +164 -0
- tests/unit/test_jinja2_prompts.py +85 -0
- tests/unit/test_json_extract.py +134 -0
- tests/unit/test_langsmith.py +600 -0
- tests/unit/test_langsmith_tools.py +204 -0
- tests/unit/test_llm_factory.py +109 -0
- tests/unit/test_llm_factory_async.py +118 -0
- tests/unit/test_loops.py +403 -0
- tests/unit/test_map_node.py +144 -0
- tests/unit/test_no_backward_compat.py +56 -0
- tests/unit/test_node_factory.py +348 -0
- tests/unit/test_passthrough_node.py +126 -0
- tests/unit/test_prompts.py +324 -0
- tests/unit/test_python_nodes.py +198 -0
- tests/unit/test_reliability.py +298 -0
- tests/unit/test_result_export.py +234 -0
- tests/unit/test_router.py +296 -0
- tests/unit/test_sanitize.py +99 -0
- tests/unit/test_schema_loader.py +295 -0
- tests/unit/test_shell_tools.py +229 -0
- tests/unit/test_state_builder.py +331 -0
- tests/unit/test_state_builder_map.py +104 -0
- tests/unit/test_state_config.py +197 -0
- tests/unit/test_streaming.py +307 -0
- tests/unit/test_subgraph.py +596 -0
- tests/unit/test_template.py +190 -0
- tests/unit/test_tool_call_integration.py +164 -0
- tests/unit/test_tool_call_node.py +178 -0
- tests/unit/test_tool_nodes.py +129 -0
- tests/unit/test_websearch.py +234 -0
- yamlgraph/__init__.py +35 -0
- yamlgraph/builder.py +110 -0
- yamlgraph/cli/__init__.py +159 -0
- yamlgraph/cli/__main__.py +6 -0
- yamlgraph/cli/commands.py +231 -0
- yamlgraph/cli/deprecation.py +92 -0
- yamlgraph/cli/graph_commands.py +541 -0
- yamlgraph/cli/validators.py +37 -0
- yamlgraph/config.py +67 -0
- yamlgraph/constants.py +70 -0
- yamlgraph/error_handlers.py +227 -0
- yamlgraph/executor.py +290 -0
- yamlgraph/executor_async.py +288 -0
- yamlgraph/graph_loader.py +451 -0
- yamlgraph/map_compiler.py +150 -0
- yamlgraph/models/__init__.py +36 -0
- yamlgraph/models/graph_schema.py +181 -0
- yamlgraph/models/schemas.py +124 -0
- yamlgraph/models/state_builder.py +236 -0
- yamlgraph/node_factory.py +768 -0
- yamlgraph/routing.py +87 -0
- yamlgraph/schema_loader.py +240 -0
- yamlgraph/storage/__init__.py +20 -0
- yamlgraph/storage/checkpointer.py +72 -0
- yamlgraph/storage/checkpointer_factory.py +123 -0
- yamlgraph/storage/database.py +320 -0
- yamlgraph/storage/export.py +269 -0
- yamlgraph/tools/__init__.py +1 -0
- yamlgraph/tools/agent.py +320 -0
- yamlgraph/tools/graph_linter.py +388 -0
- yamlgraph/tools/langsmith_tools.py +125 -0
- yamlgraph/tools/nodes.py +126 -0
- yamlgraph/tools/python_tool.py +179 -0
- yamlgraph/tools/shell.py +205 -0
- yamlgraph/tools/websearch.py +242 -0
- yamlgraph/utils/__init__.py +48 -0
- yamlgraph/utils/conditions.py +157 -0
- yamlgraph/utils/expressions.py +245 -0
- yamlgraph/utils/json_extract.py +104 -0
- yamlgraph/utils/langsmith.py +416 -0
- yamlgraph/utils/llm_factory.py +118 -0
- yamlgraph/utils/llm_factory_async.py +105 -0
- yamlgraph/utils/logging.py +104 -0
- yamlgraph/utils/prompts.py +171 -0
- yamlgraph/utils/sanitize.py +98 -0
- yamlgraph/utils/template.py +102 -0
- yamlgraph/utils/validators.py +181 -0
- yamlgraph-0.3.9.dist-info/METADATA +1105 -0
- yamlgraph-0.3.9.dist-info/RECORD +185 -0
- yamlgraph-0.3.9.dist-info/WHEEL +5 -0
- yamlgraph-0.3.9.dist-info/entry_points.txt +2 -0
- yamlgraph-0.3.9.dist-info/licenses/LICENSE +33 -0
- yamlgraph-0.3.9.dist-info/top_level.txt +4 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Integration tests for subgraph functionality.
|
|
2
|
+
|
|
3
|
+
Tests end-to-end subgraph execution with mocked LLM calls.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def subgraph_graphs(tmp_path: Path) -> tuple[Path, Path]:
|
|
14
|
+
"""Create parent and child graph files for testing."""
|
|
15
|
+
# Create prompts directory
|
|
16
|
+
prompts_dir = tmp_path / "prompts"
|
|
17
|
+
prompts_dir.mkdir()
|
|
18
|
+
|
|
19
|
+
# Create child prompt
|
|
20
|
+
child_prompt = prompts_dir / "child" / "process.yaml"
|
|
21
|
+
child_prompt.parent.mkdir()
|
|
22
|
+
child_prompt.write_text(
|
|
23
|
+
"""
|
|
24
|
+
system: You are a processor.
|
|
25
|
+
user: Process this: {input_text}
|
|
26
|
+
"""
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Create parent prompts
|
|
30
|
+
parent_prompt_dir = prompts_dir / "parent"
|
|
31
|
+
parent_prompt_dir.mkdir()
|
|
32
|
+
(parent_prompt_dir / "prepare.yaml").write_text(
|
|
33
|
+
"""
|
|
34
|
+
system: You are a preparer.
|
|
35
|
+
user: Prepare this: {raw_text}
|
|
36
|
+
"""
|
|
37
|
+
)
|
|
38
|
+
(parent_prompt_dir / "finalize.yaml").write_text(
|
|
39
|
+
"""
|
|
40
|
+
system: You are a finalizer.
|
|
41
|
+
user: Finalize this: {processed}
|
|
42
|
+
"""
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Create child subgraph
|
|
46
|
+
subgraphs_dir = tmp_path / "graphs" / "subgraphs"
|
|
47
|
+
subgraphs_dir.mkdir(parents=True)
|
|
48
|
+
child_graph = subgraphs_dir / "processor.yaml"
|
|
49
|
+
child_graph.write_text(
|
|
50
|
+
"""
|
|
51
|
+
version: "1.0"
|
|
52
|
+
name: processor
|
|
53
|
+
state:
|
|
54
|
+
input_text: str
|
|
55
|
+
output_text: str
|
|
56
|
+
nodes:
|
|
57
|
+
process:
|
|
58
|
+
type: llm
|
|
59
|
+
prompt: child/process
|
|
60
|
+
state_key: output_text
|
|
61
|
+
edges:
|
|
62
|
+
- {from: START, to: process}
|
|
63
|
+
- {from: process, to: END}
|
|
64
|
+
"""
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Create parent graph
|
|
68
|
+
parent_graph = tmp_path / "graphs" / "parent.yaml"
|
|
69
|
+
parent_graph.write_text(
|
|
70
|
+
"""
|
|
71
|
+
version: "1.0"
|
|
72
|
+
name: parent
|
|
73
|
+
state:
|
|
74
|
+
raw_text: str
|
|
75
|
+
prepared: str
|
|
76
|
+
processed: str
|
|
77
|
+
final: str
|
|
78
|
+
nodes:
|
|
79
|
+
prepare:
|
|
80
|
+
type: llm
|
|
81
|
+
prompt: parent/prepare
|
|
82
|
+
state_key: prepared
|
|
83
|
+
process:
|
|
84
|
+
type: subgraph
|
|
85
|
+
mode: invoke
|
|
86
|
+
graph: subgraphs/processor.yaml
|
|
87
|
+
input_mapping:
|
|
88
|
+
prepared: input_text
|
|
89
|
+
output_mapping:
|
|
90
|
+
processed: output_text
|
|
91
|
+
finalize:
|
|
92
|
+
type: llm
|
|
93
|
+
prompt: parent/finalize
|
|
94
|
+
state_key: final
|
|
95
|
+
edges:
|
|
96
|
+
- {from: START, to: prepare}
|
|
97
|
+
- {from: prepare, to: process}
|
|
98
|
+
- {from: process, to: finalize}
|
|
99
|
+
- {from: finalize, to: END}
|
|
100
|
+
"""
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return parent_graph, child_graph
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestSubgraphIntegration:
|
|
107
|
+
"""End-to-end subgraph tests with mocked LLM."""
|
|
108
|
+
|
|
109
|
+
def test_runs_parent_to_subgraph_to_parent(self, subgraph_graphs, monkeypatch):
|
|
110
|
+
"""Runs parent → subgraph → parent flow successfully."""
|
|
111
|
+
|
|
112
|
+
from yamlgraph.graph_loader import compile_graph, load_graph_config
|
|
113
|
+
|
|
114
|
+
parent_graph, _ = subgraph_graphs
|
|
115
|
+
prompts_dir = parent_graph.parent.parent / "prompts"
|
|
116
|
+
|
|
117
|
+
# Set prompts directory
|
|
118
|
+
monkeypatch.setenv("YAMLGRAPH_PROMPTS_DIR", str(prompts_dir))
|
|
119
|
+
|
|
120
|
+
# Mock execute_prompt to return predictable results
|
|
121
|
+
call_count = {"count": 0}
|
|
122
|
+
|
|
123
|
+
def mock_execute(prompt_name, **kwargs):
|
|
124
|
+
call_count["count"] += 1
|
|
125
|
+
if "prepare" in prompt_name:
|
|
126
|
+
return "prepared text"
|
|
127
|
+
elif "process" in prompt_name:
|
|
128
|
+
return "processed text"
|
|
129
|
+
elif "finalize" in prompt_name:
|
|
130
|
+
return "final text"
|
|
131
|
+
return f"mocked response for {prompt_name}"
|
|
132
|
+
|
|
133
|
+
with patch("yamlgraph.node_factory.execute_prompt", side_effect=mock_execute):
|
|
134
|
+
config = load_graph_config(parent_graph)
|
|
135
|
+
graph = compile_graph(config)
|
|
136
|
+
compiled = graph.compile()
|
|
137
|
+
|
|
138
|
+
result = compiled.invoke({"raw_text": "test input"})
|
|
139
|
+
|
|
140
|
+
# Verify all nodes ran
|
|
141
|
+
assert result["prepared"] == "prepared text"
|
|
142
|
+
assert result["processed"] == "processed text"
|
|
143
|
+
assert result["final"] == "final text"
|
|
144
|
+
assert call_count["count"] == 3 # prepare + process (subgraph) + finalize
|
|
145
|
+
|
|
146
|
+
def test_subgraph_state_mapping_works(self, subgraph_graphs, monkeypatch):
|
|
147
|
+
"""Input/output mapping correctly transforms state."""
|
|
148
|
+
from yamlgraph.graph_loader import compile_graph, load_graph_config
|
|
149
|
+
|
|
150
|
+
parent_graph, _ = subgraph_graphs
|
|
151
|
+
prompts_dir = parent_graph.parent.parent / "prompts"
|
|
152
|
+
monkeypatch.setenv("YAMLGRAPH_PROMPTS_DIR", str(prompts_dir))
|
|
153
|
+
|
|
154
|
+
captured_inputs = {}
|
|
155
|
+
|
|
156
|
+
def mock_execute(prompt_name, **kwargs):
|
|
157
|
+
captured_inputs[prompt_name] = kwargs.get("variables", {})
|
|
158
|
+
if "prepare" in prompt_name:
|
|
159
|
+
return "PREPARED"
|
|
160
|
+
elif "process" in prompt_name:
|
|
161
|
+
# This should receive input_text (mapped from prepared)
|
|
162
|
+
return "PROCESSED"
|
|
163
|
+
elif "finalize" in prompt_name:
|
|
164
|
+
return "FINAL"
|
|
165
|
+
return "mock"
|
|
166
|
+
|
|
167
|
+
with patch("yamlgraph.node_factory.execute_prompt", side_effect=mock_execute):
|
|
168
|
+
config = load_graph_config(parent_graph)
|
|
169
|
+
graph = compile_graph(config)
|
|
170
|
+
compiled = graph.compile()
|
|
171
|
+
|
|
172
|
+
result = compiled.invoke({"raw_text": "original"})
|
|
173
|
+
|
|
174
|
+
# Check that subgraph received mapped input
|
|
175
|
+
assert "child/process" in captured_inputs
|
|
176
|
+
# The subgraph should have input_text (mapped from parent's prepared)
|
|
177
|
+
assert captured_inputs["child/process"].get("input_text") == "PREPARED"
|
|
178
|
+
|
|
179
|
+
# Check output was mapped back
|
|
180
|
+
assert result["processed"] == "PROCESSED"
|
|
181
|
+
|
|
182
|
+
def test_nested_subgraphs(self, tmp_path, monkeypatch):
|
|
183
|
+
"""Supports subgraph within subgraph (2 levels deep)."""
|
|
184
|
+
prompts_dir = tmp_path / "prompts"
|
|
185
|
+
prompts_dir.mkdir()
|
|
186
|
+
|
|
187
|
+
# Level 2 (deepest)
|
|
188
|
+
(prompts_dir / "level2").mkdir()
|
|
189
|
+
(prompts_dir / "level2" / "process.yaml").write_text(
|
|
190
|
+
"system: L2\nuser: {data}" # Use 'data' to avoid skip
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Level 1
|
|
194
|
+
(prompts_dir / "level1").mkdir()
|
|
195
|
+
(prompts_dir / "level1" / "pre.yaml").write_text(
|
|
196
|
+
"system: L1\nuser: {input}" # Use 'input' to avoid skip
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
graphs_dir = tmp_path / "graphs"
|
|
200
|
+
graphs_dir.mkdir()
|
|
201
|
+
(graphs_dir / "subgraphs").mkdir()
|
|
202
|
+
|
|
203
|
+
# Level 2 graph
|
|
204
|
+
(graphs_dir / "subgraphs" / "level2.yaml").write_text(
|
|
205
|
+
"""
|
|
206
|
+
version: "1.0"
|
|
207
|
+
name: level2
|
|
208
|
+
state:
|
|
209
|
+
data: str
|
|
210
|
+
output: str
|
|
211
|
+
nodes:
|
|
212
|
+
work:
|
|
213
|
+
type: llm
|
|
214
|
+
prompt: level2/process
|
|
215
|
+
state_key: output
|
|
216
|
+
edges:
|
|
217
|
+
- {from: START, to: work}
|
|
218
|
+
- {from: work, to: END}
|
|
219
|
+
"""
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Level 1 graph (calls level 2)
|
|
223
|
+
(graphs_dir / "subgraphs" / "level1.yaml").write_text(
|
|
224
|
+
"""
|
|
225
|
+
version: "1.0"
|
|
226
|
+
name: level1
|
|
227
|
+
state:
|
|
228
|
+
input: str
|
|
229
|
+
prepared: str
|
|
230
|
+
output: str
|
|
231
|
+
nodes:
|
|
232
|
+
pre:
|
|
233
|
+
type: llm
|
|
234
|
+
prompt: level1/pre
|
|
235
|
+
state_key: prepared
|
|
236
|
+
nested:
|
|
237
|
+
type: subgraph
|
|
238
|
+
mode: invoke
|
|
239
|
+
graph: level2.yaml
|
|
240
|
+
input_mapping:
|
|
241
|
+
prepared: data
|
|
242
|
+
output_mapping:
|
|
243
|
+
output: output
|
|
244
|
+
edges:
|
|
245
|
+
- {from: START, to: pre}
|
|
246
|
+
- {from: pre, to: nested}
|
|
247
|
+
- {from: nested, to: END}
|
|
248
|
+
"""
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Root graph (calls level 1)
|
|
252
|
+
root = graphs_dir / "root.yaml"
|
|
253
|
+
root.write_text(
|
|
254
|
+
"""
|
|
255
|
+
version: "1.0"
|
|
256
|
+
name: root
|
|
257
|
+
state:
|
|
258
|
+
start: str
|
|
259
|
+
result: str
|
|
260
|
+
nodes:
|
|
261
|
+
delegate:
|
|
262
|
+
type: subgraph
|
|
263
|
+
mode: invoke
|
|
264
|
+
graph: subgraphs/level1.yaml
|
|
265
|
+
input_mapping:
|
|
266
|
+
start: input
|
|
267
|
+
output_mapping:
|
|
268
|
+
result: output
|
|
269
|
+
edges:
|
|
270
|
+
- {from: START, to: delegate}
|
|
271
|
+
- {from: delegate, to: END}
|
|
272
|
+
"""
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
monkeypatch.setenv("YAMLGRAPH_PROMPTS_DIR", str(prompts_dir))
|
|
276
|
+
|
|
277
|
+
call_sequence = []
|
|
278
|
+
|
|
279
|
+
def mock_execute(prompt_name, **kwargs):
|
|
280
|
+
call_sequence.append(prompt_name)
|
|
281
|
+
return f"result from {prompt_name}"
|
|
282
|
+
|
|
283
|
+
with patch("yamlgraph.node_factory.execute_prompt", side_effect=mock_execute):
|
|
284
|
+
from yamlgraph.graph_loader import compile_graph, load_graph_config
|
|
285
|
+
|
|
286
|
+
config = load_graph_config(root)
|
|
287
|
+
graph = compile_graph(config)
|
|
288
|
+
compiled = graph.compile()
|
|
289
|
+
|
|
290
|
+
result = compiled.invoke({"start": "hello"})
|
|
291
|
+
|
|
292
|
+
# Both levels should have executed
|
|
293
|
+
assert "level1/pre" in call_sequence
|
|
294
|
+
assert "level2/process" in call_sequence
|
|
295
|
+
assert result["result"] == "result from level2/process"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Integration test for FR-006: interrupt_output_mapping limitation.
|
|
2
|
+
|
|
3
|
+
This test demonstrates the known limitation where interrupt_output_mapping
|
|
4
|
+
cannot expose child state when the subgraph hits an interrupt, because
|
|
5
|
+
LangGraph's interrupt mechanism uses exceptions that bypass the mapping code.
|
|
6
|
+
|
|
7
|
+
See: docs/subgraph-interrupt-bug.md
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestSubgraphInterruptMapping:
|
|
16
|
+
"""Tests for FR-006 interrupt_output_mapping with real graphs."""
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def parent_graph_path(self) -> Path:
|
|
20
|
+
"""Path to the parent graph with interrupt_output_mapping."""
|
|
21
|
+
return Path(__file__).parent.parent.parent / "graphs" / "interrupt-parent.yaml"
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def compiled_graph(self, parent_graph_path: Path):
|
|
25
|
+
"""Compile the parent graph with checkpointer."""
|
|
26
|
+
from yamlgraph.graph_loader import compile_graph, load_graph_config
|
|
27
|
+
|
|
28
|
+
config = load_graph_config(parent_graph_path)
|
|
29
|
+
state_graph = compile_graph(config)
|
|
30
|
+
|
|
31
|
+
# Use memory checkpointer for testing
|
|
32
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
33
|
+
checkpointer = MemorySaver()
|
|
34
|
+
|
|
35
|
+
return state_graph.compile(checkpointer=checkpointer)
|
|
36
|
+
|
|
37
|
+
def test_interrupt_output_mapping_surfaces_child_state(self, compiled_graph):
|
|
38
|
+
"""FR-006: Parent should see child state when subgraph is interrupted.
|
|
39
|
+
|
|
40
|
+
EXPECTED TO FAIL: This test documents the current limitation.
|
|
41
|
+
When it passes, FR-006 is truly fixed.
|
|
42
|
+
"""
|
|
43
|
+
# Run parent graph - child will hit interrupt
|
|
44
|
+
config = {"configurable": {"thread_id": "test-fr006"}}
|
|
45
|
+
result = compiled_graph.invoke({"user_input": "hello"}, config)
|
|
46
|
+
|
|
47
|
+
# Verify we hit the interrupt
|
|
48
|
+
assert "__interrupt__" in result, "Expected graph to be interrupted"
|
|
49
|
+
|
|
50
|
+
# FR-006 EXPECTATION: child state should be mapped to parent
|
|
51
|
+
# This currently FAILS because interrupt exception bypasses mapping
|
|
52
|
+
assert "child_phase" in result, (
|
|
53
|
+
"FR-006 LIMITATION: child_phase not in result. "
|
|
54
|
+
"interrupt_output_mapping is bypassed by LangGraph's exception mechanism. "
|
|
55
|
+
"See docs/subgraph-interrupt-bug.md"
|
|
56
|
+
)
|
|
57
|
+
assert result.get("child_phase") == "processing", (
|
|
58
|
+
"Expected child_phase='processing' from child graph"
|
|
59
|
+
)
|
|
60
|
+
assert result.get("child_data") == "partial result from child", (
|
|
61
|
+
"Expected child_data from interrupt_output_mapping"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def test_output_mapping_works_on_completion(self, compiled_graph):
|
|
65
|
+
"""Verify output_mapping works when subgraph completes normally.
|
|
66
|
+
|
|
67
|
+
This test should PASS - it resumes the interrupt and completes.
|
|
68
|
+
"""
|
|
69
|
+
from langgraph.types import Command
|
|
70
|
+
|
|
71
|
+
config = {"configurable": {"thread_id": "test-completion"}}
|
|
72
|
+
|
|
73
|
+
# First run - hits interrupt
|
|
74
|
+
result = compiled_graph.invoke({"user_input": "hello"}, config)
|
|
75
|
+
assert "__interrupt__" in result
|
|
76
|
+
|
|
77
|
+
# Resume with user answer
|
|
78
|
+
result = compiled_graph.invoke(Command(resume="my answer"), config)
|
|
79
|
+
|
|
80
|
+
# After completion, output_mapping should work
|
|
81
|
+
# Note: The 'done' node sets final_result to 'all done'
|
|
82
|
+
assert result.get("final_result") == "all done", (
|
|
83
|
+
"Expected final_result from 'done' passthrough node"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def test_get_state_can_access_child_state(self, compiled_graph):
|
|
87
|
+
"""Workaround: Use get_state() to access child state after interrupt.
|
|
88
|
+
|
|
89
|
+
This test documents the workaround for the FR-006 limitation.
|
|
90
|
+
"""
|
|
91
|
+
config = {"configurable": {"thread_id": "test-workaround"}}
|
|
92
|
+
|
|
93
|
+
# Run until interrupt
|
|
94
|
+
result = compiled_graph.invoke({"user_input": "hello"}, config)
|
|
95
|
+
assert "__interrupt__" in result
|
|
96
|
+
|
|
97
|
+
# Workaround: access state via checkpointer
|
|
98
|
+
state_snapshot = compiled_graph.get_state(config)
|
|
99
|
+
|
|
100
|
+
# The parent state should be accessible
|
|
101
|
+
assert state_snapshot is not None
|
|
102
|
+
assert "values" in dir(state_snapshot) or hasattr(state_snapshot, "values")
|
|
103
|
+
|
|
104
|
+
# Note: Child state may be in nested subgraph thread
|
|
105
|
+
# This depends on checkpointer implementation
|
|
106
|
+
print(f"State snapshot values: {state_snapshot.values}")
|
tests/unit/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Unit tests for yamlgraph."""
|