ayushm98 commited on
Commit
02d5eaa
·
1 Parent(s): 454104f

v3.3.0: Add clarifying questions before planning

Browse files
Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
  # HuggingFace Spaces Dockerfile for CodePilot
2
- # BUILD_VERSION: 8 (v3.2.1 coder max 8 iterations)
3
  FROM python:3.11-slim
4
 
5
  # Set working directory
 
1
  # HuggingFace Spaces Dockerfile for CodePilot
2
+ # BUILD_VERSION: 9 (v3.3.0 clarifying questions)
3
  FROM python:3.11-slim
4
 
5
  # Set working directory
chainlit_app.py CHANGED
@@ -20,8 +20,8 @@ from concurrent.futures import ThreadPoolExecutor
20
  # ============================================================
21
  # STARTUP VERSION CHECK - Change this to detect if rebuild worked
22
  # ============================================================
23
- APP_VERSION = "3.2.1-coder-max8"
24
- BUILD_ID = "2024-12-19-v7"
25
  print("=" * 60)
26
  print(f"[STARTUP] CodePilot Chainlit App")
27
  print(f"[STARTUP] APP_VERSION: {APP_VERSION}")
@@ -121,6 +121,83 @@ async def main(message: cl.Message):
121
  # Get orchestrator
122
  orchestrator: Orchestrator = cl.user_session.get("orchestrator")
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  # Check for GitHub URL in message
125
  github_url = extract_github_url(message.content)
126
  task_context = ""
@@ -319,6 +396,18 @@ AVAILABLE TOOLS:
319
  log_msg.content = f"## Execution Log\n```\n{final_logs}\n```"
320
  await log_msg.update()
321
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  # Send results summary
323
  summary_lines = []
324
 
 
20
  # ============================================================
21
  # STARTUP VERSION CHECK - Change this to detect if rebuild worked
22
  # ============================================================
23
+ APP_VERSION = "3.3.0-clarify"
24
+ BUILD_ID = "2024-12-19-v8"
25
  print("=" * 60)
26
  print(f"[STARTUP] CodePilot Chainlit App")
27
  print(f"[STARTUP] APP_VERSION: {APP_VERSION}")
 
121
  # Get orchestrator
122
  orchestrator: Orchestrator = cl.user_session.get("orchestrator")
123
 
