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.
@@ -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