mdan-cli 2.6.0 → 2.7.0
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.
- package/AGENTS.md +48 -1
- package/README.md +123 -0
- package/cli/mdan.py +38 -4
- package/cli/mdan_crewai.py +539 -0
- package/core/crewai_orchestrator.md +419 -0
- package/integrations/__init__.py +33 -0
- package/integrations/crewai/__init__.py +27 -0
- package/integrations/crewai/agents/__init__.py +21 -0
- package/integrations/crewai/agents/architect_agent.py +264 -0
- package/integrations/crewai/agents/dev_agent.py +271 -0
- package/integrations/crewai/agents/devops_agent.py +421 -0
- package/integrations/crewai/agents/doc_agent.py +388 -0
- package/integrations/crewai/agents/product_agent.py +203 -0
- package/integrations/crewai/agents/security_agent.py +386 -0
- package/integrations/crewai/agents/test_agent.py +358 -0
- package/integrations/crewai/agents/ux_agent.py +257 -0
- package/integrations/crewai/flows/__init__.py +13 -0
- package/integrations/crewai/flows/auto_flow.py +451 -0
- package/integrations/crewai/flows/build_flow.py +297 -0
- package/integrations/crewai/flows/debate_flow.py +422 -0
- package/integrations/crewai/flows/discovery_flow.py +267 -0
- package/integrations/crewai/orchestrator.py +558 -0
- package/integrations/crewai/skills/__init__.py +8 -0
- package/integrations/crewai/skills/skill_router.py +534 -0
- package/integrations/crewai/tools/__init__.py +11 -0
- package/integrations/crewai/tools/file_tool.py +355 -0
- package/integrations/crewai/tools/serper_tool.py +169 -0
- package/integrations/crewai/tools/sql_tool.py +435 -0
- package/package.json +1 -1
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""Auto Flow - Main autonomous flow with 8 phases
|
|
2
|
+
|
|
3
|
+
Orchestrates the complete autonomous development cycle:
|
|
4
|
+
LOAD → DISCOVER → PLAN → ARCHITECT → IMPLEMENT → TEST → DEPLOY → DOC
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from crewai.flow import Flow, listen, start
|
|
8
|
+
from typing import Dict, Any, Optional
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from ..agents.product_agent import ProductAgent
|
|
14
|
+
from ..agents.architect_agent import ArchitectAgent
|
|
15
|
+
from ..agents.ux_agent import UXAgent
|
|
16
|
+
from ..agents.dev_agent import DevAgent
|
|
17
|
+
from ..agents.test_agent import TestAgent
|
|
18
|
+
from ..agents.security_agent import SecurityAgent
|
|
19
|
+
from ..agents.devops_agent import DevOpsAgent
|
|
20
|
+
from ..agents.doc_agent import DocAgent
|
|
21
|
+
|
|
22
|
+
from ..tools.sql_tool import SQLTool
|
|
23
|
+
from ..tools.serper_tool import SerperTool
|
|
24
|
+
from ..tools.file_tool import FileTool
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AutoFlow(Flow):
|
|
28
|
+
"""Main autonomous flow for full-cycle development."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
project_path: str,
|
|
33
|
+
llm=None,
|
|
34
|
+
sql_config: Optional[Dict[str, Any]] = None,
|
|
35
|
+
serper_api_key: Optional[str] = None,
|
|
36
|
+
):
|
|
37
|
+
"""Initialize Auto Flow.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
project_path: Path to the project directory
|
|
41
|
+
llm: Language model instance
|
|
42
|
+
sql_config: SQL database configuration
|
|
43
|
+
serper_api_key: Serper API key for web search
|
|
44
|
+
"""
|
|
45
|
+
super().__init__()
|
|
46
|
+
self.project_path = Path(project_path)
|
|
47
|
+
self.llm = llm
|
|
48
|
+
|
|
49
|
+
# Initialize tools
|
|
50
|
+
self.sql_tool = SQLTool(**sql_config) if sql_config else None
|
|
51
|
+
self.serper_tool = (
|
|
52
|
+
SerperTool(api_key=serper_api_key) if serper_api_key else None
|
|
53
|
+
)
|
|
54
|
+
self.file_tool = FileTool(base_path=project_path)
|
|
55
|
+
|
|
56
|
+
# Initialize agents
|
|
57
|
+
self.product_agent = ProductAgent(
|
|
58
|
+
sql_tool=self.sql_tool,
|
|
59
|
+
serper_tool=self.serper_tool,
|
|
60
|
+
file_tool=self.file_tool,
|
|
61
|
+
llm=llm,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
self.architect_agent = ArchitectAgent(
|
|
65
|
+
sql_tool=self.sql_tool,
|
|
66
|
+
serper_tool=self.serper_tool,
|
|
67
|
+
file_tool=self.file_tool,
|
|
68
|
+
llm=llm,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
self.ux_agent = UXAgent(
|
|
72
|
+
sql_tool=self.sql_tool,
|
|
73
|
+
serper_tool=self.serper_tool,
|
|
74
|
+
file_tool=self.file_tool,
|
|
75
|
+
llm=llm,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
self.dev_agent = DevAgent(
|
|
79
|
+
sql_tool=self.sql_tool,
|
|
80
|
+
serper_tool=self.serper_tool,
|
|
81
|
+
file_tool=self.file_tool,
|
|
82
|
+
llm=llm,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
self.test_agent = TestAgent(
|
|
86
|
+
sql_tool=self.sql_tool,
|
|
87
|
+
serper_tool=self.serper_tool,
|
|
88
|
+
file_tool=self.file_tool,
|
|
89
|
+
llm=llm,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self.security_agent = SecurityAgent(
|
|
93
|
+
sql_tool=self.sql_tool,
|
|
94
|
+
serper_tool=self.serper_tool,
|
|
95
|
+
file_tool=self.file_tool,
|
|
96
|
+
llm=llm,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self.devops_agent = DevOpsAgent(
|
|
100
|
+
sql_tool=self.sql_tool,
|
|
101
|
+
serper_tool=self.serper_tool,
|
|
102
|
+
file_tool=self.file_tool,
|
|
103
|
+
llm=llm,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
self.doc_agent = DocAgent(
|
|
107
|
+
sql_tool=self.sql_tool,
|
|
108
|
+
serper_tool=self.serper_tool,
|
|
109
|
+
file_tool=self.file_tool,
|
|
110
|
+
llm=llm,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Flow state
|
|
114
|
+
self.state = {
|
|
115
|
+
"current_phase": None,
|
|
116
|
+
"project_context": {},
|
|
117
|
+
"phase_results": {},
|
|
118
|
+
"errors": [],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@start
|
|
122
|
+
async def load_phase(self):
|
|
123
|
+
"""LOAD phase: Load project context and state."""
|
|
124
|
+
self.state["current_phase"] = "LOAD"
|
|
125
|
+
|
|
126
|
+
print("🔄 LOAD Phase: Loading project context...")
|
|
127
|
+
|
|
128
|
+
# Load project state if exists
|
|
129
|
+
state_file = self.project_path / "MDAN-STATE.json"
|
|
130
|
+
if state_file.exists():
|
|
131
|
+
with open(state_file, "r") as f:
|
|
132
|
+
self.state["project_context"] = json.load(f)
|
|
133
|
+
print("✅ Loaded existing project state")
|
|
134
|
+
else:
|
|
135
|
+
self.state["project_context"] = {
|
|
136
|
+
"project_name": self.project_path.name,
|
|
137
|
+
"phases_completed": [],
|
|
138
|
+
"current_phase": "LOAD",
|
|
139
|
+
}
|
|
140
|
+
print("✅ Initialized new project state")
|
|
141
|
+
|
|
142
|
+
self.state["phase_results"]["LOAD"] = {
|
|
143
|
+
"status": "completed",
|
|
144
|
+
"context": self.state["project_context"],
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return self.state["project_context"]
|
|
148
|
+
|
|
149
|
+
@listen(load_phase)
|
|
150
|
+
async def discover_phase(self, project_context: Dict[str, Any]):
|
|
151
|
+
"""DISCOVER phase: Requirements gathering and PRD creation."""
|
|
152
|
+
self.state["current_phase"] = "DISCOVER"
|
|
153
|
+
|
|
154
|
+
print("🔍 DISCOVER Phase: Gathering requirements...")
|
|
155
|
+
|
|
156
|
+
# Create DISCOVER tasks
|
|
157
|
+
tasks = [
|
|
158
|
+
self.product_agent.create_prd_task(str(project_context)),
|
|
159
|
+
self.product_agent.create_user_stories_task(str(project_context)),
|
|
160
|
+
self.product_agent.create_personas_task(str(project_context)),
|
|
161
|
+
self.product_agent.create_feature_prioritization_task(str(project_context)),
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
# Execute tasks
|
|
165
|
+
results = []
|
|
166
|
+
for task in tasks:
|
|
167
|
+
try:
|
|
168
|
+
result = await asyncio.to_thread(task.execute)
|
|
169
|
+
results.append(result)
|
|
170
|
+
print(f"✅ Completed: {task.description[:50]}...")
|
|
171
|
+
except Exception as e:
|
|
172
|
+
self.state["errors"].append(f"DISCOVER error: {str(e)}")
|
|
173
|
+
print(f"❌ Error in task: {str(e)}")
|
|
174
|
+
|
|
175
|
+
self.state["phase_results"]["DISCOVER"] = {
|
|
176
|
+
"status": "completed",
|
|
177
|
+
"results": results,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return results
|
|
181
|
+
|
|
182
|
+
@listen(discover_phase)
|
|
183
|
+
async def plan_phase(self, discover_results: list):
|
|
184
|
+
"""PLAN phase: Project planning and task breakdown."""
|
|
185
|
+
self.state["current_phase"] = "PLAN"
|
|
186
|
+
|
|
187
|
+
print("📋 PLAN Phase: Creating project plan...")
|
|
188
|
+
|
|
189
|
+
# Create planning tasks
|
|
190
|
+
tasks = [
|
|
191
|
+
self.product_agent.create_acceptance_criteria_task(str(discover_results)),
|
|
192
|
+
self.product_agent.create_project_timeline_task(str(discover_results)),
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
# Execute tasks
|
|
196
|
+
results = []
|
|
197
|
+
for task in tasks:
|
|
198
|
+
try:
|
|
199
|
+
result = await asyncio.to_thread(task.execute)
|
|
200
|
+
results.append(result)
|
|
201
|
+
print(f"✅ Completed: {task.description[:50]}...")
|
|
202
|
+
except Exception as e:
|
|
203
|
+
self.state["errors"].append(f"PLAN error: {str(e)}")
|
|
204
|
+
print(f"❌ Error in task: {str(e)}")
|
|
205
|
+
|
|
206
|
+
self.state["phase_results"]["PLAN"] = {
|
|
207
|
+
"status": "completed",
|
|
208
|
+
"results": results,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return results
|
|
212
|
+
|
|
213
|
+
@listen(plan_phase)
|
|
214
|
+
async def architect_phase(self, plan_results: list):
|
|
215
|
+
"""ARCHITECT phase: System architecture and design."""
|
|
216
|
+
self.state["current_phase"] = "ARCHITECT"
|
|
217
|
+
|
|
218
|
+
print("🏗️ ARCHITECT Phase: Designing system architecture...")
|
|
219
|
+
|
|
220
|
+
# Create architecture tasks
|
|
221
|
+
tasks = [
|
|
222
|
+
self.architect_agent.create_architecture_task(str(plan_results)),
|
|
223
|
+
self.architect_agent.create_tech_stack_task(str(plan_results)),
|
|
224
|
+
self.architect_agent.create_adr_task(str(plan_results)),
|
|
225
|
+
self.architect_agent.create_api_design_task(str(plan_results)),
|
|
226
|
+
self.architect_agent.create_database_schema_task(str(plan_results)),
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
# Execute tasks
|
|
230
|
+
results = []
|
|
231
|
+
for task in tasks:
|
|
232
|
+
try:
|
|
233
|
+
result = await asyncio.to_thread(task.execute)
|
|
234
|
+
results.append(result)
|
|
235
|
+
print(f"✅ Completed: {task.description[:50]}...")
|
|
236
|
+
except Exception as e:
|
|
237
|
+
self.state["errors"].append(f"ARCHITECT error: {str(e)}")
|
|
238
|
+
print(f"❌ Error in task: {str(e)}")
|
|
239
|
+
|
|
240
|
+
self.state["phase_results"]["ARCHITECT"] = {
|
|
241
|
+
"status": "completed",
|
|
242
|
+
"results": results,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return results
|
|
246
|
+
|
|
247
|
+
@listen(architect_phase)
|
|
248
|
+
async def implement_phase(self, architect_results: list):
|
|
249
|
+
"""IMPLEMENT phase: Code implementation."""
|
|
250
|
+
self.state["current_phase"] = "IMPLEMENT"
|
|
251
|
+
|
|
252
|
+
print("💻 IMPLEMENT Phase: Implementing features...")
|
|
253
|
+
|
|
254
|
+
# Create implementation tasks
|
|
255
|
+
tasks = [
|
|
256
|
+
self.dev_agent.create_implementation_task(str(architect_results)),
|
|
257
|
+
self.dev_agent.create_refactoring_task(str(architect_results)),
|
|
258
|
+
self.dev_agent.create_code_review_task(str(architect_results)),
|
|
259
|
+
self.dev_agent.create_testing_task(str(architect_results)),
|
|
260
|
+
self.dev_agent.create_debugging_task(str(architect_results)),
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
# Execute tasks
|
|
264
|
+
results = []
|
|
265
|
+
for task in tasks:
|
|
266
|
+
try:
|
|
267
|
+
result = await asyncio.to_thread(task.execute)
|
|
268
|
+
results.append(result)
|
|
269
|
+
print(f"✅ Completed: {task.description[:50]}...")
|
|
270
|
+
except Exception as e:
|
|
271
|
+
self.state["errors"].append(f"IMPLEMENT error: {str(e)}")
|
|
272
|
+
print(f"❌ Error in task: {str(e)}")
|
|
273
|
+
|
|
274
|
+
self.state["phase_results"]["IMPLEMENT"] = {
|
|
275
|
+
"status": "completed",
|
|
276
|
+
"results": results,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return results
|
|
280
|
+
|
|
281
|
+
@listen(implement_phase)
|
|
282
|
+
async def test_phase(self, implement_results: list):
|
|
283
|
+
"""TEST phase: Testing and quality assurance."""
|
|
284
|
+
self.state["current_phase"] = "TEST"
|
|
285
|
+
|
|
286
|
+
print("🧪 TEST Phase: Running tests...")
|
|
287
|
+
|
|
288
|
+
# Create testing tasks
|
|
289
|
+
tasks = [
|
|
290
|
+
self.test_agent.create_test_strategy_task(str(implement_results)),
|
|
291
|
+
self.test_agent.create_unit_tests_task(str(implement_results)),
|
|
292
|
+
self.test_agent.create_integration_tests_task(str(implement_results)),
|
|
293
|
+
self.test_agent.create_e2e_tests_task(str(implement_results)),
|
|
294
|
+
self.test_agent.create_test_execution_task(str(implement_results)),
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
# Execute tasks
|
|
298
|
+
results = []
|
|
299
|
+
for task in tasks:
|
|
300
|
+
try:
|
|
301
|
+
result = await asyncio.to_thread(task.execute)
|
|
302
|
+
results.append(result)
|
|
303
|
+
print(f"✅ Completed: {task.description[:50]}...")
|
|
304
|
+
except Exception as e:
|
|
305
|
+
self.state["errors"].append(f"TEST error: {str(e)}")
|
|
306
|
+
print(f"❌ Error in task: {str(e)}")
|
|
307
|
+
|
|
308
|
+
self.state["phase_results"]["TEST"] = {
|
|
309
|
+
"status": "completed",
|
|
310
|
+
"results": results,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return results
|
|
314
|
+
|
|
315
|
+
@listen(test_phase)
|
|
316
|
+
async def deploy_phase(self, test_results: list):
|
|
317
|
+
"""DEPLOY phase: Deployment and infrastructure setup."""
|
|
318
|
+
self.state["current_phase"] = "DEPLOY"
|
|
319
|
+
|
|
320
|
+
print("🚀 DEPLOY Phase: Setting up deployment...")
|
|
321
|
+
|
|
322
|
+
# Create deployment tasks
|
|
323
|
+
tasks = [
|
|
324
|
+
self.devops_agent.create_ci_cd_pipeline_task(str(test_results)),
|
|
325
|
+
self.devops_agent.create_docker_setup_task(str(test_results)),
|
|
326
|
+
self.devops_agent.create_azure_deployment_task(str(test_results)),
|
|
327
|
+
self.devops_agent.create_monitoring_setup_task(str(test_results)),
|
|
328
|
+
self.devops_agent.create_deployment_strategy_task(str(test_results)),
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
# Execute tasks
|
|
332
|
+
results = []
|
|
333
|
+
for task in tasks:
|
|
334
|
+
try:
|
|
335
|
+
result = await asyncio.to_thread(task.execute)
|
|
336
|
+
results.append(result)
|
|
337
|
+
print(f"✅ Completed: {task.description[:50]}...")
|
|
338
|
+
except Exception as e:
|
|
339
|
+
self.state["errors"].append(f"DEPLOY error: {str(e)}")
|
|
340
|
+
print(f"❌ Error in task: {str(e)}")
|
|
341
|
+
|
|
342
|
+
self.state["phase_results"]["DEPLOY"] = {
|
|
343
|
+
"status": "completed",
|
|
344
|
+
"results": results,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return results
|
|
348
|
+
|
|
349
|
+
@listen(deploy_phase)
|
|
350
|
+
async def doc_phase(self, deploy_results: list):
|
|
351
|
+
"""DOC phase: Documentation creation."""
|
|
352
|
+
self.state["current_phase"] = "DOC"
|
|
353
|
+
|
|
354
|
+
print("📚 DOC Phase: Creating documentation...")
|
|
355
|
+
|
|
356
|
+
# Create documentation tasks
|
|
357
|
+
tasks = [
|
|
358
|
+
self.doc_agent.create_readme_task(str(deploy_results)),
|
|
359
|
+
self.doc_agent.create_api_documentation_task(str(deploy_results)),
|
|
360
|
+
self.doc_agent.create_user_guide_task(str(deploy_results)),
|
|
361
|
+
self.doc_agent.create_developer_guide_task(str(deploy_results)),
|
|
362
|
+
self.doc_agent.create_architecture_documentation_task(str(deploy_results)),
|
|
363
|
+
]
|
|
364
|
+
|
|
365
|
+
# Execute tasks
|
|
366
|
+
results = []
|
|
367
|
+
for task in tasks:
|
|
368
|
+
try:
|
|
369
|
+
result = await asyncio.to_thread(task.execute)
|
|
370
|
+
results.append(result)
|
|
371
|
+
print(f"✅ Completed: {task.description[:50]}...")
|
|
372
|
+
except Exception as e:
|
|
373
|
+
self.state["errors"].append(f"DOC error: {str(e)}")
|
|
374
|
+
print(f"❌ Error in task: {str(e)}")
|
|
375
|
+
|
|
376
|
+
self.state["phase_results"]["DOC"] = {"status": "completed", "results": results}
|
|
377
|
+
|
|
378
|
+
return results
|
|
379
|
+
|
|
380
|
+
@listen(doc_phase)
|
|
381
|
+
async def finalize(self, doc_results: list):
|
|
382
|
+
"""Finalize the autonomous flow."""
|
|
383
|
+
print("✨ Autonomous flow completed!")
|
|
384
|
+
|
|
385
|
+
# Update project state
|
|
386
|
+
self.state["project_context"]["phases_completed"] = [
|
|
387
|
+
"LOAD",
|
|
388
|
+
"DISCOVER",
|
|
389
|
+
"PLAN",
|
|
390
|
+
"ARCHITECT",
|
|
391
|
+
"IMPLEMENT",
|
|
392
|
+
"TEST",
|
|
393
|
+
"DEPLOY",
|
|
394
|
+
"DOC",
|
|
395
|
+
]
|
|
396
|
+
self.state["project_context"]["current_phase"] = "COMPLETED"
|
|
397
|
+
|
|
398
|
+
# Save state
|
|
399
|
+
state_file = self.project_path / "MDAN-STATE.json"
|
|
400
|
+
with open(state_file, "w") as f:
|
|
401
|
+
json.dump(self.state["project_context"], f, indent=2)
|
|
402
|
+
|
|
403
|
+
print(f"✅ Project state saved to {state_file}")
|
|
404
|
+
|
|
405
|
+
if self.state["errors"]:
|
|
406
|
+
print(f"\n⚠️ Errors encountered: {len(self.state['errors'])}")
|
|
407
|
+
for error in self.state["errors"]:
|
|
408
|
+
print(f" - {error}")
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
"status": "completed",
|
|
412
|
+
"phases": self.state["phase_results"],
|
|
413
|
+
"errors": self.state["errors"],
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
def get_state(self) -> Dict[str, Any]:
|
|
417
|
+
"""Get current flow state.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Current flow state
|
|
421
|
+
"""
|
|
422
|
+
return self.state
|
|
423
|
+
|
|
424
|
+
def save_context(self, filepath: str):
|
|
425
|
+
"""Save flow context to file.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
filepath: Path to save context
|
|
429
|
+
"""
|
|
430
|
+
context = {"state": self.state, "project_path": str(self.project_path)}
|
|
431
|
+
with open(filepath, "w") as f:
|
|
432
|
+
json.dump(context, f, indent=2)
|
|
433
|
+
|
|
434
|
+
@classmethod
|
|
435
|
+
def load_context(cls, filepath: str, llm=None) -> "AutoFlow":
|
|
436
|
+
"""Load flow context from file.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
filepath: Path to load context from
|
|
440
|
+
llm: Language model instance
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
AutoFlow instance with loaded context
|
|
444
|
+
"""
|
|
445
|
+
with open(filepath, "r") as f:
|
|
446
|
+
context = json.load(f)
|
|
447
|
+
|
|
448
|
+
flow = cls(project_path=context["project_path"], llm=llm)
|
|
449
|
+
flow.state = context["state"]
|
|
450
|
+
|
|
451
|
+
return flow
|