124
+ # Check if we're waiting for clarification answers
125
+ if cl.user_session.get("waiting_for_clarification"):
126
+ cl.user_session.set("waiting_for_clarification", False)
127
+ user_answers = message.content
128
+
129
+ await cl.Message(content="Got it! Let me create the plan with your clarifications...").send()
130
+
131
+ # Resume the orchestrator with user answers
132
+ log_msg = cl.Message(content="")
133
+ await log_msg.send()
134
+
135
+ try:
136
+ captured_output = io.StringIO()
137
+
138
+ def resume_orchestrator():
139
+ with redirect_stdout(captured_output), redirect_stderr(captured_output):
140
+ return orchestrator.resume_after_clarification(user_answers)
141
+
142
+ loop = asyncio.get_event_loop()
143
+ executor = ThreadPoolExecutor(max_workers=1)
144
+ future = loop.run_in_executor(executor, resume_orchestrator)
145
+
146
+ # Track tokens
147
+ total_prompt_tokens = 0
148
+ total_completion_tokens = 0
149
+ total_tokens = 0
150
+ seen_token_lines = set()
151
+
152
+ # Stream logs
153
+ accumulated_logs = ""
154
+ while not future.done():
155
+ await asyncio.sleep(0.5)
156
+ current_output = captured_output.getvalue()
157
+ if current_output != accumulated_logs:
158
+ accumulated_logs = current_output
159
+ filtered_lines = []
160
+ for line in accumulated_logs.split('\n'):
161
+ if 'Tokens:' in line and line not in seen_token_lines:
162
+ seen_token_lines.add(line)
163
+ try:
164
+ parts = line.split('Tokens:')[1].strip()
165
+ prompt = int(parts.split('prompt')[0].strip())
166
+ completion = int(parts.split('+')[1].split('completion')[0].strip())
167
+ total_prompt_tokens += prompt
168
+ total_completion_tokens += completion
169
+ total_tokens += (prompt + completion)
170
+ except:
171
+ pass
172
+ if any(skip in line for skip in ['Tokens:', 'Batches:', '|##', 'it/s]']):
173
+ continue
174
+ if any(keep in line for keep in [
175
+ '[CLASSIFIER]', '[ORCHESTRATOR]', '[PLANNER]', '[CODER]', '[REVIEWER]',
176
+ '[EXPLORER]', 'Calling tool:', 'Transitioning', 'APPROVED', 'REJECTED'
177
+ ]):
178
+ filtered_lines.append(line)
179
+ filtered_output = '\n'.join(filtered_lines)
180
+ input_cost = (total_prompt_tokens / 1000000) * 3.0
181
+ output_cost = (total_completion_tokens / 1000000) * 15.0
182
+ total_cost = input_cost + output_cost
183
+ usage_summary = f"\n\nCREDITS: ${total_cost:.4f}"
184
+ log_msg.content = f"```\n{filtered_output}{usage_summary}\n```"
185
+ await log_msg.update()
186
+
187
+ result = await future
188
+ # Continue to show results (handled by falling through to normal result handling below)
189
+ # For now, show summary directly
190
+ summary = f"## Result\n**Status:** {result.get('status')}\n"
191
+ if result.get('code_changes'):
192
+ summary += f"**Files created:** {len(result['code_changes'])}\n"
193
+ summary += f"**Cost:** ${total_cost:.4f}"
194
+ await cl.Message(content=summary).send()
195
+ return
196
+
197
+ except Exception as e:
198
+ await cl.Message(content=f"Error resuming: {str(e)}").send()
199
+ return
200
+
201
  # Check for GitHub URL in message
202
  github_url = extract_github_url(message.content)
203
  task_context = ""
 
396
  log_msg.content = f"## Execution Log\n```\n{final_logs}\n```"
397
  await log_msg.update()
398
 
399
+ # Check if we need clarification from user
400
+ if result.get('status') == 'clarifying' and result.get('clarifying_questions'):
401
+ questions = result['clarifying_questions']
402
+ # Store that we're waiting for clarification
403
+ cl.user_session.set("waiting_for_clarification", True)
404
+
405
+ await cl.Message(
406
+ content=f"## Before I proceed, I have some questions:\n\n{questions}\n\n"
407
+ f"**Please answer the questions above so I can create a better plan.**"
408
+ ).send()
409
+ return # Wait for user to respond
410
+
411
  # Send results summary
412
  summary_lines = []
413
 
codepilot/agents/orchestrator.py CHANGED
@@ -9,7 +9,7 @@ The orchestrator is the "brain" that:
9
  """
10
 
11
  # VERSION CHECK - If you see this, new code is running!
12
- ORCHESTRATOR_VERSION = "3.2.1-coder-max8"
13
  print(f"[ORCHESTRATOR] ========== LOADING VERSION {ORCHESTRATOR_VERSION} ==========")
14
 
15
  from enum import Enum
@@ -23,7 +23,8 @@ from codepilot.agents.explorer_agent import ExplorerAgent
23
 
24
  class AgentState(Enum):
25
  """Possible states in the multi-agent workflow"""
26
- EXPLORING = "exploring" # NEW - Explorer gathers context first
 
27
  PLANNING = "planning"
28
  CODING = "coding"
29
  REVIEWING = "reviewing"
@@ -39,7 +40,9 @@ class TaskContext:
39
  Think of this as a clipboard that agents write to and read from.
40
  """
41
  task_description: str # Original task from user
42
- exploration_context: Optional[str] = None # NEW - Created by Explorer
 
 
43
  plan: Optional[str] = None # Created by Planner (uses exploration_context)
44
  code_changes: Optional[Dict[str, str]] = None # Created by Coder
45
  review_feedback: Optional[str] = None # Created by Reviewer
@@ -238,20 +241,21 @@ class Orchestrator:
238
 
