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,422 @@
|
|
|
1
|
+
"""Debate Flow - Multi-agent debate flow
|
|
2
|
+
|
|
3
|
+
Orchestrates debates between multiple agents to reach consensus on complex decisions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from crewai.flow import Flow, listen, start
|
|
7
|
+
from typing import Dict, Any, Optional, List
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from ..agents.product_agent import ProductAgent
|
|
13
|
+
from ..agents.architect_agent import ArchitectAgent
|
|
14
|
+
from ..agents.ux_agent import UXAgent
|
|
15
|
+
from ..agents.dev_agent import DevAgent
|
|
16
|
+
from ..agents.test_agent import TestAgent
|
|
17
|
+
from ..agents.security_agent import SecurityAgent
|
|
18
|
+
from ..agents.devops_agent import DevOpsAgent
|
|
19
|
+
from ..agents.doc_agent import DocAgent
|
|
20
|
+
|
|
21
|
+
from ..tools.sql_tool import SQLTool
|
|
22
|
+
from ..tools.serper_tool import SerperTool
|
|
23
|
+
from ..tools.file_tool import FileTool
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DebateFlow(Flow):
|
|
27
|
+
"""Multi-agent debate flow for reaching consensus on complex decisions."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
project_path: str,
|
|
32
|
+
llm=None,
|
|
33
|
+
sql_config: Optional[Dict[str, Any]] = None,
|
|
34
|
+
serper_api_key: Optional[str] = None,
|
|
35
|
+
):
|
|
36
|
+
"""Initialize Debate Flow.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
project_path: Path to the project directory
|
|
40
|
+
llm: Language model instance
|
|
41
|
+
sql_config: SQL database configuration
|
|
42
|
+
serper_api_key: Serper API key for web search
|
|
43
|
+
"""
|
|
44
|
+
super().__init__()
|
|
45
|
+
self.project_path = Path(project_path)
|
|
46
|
+
self.llm = llm
|
|
47
|
+
|
|
48
|
+
# Initialize tools
|
|
49
|
+
self.sql_tool = SQLTool(**sql_config) if sql_config else None
|
|
50
|
+
self.serper_tool = (
|
|
51
|
+
SerperTool(api_key=serper_api_key) if serper_api_key else None
|
|
52
|
+
)
|
|
53
|
+
self.file_tool = FileTool(base_path=project_path)
|
|
54
|
+
|
|
55
|
+
# Initialize all agents
|
|
56
|
+
self.product_agent = ProductAgent(
|
|
57
|
+
sql_tool=self.sql_tool,
|
|
58
|
+
serper_tool=self.serper_tool,
|
|
59
|
+
file_tool=self.file_tool,
|
|
60
|
+
llm=llm,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
self.architect_agent = ArchitectAgent(
|
|
64
|
+
sql_tool=self.sql_tool,
|
|
65
|
+
serper_tool=self.serper_tool,
|
|
66
|
+
file_tool=self.file_tool,
|
|
67
|
+
llm=llm,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
self.ux_agent = UXAgent(
|
|
71
|
+
sql_tool=self.sql_tool,
|
|
72
|
+
serper_tool=self.serper_tool,
|
|
73
|
+
file_tool=self.file_tool,
|
|
74
|
+
llm=llm,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
self.dev_agent = DevAgent(
|
|
78
|
+
sql_tool=self.sql_tool,
|
|
79
|
+
serper_tool=self.serper_tool,
|
|
80
|
+
file_tool=self.file_tool,
|
|
81
|
+
llm=llm,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self.test_agent = TestAgent(
|
|
85
|
+
sql_tool=self.sql_tool,
|
|
86
|
+
serper_tool=self.serper_tool,
|
|
87
|
+
file_tool=self.file_tool,
|
|
88
|
+
llm=llm,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
self.security_agent = SecurityAgent(
|
|
92
|
+
sql_tool=self.sql_tool,
|
|
93
|
+
serper_tool=self.serper_tool,
|
|
94
|
+
file_tool=self.file_tool,
|
|
95
|
+
llm=llm,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self.devops_agent = DevOpsAgent(
|
|
99
|
+
sql_tool=self.sql_tool,
|
|
100
|
+
serper_tool=self.serper_tool,
|
|
101
|
+
file_tool=self.file_tool,
|
|
102
|
+
llm=llm,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self.doc_agent = DocAgent(
|
|
106
|
+
sql_tool=self.sql_tool,
|
|
107
|
+
serper_tool=self.serper_tool,
|
|
108
|
+
file_tool=self.file_tool,
|
|
109
|
+
llm=llm,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Flow state
|
|
113
|
+
self.state = {
|
|
114
|
+
"current_round": 0,
|
|
115
|
+
"max_rounds": 3,
|
|
116
|
+
"topic": None,
|
|
117
|
+
"arguments": [],
|
|
118
|
+
"consensus": None,
|
|
119
|
+
"errors": [],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@start
|
|
123
|
+
async def initialize_debate(self, topic: str):
|
|
124
|
+
"""Initialize debate with topic and context."""
|
|
125
|
+
self.state["topic"] = topic
|
|
126
|
+
self.state["current_round"] = 1
|
|
127
|
+
|
|
128
|
+
print(f"🎯 Initializing debate on topic: {topic}")
|
|
129
|
+
print(f"📊 Max rounds: {self.state['max_rounds']}")
|
|
130
|
+
|
|
131
|
+
return topic
|
|
132
|
+
|
|
133
|
+
@listen(initialize_debate)
|
|
134
|
+
async def round1_initial_positions(self, topic: str):
|
|
135
|
+
"""Round 1: Agents present initial positions."""
|
|
136
|
+
print(f"\n🗣️ Round 1: Initial positions")
|
|
137
|
+
|
|
138
|
+
# Select relevant agents based on topic
|
|
139
|
+
agents = self._select_agents_for_topic(topic)
|
|
140
|
+
|
|
141
|
+
arguments = []
|
|
142
|
+
for agent_name, agent in agents.items():
|
|
143
|
+
try:
|
|
144
|
+
# Create a debate task for each agent
|
|
145
|
+
from crewai import Task
|
|
146
|
+
|
|
147
|
+
task = Task(
|
|
148
|
+
description=f"""Present your initial position on the following topic:
|
|
149
|
+
|
|
150
|
+
Topic: {topic}
|
|
151
|
+
|
|
152
|
+
Your task:
|
|
153
|
+
1. Analyze the topic from your perspective
|
|
154
|
+
2. Present your initial position
|
|
155
|
+
3. Provide supporting arguments
|
|
156
|
+
4. Identify potential concerns
|
|
157
|
+
|
|
158
|
+
Be concise but thorough in your response.
|
|
159
|
+
""",
|
|
160
|
+
agent=agent.get_agent(),
|
|
161
|
+
expected_output="Initial position with supporting arguments",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
result = await asyncio.to_thread(task.execute)
|
|
165
|
+
arguments.append({"round": 1, "agent": agent_name, "position": result})
|
|
166
|
+
print(f" ✅ {agent_name}: Position presented")
|
|
167
|
+
except Exception as e:
|
|
168
|
+
self.state["errors"].append(f"{agent_name} error: {str(e)}")
|
|
169
|
+
print(f" ❌ {agent_name}: Error - {str(e)}")
|
|
170
|
+
|
|
171
|
+
self.state["arguments"].extend(arguments)
|
|
172
|
+
return arguments
|
|
173
|
+
|
|
174
|
+
@listen(round1_initial_positions)
|
|
175
|
+
async def round2_counterarguments(self, round1_args: List[Dict]):
|
|
176
|
+
"""Round 2: Agents present counterarguments."""
|
|
177
|
+
self.state["current_round"] = 2
|
|
178
|
+
print(f"\n🔄 Round 2: Counterarguments")
|
|
179
|
+
|
|
180
|
+
# Select relevant agents
|
|
181
|
+
agents = self._select_agents_for_topic(self.state["topic"])
|
|
182
|
+
|
|
183
|
+
# Create summary of round 1 for context
|
|
184
|
+
round1_summary = "\n".join(
|
|
185
|
+
[f"{arg['agent']}: {arg['position'][:200]}..." for arg in round1_args]
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
arguments = []
|
|
189
|
+
for agent_name, agent in agents.items():
|
|
190
|
+
try:
|
|
191
|
+
from crewai import Task
|
|
192
|
+
|
|
193
|
+
task = Task(
|
|
194
|
+
description=f"""Present counterarguments to the following positions:
|
|
195
|
+
|
|
196
|
+
Topic: {self.state["topic"]}
|
|
197
|
+
|
|
198
|
+
Previous positions:
|
|
199
|
+
{round1_summary}
|
|
200
|
+
|
|
201
|
+
Your task:
|
|
202
|
+
1. Review other agents' positions
|
|
203
|
+
2. Present counterarguments where you disagree
|
|
204
|
+
3. Acknowledge valid points from others
|
|
205
|
+
4. Refine your position based on new insights
|
|
206
|
+
|
|
207
|
+
Be constructive and respectful in your counterarguments.
|
|
208
|
+
""",
|
|
209
|
+
agent=agent.get_agent(),
|
|
210
|
+
expected_output="Counterarguments with refined position",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
result = await asyncio.to_thread(task.execute)
|
|
214
|
+
arguments.append({"round": 2, "agent": agent_name, "position": result})
|
|
215
|
+
print(f" ✅ {agent_name}: Counterarguments presented")
|
|
216
|
+
except Exception as e:
|
|
217
|
+
self.state["errors"].append(f"{agent_name} error: {str(e)}")
|
|
218
|
+
print(f" ❌ {agent_name}: Error - {str(e)}")
|
|
219
|
+
|
|
220
|
+
self.state["arguments"].extend(arguments)
|
|
221
|
+
return arguments
|
|
222
|
+
|
|
223
|
+
@listen(round2_counterarguments)
|
|
224
|
+
async def round3_consensus_building(self, round2_args: List[Dict]):
|
|
225
|
+
"""Round 3: Build consensus."""
|
|
226
|
+
self.state["current_round"] = 3
|
|
227
|
+
print(f"\n🤝 Round 3: Consensus building")
|
|
228
|
+
|
|
229
|
+
# Select relevant agents
|
|
230
|
+
agents = self._select_agents_for_topic(self.state["topic"])
|
|
231
|
+
|
|
232
|
+
# Create summary of all previous rounds
|
|
233
|
+
all_args_summary = "\n".join(
|
|
234
|
+
[
|
|
235
|
+
f"Round {arg['round']} - {arg['agent']}: {arg['position'][:200]}..."
|
|
236
|
+
for arg in self.state["arguments"]
|
|
237
|
+
]
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
arguments = []
|
|
241
|
+
for agent_name, agent in agents.items():
|
|
242
|
+
try:
|
|
243
|
+
from crewai import Task
|
|
244
|
+
|
|
245
|
+
task = Task(
|
|
246
|
+
description=f"""Work towards consensus on the following topic:
|
|
247
|
+
|
|
248
|
+
Topic: {self.state["topic"]}
|
|
249
|
+
|
|
250
|
+
All previous arguments:
|
|
251
|
+
{all_args_summary}
|
|
252
|
+
|
|
253
|
+
Your task:
|
|
254
|
+
1. Review all arguments from previous rounds
|
|
255
|
+
2. Identify areas of agreement
|
|
256
|
+
3. Propose compromises where there's disagreement
|
|
257
|
+
4. Suggest a consensus position
|
|
258
|
+
5. Highlight any remaining concerns
|
|
259
|
+
|
|
260
|
+
Focus on finding common ground and practical solutions.
|
|
261
|
+
""",
|
|
262
|
+
agent=agent.get_agent(),
|
|
263
|
+
expected_output="Consensus proposal with compromises",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
result = await asyncio.to_thread(task.execute)
|
|
267
|
+
arguments.append({"round": 3, "agent": agent_name, "position": result})
|
|
268
|
+
print(f" ✅ {agent_name}: Consensus proposal presented")
|
|
269
|
+
except Exception as e:
|
|
270
|
+
self.state["errors"].append(f"{agent_name} error: {str(e)}")
|
|
271
|
+
print(f" ❌ {agent_name}: Error - {str(e)}")
|
|
272
|
+
|
|
273
|
+
self.state["arguments"].extend(arguments)
|
|
274
|
+
return arguments
|
|
275
|
+
|
|
276
|
+
@listen(round3_consensus_building)
|
|
277
|
+
async def finalize_consensus(self, round3_args: List[Dict]):
|
|
278
|
+
"""Finalize consensus and generate summary."""
|
|
279
|
+
print(f"\n✨ Finalizing consensus...")
|
|
280
|
+
|
|
281
|
+
# Use Product Agent to synthesize final consensus
|
|
282
|
+
try:
|
|
283
|
+
from crewai import Task
|
|
284
|
+
|
|
285
|
+
task = Task(
|
|
286
|
+
description=f"""Synthesize the final consensus from all debate rounds.
|
|
287
|
+
|
|
288
|
+
Topic: {self.state["topic"]}
|
|
289
|
+
|
|
290
|
+
All arguments:
|
|
291
|
+
{json.dumps(self.state["arguments"], indent=2)}
|
|
292
|
+
|
|
293
|
+
Your task:
|
|
294
|
+
1. Analyze all arguments from all rounds
|
|
295
|
+
2. Identify the consensus position
|
|
296
|
+
3. Document areas of agreement
|
|
297
|
+
4. Document areas of disagreement
|
|
298
|
+
5. Provide final recommendation
|
|
299
|
+
6. List any action items or next steps
|
|
300
|
+
|
|
301
|
+
Generate a comprehensive consensus report.
|
|
302
|
+
""",
|
|
303
|
+
agent=self.product_agent.get_agent(),
|
|
304
|
+
expected_output="Comprehensive consensus report",
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
consensus = await asyncio.to_thread(task.execute)
|
|
308
|
+
self.state["consensus"] = consensus
|
|
309
|
+
|
|
310
|
+
print("✅ Consensus reached!")
|
|
311
|
+
print(f"\n📋 Consensus Summary:\n{consensus[:500]}...")
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
self.state["errors"].append(f"Consensus synthesis error: {str(e)}")
|
|
315
|
+
print(f"❌ Error synthesizing consensus: {str(e)}")
|
|
316
|
+
self.state["consensus"] = "Unable to reach consensus due to errors"
|
|
317
|
+
|
|
318
|
+
# Save debate results
|
|
319
|
+
debate_file = self.project_path / "debate_results.json"
|
|
320
|
+
with open(debate_file, "w") as f:
|
|
321
|
+
json.dump(self.state, f, indent=2)
|
|
322
|
+
|
|
323
|
+
print(f"\n💾 Debate results saved to {debate_file}")
|
|
324
|
+
|
|
325
|
+
if self.state["errors"]:
|
|
326
|
+
print(f"\n⚠️ Errors encountered: {len(self.state['errors'])}")
|
|
327
|
+
for error in self.state["errors"]:
|
|
328
|
+
print(f" - {error}")
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
"topic": self.state["topic"],
|
|
332
|
+
"consensus": self.state["consensus"],
|
|
333
|
+
"arguments": self.state["arguments"],
|
|
334
|
+
"errors": self.state["errors"],
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
def _select_agents_for_topic(self, topic: str) -> Dict[str, Any]:
|
|
338
|
+
"""Select relevant agents based on topic keywords.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
topic: Debate topic
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Dictionary of relevant agents
|
|
345
|
+
"""
|
|
346
|
+
topic_lower = topic.lower()
|
|
347
|
+
|
|
348
|
+
# Default agents for most topics
|
|
349
|
+
agents = {
|
|
350
|
+
"product": self.product_agent,
|
|
351
|
+
"architect": self.architect_agent,
|
|
352
|
+
"dev": self.dev_agent,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
# Add agents based on topic keywords
|
|
356
|
+
if any(
|
|
357
|
+
keyword in topic_lower
|
|
358
|
+
for keyword in ["user", "ux", "ui", "design", "interface"]
|
|
359
|
+
):
|
|
360
|
+
agents["ux"] = self.ux_agent
|
|
361
|
+
|
|
362
|
+
if any(
|
|
363
|
+
keyword in topic_lower for keyword in ["test", "quality", "verify", "bug"]
|
|
364
|
+
):
|
|
365
|
+
agents["test"] = self.test_agent
|
|
366
|
+
|
|
367
|
+
if any(
|
|
368
|
+
keyword in topic_lower
|
|
369
|
+
for keyword in ["security", "vulnerability", "auth", "encrypt"]
|
|
370
|
+
):
|
|
371
|
+
agents["security"] = self.security_agent
|
|
372
|
+
|
|
373
|
+
if any(
|
|
374
|
+
keyword in topic_lower
|
|
375
|
+
for keyword in ["deploy", "ci/cd", "infrastructure", "cloud", "azure"]
|
|
376
|
+
):
|
|
377
|
+
agents["devops"] = self.devops_agent
|
|
378
|
+
|
|
379
|
+
if any(
|
|
380
|
+
keyword in topic_lower
|
|
381
|
+
for keyword in ["document", "guide", "readme", "api doc"]
|
|
382
|
+
):
|
|
383
|
+
agents["doc"] = self.doc_agent
|
|
384
|
+
|
|
385
|
+
return agents
|
|
386
|
+
|
|
387
|
+
def get_state(self) -> Dict[str, Any]:
|
|
388
|
+
"""Get current flow state.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Current flow state
|
|
392
|
+
"""
|
|
393
|
+
return self.state
|
|
394
|
+
|
|
395
|
+
def save_context(self, filepath: str):
|
|
396
|
+
"""Save flow context to file.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
filepath: Path to save context
|
|
400
|
+
"""
|
|
401
|
+
context = {"state": self.state, "project_path": str(self.project_path)}
|
|
402
|
+
with open(filepath, "w") as f:
|
|
403
|
+
json.dump(context, f, indent=2)
|
|
404
|
+
|
|
405
|
+
@classmethod
|
|
406
|
+
def load_context(cls, filepath: str, llm=None) -> "DebateFlow":
|
|
407
|
+
"""Load flow context from file.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
filepath: Path to load context from
|
|
411
|
+
llm: Language model instance
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
DebateFlow instance with loaded context
|
|
415
|
+
"""
|
|
416
|
+
with open(filepath, "r") as f:
|
|
417
|
+
context = json.load(f)
|
|
418
|
+
|
|
419
|
+
flow = cls(project_path=context["project_path"], llm=llm)
|
|
420
|
+
flow.state = context["state"]
|
|
421
|
+
|
|
422
|
+
return flow
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Discovery Flow - DISCOVER phase flow
|
|
2
|
+
|
|
3
|
+
Orchestrates the DISCOVER phase: requirements gathering, PRD creation, user stories, personas.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from crewai.flow import Flow, listen, start
|
|
7
|
+
from typing import Dict, Any, Optional
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from ..agents.product_agent import ProductAgent
|
|
13
|
+
|
|
14
|
+
from ..tools.sql_tool import SQLTool
|
|
15
|
+
from ..tools.serper_tool import SerperTool
|
|
16
|
+
from ..tools.file_tool import FileTool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DiscoveryFlow(Flow):
|
|
20
|
+
"""DISCOVER phase flow for requirements gathering."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
project_path: str,
|
|
25
|
+
llm=None,
|
|
26
|
+
sql_config: Optional[Dict[str, Any]] = None,
|
|
27
|
+
serper_api_key: Optional[str] = None,
|
|
28
|
+
):
|
|
29
|
+
"""Initialize Discovery Flow.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
project_path: Path to the project directory
|
|
33
|
+
llm: Language model instance
|
|
34
|
+
sql_config: SQL database configuration
|
|
35
|
+
serper_api_key: Serper API key for web search
|
|
36
|
+
"""
|
|
37
|
+
super().__init__()
|
|
38
|
+
self.project_path = Path(project_path)
|
|
39
|
+
self.llm = llm
|
|
40
|
+
|
|
41
|
+
# Initialize tools
|
|
42
|
+
self.sql_tool = SQLTool(**sql_config) if sql_config else None
|
|
43
|
+
self.serper_tool = (
|
|
44
|
+
SerperTool(api_key=serper_api_key) if serper_api_key else None
|
|
45
|
+
)
|
|
46
|
+
self.file_tool = FileTool(base_path=project_path)
|
|
47
|
+
|
|
48
|
+
# Initialize agent
|
|
49
|
+
self.product_agent = ProductAgent(
|
|
50
|
+
sql_tool=self.sql_tool,
|
|
51
|
+
serper_tool=self.serper_tool,
|
|
52
|
+
file_tool=self.file_tool,
|
|
53
|
+
llm=llm,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Flow state
|
|
57
|
+
self.state = {
|
|
58
|
+
"current_step": None,
|
|
59
|
+
"project_context": {},
|
|
60
|
+
"step_results": {},
|
|
61
|
+
"errors": [],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@start
|
|
65
|
+
async def load_context(self, user_input: str):
|
|
66
|
+
"""Load project context and user input."""
|
|
67
|
+
self.state["current_step"] = "load_context"
|
|
68
|
+
|
|
69
|
+
print("📂 Loading project context...")
|
|
70
|
+
|
|
71
|
+
# Load project state if exists
|
|
72
|
+
state_file = self.project_path / "MDAN-STATE.json"
|
|
73
|
+
if state_file.exists():
|
|
74
|
+
with open(state_file, "r") as f:
|
|
75
|
+
self.state["project_context"] = json.load(f)
|
|
76
|
+
else:
|
|
77
|
+
self.state["project_context"] = {
|
|
78
|
+
"project_name": self.project_path.name,
|
|
79
|
+
"user_input": user_input,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
self.state["step_results"]["load_context"] = {
|
|
83
|
+
"status": "completed",
|
|
84
|
+
"context": self.state["project_context"],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return self.state["project_context"]
|
|
88
|
+
|
|
89
|
+
@listen(load_context)
|
|
90
|
+
async def create_prd(self, context: Dict[str, Any]):
|
|
91
|
+
"""Create Product Requirements Document."""
|
|
92
|
+
self.state["current_step"] = "create_prd"
|
|
93
|
+
|
|
94
|
+
print("📝 Creating Product Requirements Document...")
|
|
95
|
+
|
|
96
|
+
task = self.product_agent.create_prd_task(str(context))
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
result = await asyncio.to_thread(task.execute)
|
|
100
|
+
self.state["step_results"]["create_prd"] = {
|
|
101
|
+
"status": "completed",
|
|
102
|
+
"result": result,
|
|
103
|
+
}
|
|
104
|
+
print("✅ PRD created successfully")
|
|
105
|
+
return result
|
|
106
|
+
except Exception as e:
|
|
107
|
+
self.state["errors"].append(f"PRD creation error: {str(e)}")
|
|
108
|
+
print(f"❌ Error creating PRD: {str(e)}")
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
@listen(create_prd)
|
|
112
|
+
async def create_user_stories(self, prd_result: Any):
|
|
113
|
+
"""Create user stories."""
|
|
114
|
+
self.state["current_step"] = "create_user_stories"
|
|
115
|
+
|
|
116
|
+
print("📋 Creating user stories...")
|
|
117
|
+
|
|
118
|
+
task = self.product_agent.create_user_stories_task(str(prd_result))
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
result = await asyncio.to_thread(task.execute)
|
|
122
|
+
self.state["step_results"]["create_user_stories"] = {
|
|
123
|
+
"status": "completed",
|
|
124
|
+
"result": result,
|
|
125
|
+
}
|
|
126
|
+
print("✅ User stories created successfully")
|
|
127
|
+
return result
|
|
128
|
+
except Exception as e:
|
|
129
|
+
self.state["errors"].append(f"User stories creation error: {str(e)}")
|
|
130
|
+
print(f"❌ Error creating user stories: {str(e)}")
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
@listen(create_user_stories)
|
|
134
|
+
async def create_personas(self, user_stories_result: Any):
|
|
135
|
+
"""Create user personas."""
|
|
136
|
+
self.state["current_step"] = "create_personas"
|
|
137
|
+
|
|
138
|
+
print("👥 Creating user personas...")
|
|
139
|
+
|
|
140
|
+
task = self.product_agent.create_personas_task(str(user_stories_result))
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
result = await asyncio.to_thread(task.execute)
|
|
144
|
+
self.state["step_results"]["create_personas"] = {
|
|
145
|
+
"status": "completed",
|
|
146
|
+
"result": result,
|
|
147
|
+
}
|
|
148
|
+
print("✅ User personas created successfully")
|
|
149
|
+
return result
|
|
150
|
+
except Exception as e:
|
|
151
|
+
self.state["errors"].append(f"Personas creation error: {str(e)}")
|
|
152
|
+
print(f"❌ Error creating personas: {str(e)}")
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
@listen(create_personas)
|
|
156
|
+
async def prioritize_features(self, personas_result: Any):
|
|
157
|
+
"""Prioritize features."""
|
|
158
|
+
self.state["current_step"] = "prioritize_features"
|
|
159
|
+
|
|
160
|
+
print("🎯 Prioritizing features...")
|
|
161
|
+
|
|
162
|
+
task = self.product_agent.create_feature_prioritization_task(
|
|
163
|
+
str(personas_result)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
result = await asyncio.to_thread(task.execute)
|
|
168
|
+
self.state["step_results"]["prioritize_features"] = {
|
|
169
|
+
"status": "completed",
|
|
170
|
+
"result": result,
|
|
171
|
+
}
|
|
172
|
+
print("✅ Features prioritized successfully")
|
|
173
|
+
return result
|
|
174
|
+
except Exception as e:
|
|
175
|
+
self.state["errors"].append(f"Feature prioritization error: {str(e)}")
|
|
176
|
+
print(f"❌ Error prioritizing features: {str(e)}")
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
@listen(prioritize_features)
|
|
180
|
+
async def create_acceptance_criteria(self, features_result: Any):
|
|
181
|
+
"""Create acceptance criteria."""
|
|
182
|
+
self.state["current_step"] = "create_acceptance_criteria"
|
|
183
|
+
|
|
184
|
+
print("✅ Creating acceptance criteria...")
|
|
185
|
+
|
|
186
|
+
task = self.product_agent.create_acceptance_criteria_task(str(features_result))
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
result = await asyncio.to_thread(task.execute)
|
|
190
|
+
self.state["step_results"]["create_acceptance_criteria"] = {
|
|
191
|
+
"status": "completed",
|
|
192
|
+
"result": result,
|
|
193
|
+
}
|
|
194
|
+
print("✅ Acceptance criteria created successfully")
|
|
195
|
+
return result
|
|
196
|
+
except Exception as e:
|
|
197
|
+
self.state["errors"].append(f"Acceptance criteria creation error: {str(e)}")
|
|
198
|
+
print(f"❌ Error creating acceptance criteria: {str(e)}")
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
@listen(create_acceptance_criteria)
|
|
202
|
+
async def finalize_discovery(self, acceptance_criteria_result: Any):
|
|
203
|
+
"""Finalize DISCOVER phase."""
|
|
204
|
+
print("✨ DISCOVER phase completed!")
|
|
205
|
+
|
|
206
|
+
# Update project state
|
|
207
|
+
self.state["project_context"]["phases_completed"] = self.state[
|
|
208
|
+
"project_context"
|
|
209
|
+
].get("phases_completed", [])
|
|
210
|
+
if "DISCOVER" not in self.state["project_context"]["phases_completed"]:
|
|
211
|
+
self.state["project_context"]["phases_completed"].append("DISCOVER")
|
|
212
|
+
self.state["project_context"]["current_phase"] = "DISCOVER_COMPLETED"
|
|
213
|
+
|
|
214
|
+
# Save state
|
|
215
|
+
state_file = self.project_path / "MDAN-STATE.json"
|
|
216
|
+
with open(state_file, "w") as f:
|
|
217
|
+
json.dump(self.state["project_context"], f, indent=2)
|
|
218
|
+
|
|
219
|
+
print(f"✅ Project state saved to {state_file}")
|
|
220
|
+
|
|
221
|
+
if self.state["errors"]:
|
|
222
|
+
print(f"\n⚠️ Errors encountered: {len(self.state['errors'])}")
|
|
223
|
+
for error in self.state["errors"]:
|
|
224
|
+
print(f" - {error}")
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
"status": "completed",
|
|
228
|
+
"steps": self.state["step_results"],
|
|
229
|
+
"errors": self.state["errors"],
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
def get_state(self) -> Dict[str, Any]:
|
|
233
|
+
"""Get current flow state.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Current flow state
|
|
237
|
+
"""
|
|
238
|
+
return self.state
|
|
239
|
+
|
|
240
|
+
def save_context(self, filepath: str):
|
|
241
|
+
"""Save flow context to file.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
filepath: Path to save context
|
|
245
|
+
"""
|
|
246
|
+
context = {"state": self.state, "project_path": str(self.project_path)}
|
|
247
|
+
with open(filepath, "w") as f:
|
|
248
|
+
json.dump(context, f, indent=2)
|
|
249
|
+
|
|
250
|
+
@classmethod
|
|
251
|
+
def load_context(cls, filepath: str, llm=None) -> "DiscoveryFlow":
|
|
252
|
+
"""Load flow context from file.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
filepath: Path to load context from
|
|
256
|
+
llm: Language model instance
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
DiscoveryFlow instance with loaded context
|
|
260
|
+
"""
|
|
261
|
+
with open(filepath, "r") as f:
|
|
262
|
+
context = json.load(f)
|
|
263
|
+
|
|
264
|
+
flow = cls(project_path=context["project_path"], llm=llm)
|
|
265
|
+
flow.state = context["state"]
|
|
266
|
+
|
|
267
|
+
return flow
|