239
  def _run_full_workflow(self, task: str) -> Dict[str, Any]:
240
  """
241
- Run the full Explorer → Planner → Coder → Reviewer workflow.
242
 
243
- v3.0: Now starts with Explorer to gather context efficiently,
244
- then Planner creates plan based on exploration (no tools).
245
 
246
  Args:
247
  task: User's task description
248
 
249
  Returns:
250
  Result dict with status, changes, and messages
 
251
  """
252
- # Initialize context
253
- self.context = TaskContext(task_description=task)
254
- self.state = AgentState.EXPLORING # v3.0: Start with EXPLORING
 
255
 
256
  # Main state machine loop
257
  while self.state not in [AgentState.COMPLETE, AgentState.FAILED]:
@@ -263,7 +267,13 @@ class Orchestrator:
263
 
264
  # Execute current state
265
  if self.state == AgentState.EXPLORING:
266
- self._execute_exploring() # NEW - Explorer first
 
 
 
 
 
 
267
 
268
  elif self.state == AgentState.PLANNING:
269
  self._execute_planning()
@@ -279,6 +289,19 @@ class Orchestrator:
279
  # Return final result
280
  return self._build_result()
281
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  def _execute_exploring(self):
283
  """
284
  Execute exploring state: call Explorer agent to gather context.
@@ -288,7 +311,7 @@ class Orchestrator:
288
  - Find relevant files, functions, and patterns
289
  - Return context summary for Planner to use
290
 
291
- Transition: Always go to PLANNING next
292
  """
293
  print(f"\n[ORCHESTRATOR] State: EXPLORING")
294
  print(f"[ORCHESTRATOR] Running Explorer to gather codebase context...")
@@ -299,9 +322,51 @@ class Orchestrator:
299
  # Store exploration context for Planner to use
300
  self.context.exploration_context = exploration_result
301
 
302
- # Transition to planning
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  self.state = AgentState.PLANNING
304
- print(f"[ORCHESTRATOR] Exploration complete. Transitioning to PLANNING")
305
 
306
  def _execute_planning(self):
307
  """
@@ -309,18 +374,23 @@ class Orchestrator:
309
 
310
  Planner's job (v3.0):
311
  - Receive exploration context from Explorer
 
312
  - Create step-by-step plan based on exploration (NO TOOLS)
313
  - Pure LLM reasoning - no searching
314
 
315
  Transition: Always go to CODING next
316
  """
317
  print(f"\n[ORCHESTRATOR] State: PLANNING")
318
- print(f"[ORCHESTRATOR] Using exploration context to create plan (no tools)...")
 
 
 
319
 
320
- # Call the Planner with exploration context (v3.0: Planner has no tools)
321
  self.context.plan = self.planner.run(
322
  task=self.context.task_description,
323
- exploration_context=self.context.exploration_context
 
324
  )
325
 
326
  # Transition to coding
@@ -403,6 +473,8 @@ class Orchestrator:
403
  'status': self.state.value,
404
  'success': self.state == AgentState.COMPLETE,
405
  'task': self.context.task_description,
 
 
406
  'plan': self.context.plan,
407
  'code_changes': self.context.code_changes,
408
  'review_feedback': self.context.review_feedback,
 
9
  """
10
 
11
  # VERSION CHECK - If you see this, new code is running!
12
+ ORCHESTRATOR_VERSION = "3.3.0-clarify"
13
  print(f"[ORCHESTRATOR] ========== LOADING VERSION {ORCHESTRATOR_VERSION} ==========")
14
 
15
  from enum import Enum
 
23
 
24
  class AgentState(Enum):
25
  """Possible states in the multi-agent workflow"""
26
+ EXPLORING = "exploring" # Explorer gathers context first
27
+ CLARIFYING = "clarifying" # NEW - Ask user clarifying questions
28
  PLANNING = "planning"
29
  CODING = "coding"
30
  REVIEWING = "reviewing"
 
40
  Think of this as a clipboard that agents write to and read from.
41
  """
42
  task_description: str # Original task from user
43
+ exploration_context: Optional[str] = None # Created by Explorer
44
+ clarifying_questions: Optional[str] = None # NEW - Questions from Planner
45
+ user_answers: Optional[str] = None # NEW - User's answers to questions
46
  plan: Optional[str] = None # Created by Planner (uses exploration_context)
47
  code_changes: Optional[Dict[str, str]] = None # Created by Coder
48
  review_feedback: Optional[str] = None # Created by Reviewer
 
241
 
242
  def _run_full_workflow(self, task: str) -> Dict[str, Any]:
243
  """
244
+ Run the full Explorer → Clarify → Planner → Coder → Reviewer workflow.
245
 
246
+ v3.3: Now includes clarification step before planning.
 
247
 
248
  Args:
249
  task: User's task description
250
 
251
  Returns:
252
  Result dict with status, changes, and messages
253
+ If questions need to be asked, returns with state='clarifying'
254
  """
255
+ # Initialize context if not already done
256
+ if self.context is None:
257
+ self.context = TaskContext(task_description=task)
258
+ self.state = AgentState.EXPLORING
259
 
260
  # Main state machine loop
261
  while self.state not in [AgentState.COMPLETE, AgentState.FAILED]:
 
267
 
268
  # Execute current state
269
  if self.state == AgentState.EXPLORING:
270
+ self._execute_exploring()
271
+
272
+ elif self.state == AgentState.CLARIFYING:
273
+ self._execute_clarifying()
274
+ # If questions were generated, pause and return to Chainlit
275
+ if self.context.clarifying_questions and self.state == AgentState.CLARIFYING:
276
+ return self._build_result() # Return with questions, Chainlit will resume
277
 
278
  elif self.state == AgentState.PLANNING:
279
  self._execute_planning()
 
289
  # Return final result
290
  return self._build_result()
291
 
292
+ def resume_after_clarification(self, user_answers: str) -> Dict[str, Any]:
293
+ """
294
+ Resume workflow after user provides answers to clarifying questions.
295
+
296
+ Args:
297
+ user_answers: User's answers to the questions
298
+
299
+ Returns:
300
+ Result dict from continued workflow
301
+ """
302
+ self.provide_user_answers(user_answers)
303
+ return self._run_full_workflow(self.context.task_description)
304
+
305
  def _execute_exploring(self):
306
  """
307
  Execute exploring state: call Explorer agent to gather context.
 
311
  - Find relevant files, functions, and patterns
312
  - Return context summary for Planner to use
313
 
314
+ Transition: Always go to CLARIFYING next
315
  """
316
  print(f"\n[ORCHESTRATOR] State: EXPLORING")
317
  print(f"[ORCHESTRATOR] Running Explorer to gather codebase context...")
 
322
  # Store exploration context for Planner to use
323
  self.context.exploration_context = exploration_result
324
 
325
+ # Transition to clarifying (ask user questions before planning)
326
+ self.state = AgentState.CLARIFYING
327
+ print(f"[ORCHESTRATOR] Exploration complete. Transitioning to CLARIFYING")
328
+
329
+ def _execute_clarifying(self):
330
+ """
331
+ Execute clarifying state: ask user clarifying questions.
332
+
333
+ Planner generates questions, user answers, then we proceed to planning.
334
+ If no questions needed, skip straight to planning.
335
+
336
+ Transition: Go to PLANNING (with or without answers)
337
+ """
338
+ print(f"\n[ORCHESTRATOR] State: CLARIFYING")
339
+ print(f"[ORCHESTRATOR] Generating clarifying questions...")
340
+
341
+ # Get clarifying questions from Planner
342
+ questions = self.planner.get_clarifying_questions(
343
+ task=self.context.task_description,
344
+ exploration_context=self.context.exploration_context
345
+ )
346
+
347
+ if questions:
348
+ # Store questions - Chainlit will handle getting user answers
349
+ self.context.clarifying_questions = questions
350
+ print(f"[ORCHESTRATOR] Questions generated. Waiting for user answers...")
351
+ # Note: We'll pause here and let Chainlit get user input
352
+ # The state stays at CLARIFYING until user answers are provided
353
+ else:
354
+ # No questions needed, go straight to planning
355
+ print(f"[ORCHESTRATOR] No clarifying questions needed. Transitioning to PLANNING")
356
+ self.state = AgentState.PLANNING
357
+
358
+ def provide_user_answers(self, answers: str):
359
+ """
360
+ Provide user answers to clarifying questions and continue workflow.
361
+
362
+ Called by Chainlit after user responds to questions.
363
+
364
+ Args:
365
+ answers: User's answers to the clarifying questions
366
+ """
367
+ self.context.user_answers = answers
368
  self.state = AgentState.PLANNING
369
+ print(f"[ORCHESTRATOR] User answers received. Transitioning to PLANNING")
370
 
371
  def _execute_planning(self):
372
  """
 
374
 
375
  Planner's job (v3.0):
376
  - Receive exploration context from Explorer
377
+ - Use user answers if clarifying questions were asked
378
  - Create step-by-step plan based on exploration (NO TOOLS)
379
  - Pure LLM reasoning - no searching
380
 
381
  Transition: Always go to CODING next
382
  """
383
  print(f"\n[ORCHESTRATOR] State: PLANNING")
384
+ if self.context.user_answers:
385
+ print(f"[ORCHESTRATOR] Using exploration context + user answers to create plan...")
386
+ else:
387
+ print(f"[ORCHESTRATOR] Using exploration context to create plan (no tools)...")
388
 
389
+ # Call the Planner with exploration context and user answers
390
  self.context.plan = self.planner.run(
391
  task=self.context.task_description,
392
+ exploration_context=self.context.exploration_context,
393
+ user_answers=self.context.user_answers
394
  )
395
 
396
  # Transition to coding
 
473
  'status': self.state.value,
474
  'success': self.state == AgentState.COMPLETE,
475
  'task': self.context.task_description,
476
+ 'clarifying_questions': self.context.clarifying_questions, # NEW
477
+ 'user_answers': self.context.user_answers, # NEW
478
  'plan': self.context.plan,
479
  'code_changes': self.context.code_changes,
480
  'review_feedback': self.context.review_feedback,
codepilot/agents/planner_agent.py CHANGED
@@ -19,6 +19,24 @@ from codepilot.agents.conversation import ConversationManager
19
  from typing import Optional
20
 
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  # Planner's system prompt (v3.0 - no tools, just planning)
23
  PLANNER_SYSTEM_PROMPT = """You are a senior software architect and planning expert.
24
 
@@ -71,7 +89,59 @@ class PlannerAgent:
71
  else:
72
  self.client = OpenAIClient(model=model)
73
 
74
- def run(self, task: str, exploration_context: Optional[str] = None) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  """
76
  Create a plan for the given task using exploration context.
77
 
@@ -80,6 +150,7 @@ class PlannerAgent:
80
  Args:
81
  task: Task description (e.g., "Add login feature")
82
  exploration_context: Context gathered by Explorer agent
 
83
 
84
  Returns:
85
  Detailed implementation plan as a string
@@ -93,7 +164,15 @@ class PlannerAgent:
93
 
94
  === TASK ===
95
  {task}
 
 
 
 
 
 
 
96
 
 
97
  Based on the exploration results above, create a detailed implementation plan.
98
  Include specific file paths, function names, and step-by-step instructions for the Coder agent.
99
  """
@@ -101,7 +180,13 @@ Include specific file paths, function names, and step-by-step instructions for t
101
  # Fallback if no exploration context (shouldn't happen in v3.0)
102
  user_prompt = f"""=== TASK ===
103
  {task}
104
-
 
 
 
 
 
 
105
  Create a detailed implementation plan for this task.
106
  Note: No exploration context was provided, so make reasonable assumptions about the codebase structure.
107
  """
 
19
  from typing import Optional
20
 
21
 
22
+ # Planner's system prompt for asking clarifying questions
23
+ PLANNER_QUESTIONS_PROMPT = """You are a senior software architect helping clarify requirements.
24
+
25
+ Based on the task and codebase exploration, generate 2-4 SHORT clarifying questions that will help create a better implementation plan.
26
+
27
+ IMPORTANT:
28
+ - Only ask questions if something is genuinely unclear or there are multiple valid approaches
29
+ - Questions should be answerable in 1-2 sentences
30
+ - Focus on: location, naming, behavior, edge cases
31
+ - If the task is already clear, respond with: "NO_QUESTIONS_NEEDED"
32
+
33
+ Format your response as a numbered list:
34
+ 1. Question one?
35
+ 2. Question two?
36
+
37
+ Or just: NO_QUESTIONS_NEEDED
38
+ """
39
+
40
  # Planner's system prompt (v3.0 - no tools, just planning)
41
  PLANNER_SYSTEM_PROMPT = """You are a senior software architect and planning expert.
42
 
 
89
  else:
90
  self.client = OpenAIClient(model=model)
91
 
92
+ def get_clarifying_questions(self, task: str, exploration_context: Optional[str] = None) -> Optional[str]:
93
+ """
94
+ Generate clarifying questions before creating the plan.
95
+
96
+ Args:
97
+ task: Task description
98
+ exploration_context: Context gathered by Explorer agent
99
+
100
+ Returns:
101
+ Questions as a string, or None if no questions needed
102
+ """
103
+ print(f"[PLANNER] Generating clarifying questions...")
104
+
105
+ # Build prompt
106
+ if exploration_context:
107
+ user_prompt = f"""=== EXPLORATION RESULTS ===
108
+ {exploration_context}
109
+
110
+ === TASK ===
111
+ {task}
112
+
113
+ Based on the above, what clarifying questions would help create a better implementation plan?
114
+ """
115
+ else:
116
+ user_prompt = f"""=== TASK ===
117
+ {task}
118
+
119
+ What clarifying questions would help create a better implementation plan?
120
+ """
121
+
122
+ # Create conversation
123
+ conversation = ConversationManager()
124
+ conversation.add_message("system", PLANNER_QUESTIONS_PROMPT)
125
+ conversation.add_message("user", user_prompt)
126
+
127
+ # Single LLM call
128
+ response = self.client.chat(
129
+ messages=conversation.get_messages(),
130
+ tools=None,
131
+ max_tokens=500
132
+ )
133
+
134
+ questions = response.choices[0].message.content
135
+
136
+ # Check if questions are needed
137
+ if questions and "NO_QUESTIONS_NEEDED" in questions.upper():
138
+ print(f"[PLANNER] No clarifying questions needed")
139
+ return None
140
+
141
+ print(f"[PLANNER] Generated clarifying questions")
142
+ return questions
143
+
144
+ def run(self, task: str, exploration_context: Optional[str] = None, user_answers: Optional[str] = None) -> str:
145
  """
146
  Create a plan for the given task using exploration context.
147
 
 
150
  Args:
151
  task: Task description (e.g., "Add login feature")
152
  exploration_context: Context gathered by Explorer agent
153
+ user_answers: User's answers to clarifying questions (optional)
154
 
155
  Returns:
156
  Detailed implementation plan as a string
 
164
 
165
  === TASK ===
166
  {task}
167
+ """
168
+ # Add user answers if provided
169
+ if user_answers:
170
+ user_prompt += f"""
171
+ === USER CLARIFICATIONS ===
172
+ {user_answers}
173
+ """
174
 
175
+ user_prompt += """
176
  Based on the exploration results above, create a detailed implementation plan.
177
  Include specific file paths, function names, and step-by-step instructions for the Coder agent.
178
  """
 
180
  # Fallback if no exploration context (shouldn't happen in v3.0)
181
  user_prompt = f"""=== TASK ===
182
  {task}
183
+ """
184
+ if user_answers:
185
+ user_prompt += f"""
186
+ === USER CLARIFICATIONS ===
187
+ {user_answers}
188
+ """
189
+ user_prompt += """
190
  Create a detailed implementation plan for this task.
191
  Note: No exploration context was provided, so make reasonable assumptions about the codebase structure.
192
  """