Add clean UI with agent progress, copyable code, and results table
Browse filesNew v3.4.0 features:
- Live agent progress table (Explorer → Planner → Coder → Reviewer)
- Status icons: ✅ done, ⏳ working, ⬜ waiting
- Copyable code blocks with syntax highlighting
- Review results table showing pass/fail for each step
- Cost summary displayed throughout
- Dockerfile +1 -1
- chainlit_app.py +320 -385
Dockerfile
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# HuggingFace Spaces Dockerfile for CodePilot
|
| 2 |
-
# BUILD_VERSION:
|
| 3 |
FROM python:3.11-slim
|
| 4 |
|
| 5 |
# Set working directory
|
|
|
|
| 1 |
# HuggingFace Spaces Dockerfile for CodePilot
|
| 2 |
+
# BUILD_VERSION: 17 (v3.4.0 clean-ui - clean progress display, copyable code, results table)
|
| 3 |
FROM python:3.11-slim
|
| 4 |
|
| 5 |
# Set working directory
|
chainlit_app.py
CHANGED
|
@@ -2,17 +2,19 @@
|
|
| 2 |
Chainlit UI for CodePilot Multi-Agent System
|
| 3 |
|
| 4 |
This provides a chat interface showing detailed agent workflow:
|
|
|
|
| 5 |
- Planner creates implementation plans
|
| 6 |
-
- Coder writes code
|
| 7 |
- Reviewer checks and approves code
|
| 8 |
|
| 9 |
-
User
|
| 10 |
"""
|
| 11 |
|
| 12 |
import chainlit as cl
|
| 13 |
import os
|
| 14 |
import sys
|
| 15 |
import io
|
|
|
|
| 16 |
from contextlib import redirect_stdout, redirect_stderr
|
| 17 |
import asyncio
|
| 18 |
from concurrent.futures import ThreadPoolExecutor
|
|
@@ -20,8 +22,8 @@ from concurrent.futures import ThreadPoolExecutor
|
|
| 20 |
# ============================================================
|
| 21 |
# STARTUP VERSION CHECK - Change this to detect if rebuild worked
|
| 22 |
# ============================================================
|
| 23 |
-
APP_VERSION = "3.
|
| 24 |
-
BUILD_ID = "2024-12-
|
| 25 |
print("=" * 60)
|
| 26 |
print(f"[STARTUP] CodePilot Chainlit App")
|
| 27 |
print(f"[STARTUP] APP_VERSION: {APP_VERSION}")
|
|
@@ -47,31 +49,186 @@ from codepilot.tools.github_tools import (
|
|
| 47 |
)
|
| 48 |
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
|
| 70 |
@cl.on_chat_start
|
| 71 |
async def start():
|
| 72 |
"""Initialize the agent system when chat starts."""
|
| 73 |
|
| 74 |
-
print("[CHAINLIT] on_chat_start triggered")
|
| 75 |
|
| 76 |
await cl.Message(
|
| 77 |
content=f"# CodePilot - Autonomous AI Coding Agent\n\n"
|
|
@@ -80,20 +237,17 @@ async def start():
|
|
| 80 |
"**How to use:**\n"
|
| 81 |
"1. Paste a **public GitHub URL** and I'll clone and analyze it\n"
|
| 82 |
"2. Tell me what you want to build or fix\n"
|
| 83 |
-
"3. Watch my agents (Planner
|
| 84 |
"**Example:**\n"
|
| 85 |
-
"```\
|
| 86 |
-
"**Ready!** Paste a GitHub URL
|
| 87 |
).send()
|
| 88 |
|
| 89 |
-
print("[CHAINLIT] Welcome message sent")
|
| 90 |
|
| 91 |
# Initialize session variables
|
| 92 |
cl.user_session.set("repo_path", None)
|
| 93 |
cl.user_session.set("repo_info", None)
|
| 94 |
-
|
| 95 |
-
# Skip self-indexing - agents will only work with cloned GitHub repos
|
| 96 |
-
# Create orchestrator and mark as ready
|
| 97 |
cl.user_session.set("orchestrator", Orchestrator(max_iterations=10))
|
| 98 |
cl.user_session.set("ready", True)
|
| 99 |
print("[CHAINLIT] Orchestrator created, ready for GitHub repos")
|
|
@@ -102,115 +256,138 @@ async def start():
|
|
| 102 |
@cl.on_chat_end
|
| 103 |
async def end():
|
| 104 |
"""Cleanup when chat ends."""
|
| 105 |
-
# Clean up any cloned repositories
|
| 106 |
repo_path = cl.user_session.get("repo_path")
|
| 107 |
if repo_path:
|
| 108 |
print(f"[CHAINLIT] Cleaning up repo: {repo_path}")
|
| 109 |
cleanup_repository(repo_path)
|
| 110 |
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
@cl.on_message
|
| 113 |
async def main(message: cl.Message):
|
| 114 |
"""Handle user messages and run the agent workflow."""
|
| 115 |
|
| 116 |
-
# Check if ready
|
| 117 |
if not cl.user_session.get("ready"):
|
| 118 |
await cl.Message(content="System is still initializing, please wait...").send()
|
| 119 |
return
|
| 120 |
|
| 121 |
-
# Get orchestrator
|
| 122 |
orchestrator: Orchestrator = cl.user_session.get("orchestrator")
|
| 123 |
|
| 124 |
-
#
|
| 125 |
-
# If NOT waiting for clarification, this is a NEW task - reset orchestrator
|
| 126 |
-
if not cl.user_session.get("waiting_for_clarification"):
|
| 127 |
-
# Create fresh orchestrator for new tasks
|
| 128 |
-
orchestrator = Orchestrator(max_iterations=10)
|
| 129 |
-
cl.user_session.set("orchestrator", orchestrator)
|
| 130 |
-
print("[CHAINLIT] Created fresh orchestrator for new task")
|
| 131 |
if cl.user_session.get("waiting_for_clarification"):
|
| 132 |
cl.user_session.set("waiting_for_clarification", False)
|
| 133 |
user_answers = message.content
|
| 134 |
|
| 135 |
-
await cl.Message(content="
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
def resume_orchestrator():
|
| 145 |
-
with redirect_stdout(captured_output), redirect_stderr(captured_output):
|
| 146 |
-
return orchestrator.resume_after_clarification(user_answers)
|
| 147 |
-
|
| 148 |
-
loop = asyncio.get_event_loop()
|
| 149 |
-
executor = ThreadPoolExecutor(max_workers=1)
|
| 150 |
-
future = loop.run_in_executor(executor, resume_orchestrator)
|
| 151 |
-
|
| 152 |
-
# Track tokens
|
| 153 |
-
total_prompt_tokens = 0
|
| 154 |
-
total_completion_tokens = 0
|
| 155 |
-
total_tokens = 0
|
| 156 |
-
seen_token_lines = set()
|
| 157 |
-
|
| 158 |
-
# Stream logs
|
| 159 |
-
accumulated_logs = ""
|
| 160 |
-
while not future.done():
|
| 161 |
-
await asyncio.sleep(0.5)
|
| 162 |
-
current_output = captured_output.getvalue()
|
| 163 |
-
if current_output != accumulated_logs:
|
| 164 |
-
accumulated_logs = current_output
|
| 165 |
-
filtered_lines = []
|
| 166 |
-
for line in accumulated_logs.split('\n'):
|
| 167 |
-
if 'Tokens:' in line and line not in seen_token_lines:
|
| 168 |
-
seen_token_lines.add(line)
|
| 169 |
-
try:
|
| 170 |
-
parts = line.split('Tokens:')[1].strip()
|
| 171 |
-
prompt = int(parts.split('prompt')[0].strip())
|
| 172 |
-
completion = int(parts.split('+')[1].split('completion')[0].strip())
|
| 173 |
-
total_prompt_tokens += prompt
|
| 174 |
-
total_completion_tokens += completion
|
| 175 |
-
total_tokens += (prompt + completion)
|
| 176 |
-
except:
|
| 177 |
-
pass
|
| 178 |
-
if any(skip in line for skip in ['Tokens:', 'Batches:', '|##', 'it/s]']):
|
| 179 |
-
continue
|
| 180 |
-
if any(keep in line for keep in [
|
| 181 |
-
'[CLASSIFIER]', '[ORCHESTRATOR]', '[PLANNER]', '[CODER]', '[REVIEWER]',
|
| 182 |
-
'[EXPLORER]', 'Calling tool:', 'Transitioning', 'APPROVED', 'REJECTED'
|
| 183 |
-
]):
|
| 184 |
-
filtered_lines.append(line)
|
| 185 |
-
filtered_output = '\n'.join(filtered_lines)
|
| 186 |
-
input_cost = (total_prompt_tokens / 1000000) * 3.0
|
| 187 |
-
output_cost = (total_completion_tokens / 1000000) * 15.0
|
| 188 |
-
total_cost = input_cost + output_cost
|
| 189 |
-
usage_summary = f"\n\nCREDITS: ${total_cost:.4f}"
|
| 190 |
-
log_msg.content = f"```\n{filtered_output}{usage_summary}\n```"
|
| 191 |
-
await log_msg.update()
|
| 192 |
-
|
| 193 |
-
result = await future
|
| 194 |
-
# Continue to show results (handled by falling through to normal result handling below)
|
| 195 |
-
# For now, show summary directly
|
| 196 |
-
summary = f"## Result\n**Status:** {result.get('status')}\n"
|
| 197 |
if result.get('code_changes'):
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
await cl.Message(content=summary).send()
|
| 201 |
-
return
|
| 202 |
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
|
|
|
| 206 |
|
| 207 |
-
# Check for GitHub URL
|
| 208 |
github_url = extract_github_url(message.content)
|
| 209 |
task_context = ""
|
| 210 |
|
| 211 |
if github_url:
|
| 212 |
-
|
| 213 |
-
clone_msg = await cl.Message(content=f"Cloning repository: `{github_url}`...").send()
|
| 214 |
|
| 215 |
success, result, repo_name = clone_repository(github_url)
|
| 216 |
|
|
@@ -218,24 +395,16 @@ async def main(message: cl.Message):
|
|
| 218 |
repo_path = result
|
| 219 |
repo_info = get_repo_info(repo_path)
|
| 220 |
|
| 221 |
-
# Store in session
|
| 222 |
cl.user_session.set("repo_path", repo_path)
|
| 223 |
cl.user_session.set("repo_info", repo_info)
|
| 224 |
|
| 225 |
-
# Index
|
| 226 |
try:
|
| 227 |
-
|
| 228 |
-
print(f"[CHAINLIT] Repository indexed: {index_result}")
|
| 229 |
except Exception as e:
|
| 230 |
-
print(f"[CHAINLIT] Indexing failed
|
| 231 |
|
| 232 |
-
# Create context for the task (limited to avoid token overflow)
|
| 233 |
languages = ", ".join(repo_info["languages"][:5]) if repo_info["languages"] else "Unknown"
|
| 234 |
-
# Only include first 20 files to keep context small
|
| 235 |
-
sample_files = repo_info["files"][:20] if repo_info["files"] else []
|
| 236 |
-
files_preview = "\n".join(f" - {f}" for f in sample_files)
|
| 237 |
-
if len(repo_info["files"]) > 20:
|
| 238 |
-
files_preview += f"\n ... and {len(repo_info['files']) - 20} more files"
|
| 239 |
|
| 240 |
task_context = f"""
|
| 241 |
[REPOSITORY CONTEXT]
|
|
@@ -243,30 +412,19 @@ Repository: {repo_name}
|
|
| 243 |
Path: {repo_path}
|
| 244 |
Total Files: {repo_info['total_files']}
|
| 245 |
Languages: {languages}
|
| 246 |
-
Sample Files:
|
| 247 |
-
{files_preview}
|
| 248 |
|
| 249 |
AVAILABLE TOOLS:
|
| 250 |
-
- search_repository: Search
|
| 251 |
-
- read_file: Read
|
| 252 |
-
- search_code: Grep for
|
| 253 |
"""
|
| 254 |
-
|
| 255 |
-
clone_msg.content = f"**Repository cloned successfully!**\n\n" \
|
| 256 |
-
f"- **Name:** {repo_name}\n" \
|
| 257 |
-
f"- **Files:** {repo_info['total_files']}\n" \
|
| 258 |
-
f"- **Languages:** {languages}\n" \
|
| 259 |
-
f"- **Path:** `{repo_path}`"
|
| 260 |
await clone_msg.update()
|
| 261 |
-
|
| 262 |
else:
|
| 263 |
-
|
| 264 |
-
clone_msg.content = f"**Failed to clone repository**\n\n{result}\n\n" \
|
| 265 |
-
f"Make sure the repository is public and the URL is correct."
|
| 266 |
await clone_msg.update()
|
| 267 |
return
|
| 268 |
|
| 269 |
-
# Check if we have a repo from previous message
|
| 270 |
elif cl.user_session.get("repo_path"):
|
| 271 |
repo_path = cl.user_session.get("repo_path")
|
| 272 |
repo_info = cl.user_session.get("repo_info")
|
|
@@ -280,266 +438,43 @@ Total Files: {repo_info['total_files']}
|
|
| 280 |
Languages: {languages}
|
| 281 |
|
| 282 |
AVAILABLE TOOLS:
|
| 283 |
-
- search_repository: Search
|
| 284 |
-
- read_file: Read
|
| 285 |
-
- search_code: Grep for
|
| 286 |
"""
|
| 287 |
|
| 288 |
-
#
|
| 289 |
-
# Remove the GitHub URL from the message to get just the user's query
|
| 290 |
user_query = message.content
|
| 291 |
-
print(f"[DEBUG] Original message.content: '{message.content}'")
|
| 292 |
-
print(f"[DEBUG] GitHub URL found: '{github_url}'")
|
| 293 |
-
|
| 294 |
if github_url:
|
| 295 |
-
# Remove the URL from the message to get the actual task
|
| 296 |
-
import re
|
| 297 |
user_query = re.sub(r'https?://github\.com/[^\s]+', '', user_query).strip()
|
| 298 |
-
print(f"[DEBUG] After URL removal: '{user_query}'")
|
| 299 |
|
| 300 |
full_task = task_context + "\n\n" + user_query if task_context else user_query
|
| 301 |
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
print(f"[DEBUG] Final user_query: '{user_query}'")
|
| 305 |
-
print(f"[DEBUG] Full task (first 500 chars): '{full_task[:500]}...'")
|
| 306 |
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
await log_msg.send()
|
| 310 |
-
|
| 311 |
-
try:
|
| 312 |
-
# Capture stdout/stderr to stream logs
|
| 313 |
-
captured_output = io.StringIO()
|
| 314 |
-
|
| 315 |
-
def run_orchestrator():
|
| 316 |
-
"""Run orchestrator in thread and capture output."""
|
| 317 |
-
try:
|
| 318 |
-
with redirect_stdout(captured_output), redirect_stderr(captured_output):
|
| 319 |
-
return orchestrator.run(full_task)
|
| 320 |
-
except Exception as e:
|
| 321 |
-
# Capture any exceptions from orchestrator
|
| 322 |
-
print(f"Error in orchestrator: {str(e)}")
|
| 323 |
-
import traceback
|
| 324 |
-
traceback.print_exc()
|
| 325 |
-
raise
|
| 326 |
-
|
| 327 |
-
# Run in thread pool to avoid blocking
|
| 328 |
-
loop = asyncio.get_event_loop()
|
| 329 |
-
executor = ThreadPoolExecutor(max_workers=1)
|
| 330 |
-
|
| 331 |
-
# Start the orchestrator in background
|
| 332 |
-
future = loop.run_in_executor(executor, run_orchestrator)
|
| 333 |
-
|
| 334 |
-
# Track API usage
|
| 335 |
-
total_prompt_tokens = 0
|
| 336 |
-
total_completion_tokens = 0
|
| 337 |
-
total_tokens = 0
|
| 338 |
-
seen_token_lines = set() # Track which token lines we've already counted
|
| 339 |
-
|
| 340 |
-
# Stream logs while orchestrator is running - FILTERED
|
| 341 |
-
accumulated_logs = ""
|
| 342 |
-
while not future.done():
|
| 343 |
-
await asyncio.sleep(0.5) # Check every 500ms
|
| 344 |
-
|
| 345 |
-
# Get new output
|
| 346 |
-
current_output = captured_output.getvalue()
|
| 347 |
-
if current_output != accumulated_logs:
|
| 348 |
-
accumulated_logs = current_output
|
| 349 |
-
|
| 350 |
-
# Filter logs to show only important lines
|
| 351 |
-
filtered_lines = []
|
| 352 |
-
for line in accumulated_logs.split('\n'):
|
| 353 |
-
# Extract token usage before filtering (only count each line once!)
|
| 354 |
-
if 'Tokens:' in line and line not in seen_token_lines:
|
| 355 |
-
seen_token_lines.add(line) # Mark as counted
|
| 356 |
-
try:
|
| 357 |
-
# Parse: "Tokens: 505 prompt + 20 completion = 525 total"
|
| 358 |
-
parts = line.split('Tokens:')[1].strip()
|
| 359 |
-
prompt = int(parts.split('prompt')[0].strip())
|
| 360 |
-
completion = int(parts.split('+')[1].split('completion')[0].strip())
|
| 361 |
-
total_prompt_tokens += prompt
|
| 362 |
-
total_completion_tokens += completion
|
| 363 |
-
total_tokens += (prompt + completion)
|
| 364 |
-
except:
|
| 365 |
-
pass
|
| 366 |
-
|
| 367 |
-
# Skip token counts, progress bars, and verbose details
|
| 368 |
-
if any(skip in line for skip in ['Tokens:', 'Batches:', '|##', 'it/s]']):
|
| 369 |
-
continue
|
| 370 |
-
# Keep important lines
|
| 371 |
-
if any(keep in line for keep in [
|
| 372 |
-
'[CLASSIFIER]', '[ORCHESTRATOR]', '[PLANNER]', '[CODER]', '[REVIEWER]',
|
| 373 |
-
'[EXPLORER]', 'Calling tool:', 'Tool', 'Transitioning', 'APPROVED', 'REJECTED',
|
| 374 |
-
'[GITHUB]', 'Cloning', 'Repository'
|
| 375 |
-
]):
|
| 376 |
-
filtered_lines.append(line)
|
| 377 |
-
|
| 378 |
-
filtered_output = '\n'.join(filtered_lines)
|
| 379 |
-
|
| 380 |
-
# Calculate cost (Claude Sonnet 4.5 pricing: $3/1M input, $15/1M output)
|
| 381 |
-
input_cost = (total_prompt_tokens / 1000000) * 3.0
|
| 382 |
-
output_cost = (total_completion_tokens / 1000000) * 15.0
|
| 383 |
-
total_cost = input_cost + output_cost
|
| 384 |
-
|
| 385 |
-
# Add usage summary to logs
|
| 386 |
-
usage_summary = f"\n\nCREDITS USED:\n"
|
| 387 |
-
usage_summary += f" Input: {total_prompt_tokens:,} tokens (${input_cost:.4f})\n"
|
| 388 |
-
usage_summary += f" Output: {total_completion_tokens:,} tokens (${output_cost:.4f})\n"
|
| 389 |
-
usage_summary += f" Total: {total_tokens:,} tokens (${total_cost:.4f})"
|
| 390 |
-
|
| 391 |
-
# Update message with filtered logs + usage
|
| 392 |
-
log_msg.content = f"```\n{filtered_output}\n{usage_summary}\n```"
|
| 393 |
-
await log_msg.update()
|
| 394 |
-
|
| 395 |
-
# Get final result
|
| 396 |
-
result = await future
|
| 397 |
-
|
| 398 |
-
# Get final logs
|
| 399 |
-
final_logs = captured_output.getvalue()
|
| 400 |
-
|
| 401 |
-
# Update with final logs
|
| 402 |
-
log_msg.content = f"## Execution Log\n```\n{final_logs}\n```"
|
| 403 |
-
await log_msg.update()
|
| 404 |
-
|
| 405 |
-
# Check if we need clarification from user
|
| 406 |
-
if result.get('status') == 'clarifying' and result.get('clarifying_questions'):
|
| 407 |
-
questions = result['clarifying_questions']
|
| 408 |
-
# Store that we're waiting for clarification
|
| 409 |
-
cl.user_session.set("waiting_for_clarification", True)
|
| 410 |
-
|
| 411 |
-
await cl.Message(
|
| 412 |
-
content=f"## Before I proceed, I have some questions:\n\n{questions}\n\n"
|
| 413 |
-
f"**Please answer the questions above so I can create a better plan.**"
|
| 414 |
-
).send()
|
| 415 |
-
return # Wait for user to respond
|
| 416 |
-
|
| 417 |
-
# Send results summary
|
| 418 |
-
summary_lines = []
|
| 419 |
-
|
| 420 |
-
if result.get('plan'):
|
| 421 |
-
summary_lines.append("## Planner")
|
| 422 |
-
summary_lines.append(f"Plan created ({len(result['plan'])} chars)\n")
|
| 423 |
-
|
| 424 |
-
if result.get('code_changes'):
|
| 425 |
-
summary_lines.append("## Coder")
|
| 426 |
-
summary_lines.append(f"Created {len(result['code_changes'])} file(s):")
|
| 427 |
-
for file_path in result['code_changes'].keys():
|
| 428 |
-
summary_lines.append(f" - {file_path}")
|
| 429 |
-
summary_lines.append("")
|
| 430 |
-
|
| 431 |
-
if result.get('review_feedback'):
|
| 432 |
-
summary_lines.append("## Reviewer")
|
| 433 |
-
if result.get('success'):
|
| 434 |
-
summary_lines.append("Code approved")
|
| 435 |
-
else:
|
| 436 |
-
summary_lines.append("Needs revision")
|
| 437 |
-
summary_lines.append("")
|
| 438 |
-
|
| 439 |
-
summary_lines.append("## Result")
|
| 440 |
-
if result.get('success'):
|
| 441 |
-
summary_lines.append(f"**Success** (Iterations: {result.get('iterations', 'N/A')})")
|
| 442 |
-
else:
|
| 443 |
-
summary_lines.append(f"**Incomplete** (Iterations: {result.get('iterations', 'N/A')})")
|
| 444 |
-
|
| 445 |
-
# Add final cost summary (Claude Sonnet 4.5 pricing: $3/1M input, $15/1M output)
|
| 446 |
-
summary_lines.append("\n## API Credits Used (Claude Sonnet 4.5)")
|
| 447 |
-
summary_lines.append(f"**Total Tokens:** {total_tokens:,}")
|
| 448 |
-
summary_lines.append(f"- Input: {total_prompt_tokens:,} tokens (${(total_prompt_tokens/1000000)*3.0:.4f})")
|
| 449 |
-
summary_lines.append(f"- Output: {total_completion_tokens:,} tokens (${(total_completion_tokens/1000000)*15.0:.4f})")
|
| 450 |
-
summary_lines.append(f"\n**Estimated Cost:** ${total_cost:.4f}")
|
| 451 |
-
|
| 452 |
-
await cl.Message(content="\n".join(summary_lines)).send()
|
| 453 |
-
|
| 454 |
-
except Exception as e:
|
| 455 |
-
# Determine error type and provide specific guidance
|
| 456 |
-
error_message = str(e)
|
| 457 |
-
error_type = type(e).__name__
|
| 458 |
-
|
| 459 |
-
if "rate_limit" in error_message.lower() or "429" in error_message:
|
| 460 |
-
user_message = f"""## Rate Limit Reached
|
| 461 |
-
|
| 462 |
-
Claude API rate limit exceeded. This happens when too many requests are made in a short time.
|
| 463 |
-
|
| 464 |
-
**What to do:**
|
| 465 |
-
- Wait a few minutes and try again
|
| 466 |
-
- Reduce max_iterations (currently: {orchestrator.max_iterations})
|
| 467 |
-
- Your request will work once the rate limit resets
|
| 468 |
-
|
| 469 |
-
**Error details:**
|
| 470 |
-
```
|
| 471 |
-
{error_message}
|
| 472 |
-
```
|
| 473 |
-
"""
|
| 474 |
-
elif "insufficient_quota" in error_message.lower() or "credit" in error_message.lower():
|
| 475 |
-
user_message = f"""## API Credits Exhausted
|
| 476 |
-
|
| 477 |
-
Your Anthropic API credits have been exhausted.
|
| 478 |
-
|
| 479 |
-
**What to do:**
|
| 480 |
-
- Add credits to your Anthropic account at https://console.anthropic.com/settings/billing
|
| 481 |
-
- Check your usage at https://console.anthropic.com/settings/usage
|
| 482 |
-
- Current model: Claude Sonnet 4.5 (~$0.20 per task)
|
| 483 |
-
|
| 484 |
-
**Error details:**
|
| 485 |
-
```
|
| 486 |
-
{error_message}
|
| 487 |
-
```
|
| 488 |
-
"""
|
| 489 |
-
elif "api_key" in error_message.lower() or "authentication" in error_message.lower():
|
| 490 |
-
user_message = f"""## API Key Error
|
| 491 |
-
|
| 492 |
-
There's an issue with your Anthropic API key.
|
| 493 |
-
|
| 494 |
-
**What to do:**
|
| 495 |
-
- Verify your ANTHROPIC_API_KEY in .env file
|
| 496 |
-
- Check that the key is valid at https://console.anthropic.com/settings/keys
|
| 497 |
-
- Restart the application after updating .env
|
| 498 |
-
|
| 499 |
-
**Error details:**
|
| 500 |
-
```
|
| 501 |
-
{error_message}
|
| 502 |
-
```
|
| 503 |
-
"""
|
| 504 |
-
elif "timeout" in error_message.lower():
|
| 505 |
-
user_message = f"""## Request Timeout
|
| 506 |
-
|
| 507 |
-
The operation took too long and timed out.
|
| 508 |
-
|
| 509 |
-
**What to do:**
|
| 510 |
-
- Try again with a simpler task
|
| 511 |
-
- The task may be too complex for one iteration
|
| 512 |
-
- Consider breaking it into smaller steps
|
| 513 |
-
|
| 514 |
-
**Error details:**
|
| 515 |
-
```
|
| 516 |
-
{error_message}
|
| 517 |
-
```
|
| 518 |
-
"""
|
| 519 |
-
else:
|
| 520 |
-
# Generic error with helpful context
|
| 521 |
-
user_message = f"""## Error Occurred
|
| 522 |
-
|
| 523 |
-
An unexpected error occurred during execution.
|
| 524 |
-
|
| 525 |
-
**Error type:** {error_type}
|
| 526 |
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
|
|
|
|
|
|
|
|
|
| 531 |
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
{error_message}
|
| 535 |
-
```
|
| 536 |
|
| 537 |
-
|
| 538 |
-
|
|
|
|
| 539 |
|
| 540 |
-
|
|
|
|
|
|
|
| 541 |
|
| 542 |
|
| 543 |
if __name__ == "__main__":
|
| 544 |
-
import sys
|
| 545 |
sys.exit("Run with: chainlit run chainlit_app.py")
|
|
|
|
| 2 |
Chainlit UI for CodePilot Multi-Agent System
|
| 3 |
|
| 4 |
This provides a chat interface showing detailed agent workflow:
|
| 5 |
+
- Explorer searches the codebase
|
| 6 |
- Planner creates implementation plans
|
| 7 |
+
- Coder writes code
|
| 8 |
- Reviewer checks and approves code
|
| 9 |
|
| 10 |
+
User sees clean progress updates and copyable code output.
|
| 11 |
"""
|
| 12 |
|
| 13 |
import chainlit as cl
|
| 14 |
import os
|
| 15 |
import sys
|
| 16 |
import io
|
| 17 |
+
import re
|
| 18 |
from contextlib import redirect_stdout, redirect_stderr
|
| 19 |
import asyncio
|
| 20 |
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
| 22 |
# ============================================================
|
| 23 |
# STARTUP VERSION CHECK - Change this to detect if rebuild worked
|
| 24 |
# ============================================================
|
| 25 |
+
APP_VERSION = "3.4.0-clean-ui"
|
| 26 |
+
BUILD_ID = "2024-12-20-v1"
|
| 27 |
print("=" * 60)
|
| 28 |
print(f"[STARTUP] CodePilot Chainlit App")
|
| 29 |
print(f"[STARTUP] APP_VERSION: {APP_VERSION}")
|
|
|
|
| 49 |
)
|
| 50 |
|
| 51 |
|
| 52 |
+
def get_file_extension(file_path: str) -> str:
|
| 53 |
+
"""Get language identifier for syntax highlighting."""
|
| 54 |
+
ext_map = {
|
| 55 |
+
'.py': 'python',
|
| 56 |
+
'.js': 'javascript',
|
| 57 |
+
'.ts': 'typescript',
|
| 58 |
+
'.jsx': 'jsx',
|
| 59 |
+
'.tsx': 'tsx',
|
| 60 |
+
'.html': 'html',
|
| 61 |
+
'.css': 'css',
|
| 62 |
+
'.json': 'json',
|
| 63 |
+
'.md': 'markdown',
|
| 64 |
+
'.yml': 'yaml',
|
| 65 |
+
'.yaml': 'yaml',
|
| 66 |
+
'.sql': 'sql',
|
| 67 |
+
'.sh': 'bash',
|
| 68 |
+
'.rs': 'rust',
|
| 69 |
+
'.go': 'go',
|
| 70 |
+
'.java': 'java',
|
| 71 |
+
'.rb': 'ruby',
|
| 72 |
+
'.php': 'php',
|
| 73 |
+
}
|
| 74 |
+
ext = os.path.splitext(file_path)[1].lower()
|
| 75 |
+
return ext_map.get(ext, '')
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def format_code_output(code_changes: dict) -> str:
|
| 79 |
+
"""Format code changes as copyable markdown code blocks."""
|
| 80 |
+
if not code_changes:
|
| 81 |
+
return "No code changes."
|
| 82 |
+
|
| 83 |
+
output = []
|
| 84 |
+
for file_path, content in code_changes.items():
|
| 85 |
+
# Get just the filename for display
|
| 86 |
+
filename = os.path.basename(file_path)
|
| 87 |
+
lang = get_file_extension(file_path)
|
| 88 |
+
|
| 89 |
+
output.append(f"### `{filename}`")
|
| 90 |
+
output.append(f"**Full path:** `{file_path}`")
|
| 91 |
+
output.append(f"```{lang}")
|
| 92 |
+
output.append(content)
|
| 93 |
+
output.append("```")
|
| 94 |
+
output.append("")
|
| 95 |
+
|
| 96 |
+
return "\n".join(output)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def parse_agent_status(logs: str) -> dict:
|
| 100 |
+
"""Parse logs to extract agent status information."""
|
| 101 |
+
status = {
|
| 102 |
+
'current_agent': None,
|
| 103 |
+
'explorer_done': False,
|
| 104 |
+
'planner_done': False,
|
| 105 |
+
'coder_done': False,
|
| 106 |
+
'reviewer_done': False,
|
| 107 |
+
'approved': None,
|
| 108 |
+
'tools_called': [],
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
for line in logs.split('\n'):
|
| 112 |
+
if '[EXPLORER]' in line:
|
| 113 |
+
status['current_agent'] = 'Explorer'
|
| 114 |
+
if 'Calling tool:' in line:
|
| 115 |
+
tool = line.split('Calling tool:')[1].strip()
|
| 116 |
+
status['tools_called'].append(f"Explorer: {tool}")
|
| 117 |
+
elif '[PLANNER]' in line:
|
| 118 |
+
status['current_agent'] = 'Planner'
|
| 119 |
+
if 'Plan created' in line:
|
| 120 |
+
status['planner_done'] = True
|
| 121 |
+
elif '[CODER]' in line:
|
| 122 |
+
status['current_agent'] = 'Coder'
|
| 123 |
+
if 'Calling tool:' in line:
|
| 124 |
+
tool = line.split('Calling tool:')[1].strip()
|
| 125 |
+
status['tools_called'].append(f"Coder: {tool}")
|
| 126 |
+
if 'Finished implementation' in line:
|
| 127 |
+
status['coder_done'] = True
|
| 128 |
+
elif '[REVIEWER]' in line:
|
| 129 |
+
status['current_agent'] = 'Reviewer'
|
| 130 |
+
if 'Calling tool:' in line:
|
| 131 |
+
tool = line.split('Calling tool:')[1].strip()
|
| 132 |
+
status['tools_called'].append(f"Reviewer: {tool}")
|
| 133 |
+
elif 'APPROVED' in line:
|
| 134 |
+
status['approved'] = True
|
| 135 |
+
status['reviewer_done'] = True
|
| 136 |
+
elif 'REJECTED' in line:
|
| 137 |
+
status['approved'] = False
|
| 138 |
+
status['reviewer_done'] = True
|
| 139 |
+
elif 'Transitioning to CLARIFYING' in line:
|
| 140 |
+
status['explorer_done'] = True
|
| 141 |
+
elif 'Transitioning to PLANNING' in line:
|
| 142 |
+
status['explorer_done'] = True
|
| 143 |
+
elif 'Transitioning to CODING' in line:
|
| 144 |
+
status['planner_done'] = True
|
| 145 |
+
elif 'Transitioning to REVIEWING' in line:
|
| 146 |
+
status['coder_done'] = True
|
| 147 |
+
elif 'Transitioning to COMPLETE' in line:
|
| 148 |
+
status['reviewer_done'] = True
|
| 149 |
+
|
| 150 |
+
return status
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def format_progress_display(status: dict, total_cost: float) -> str:
|
| 154 |
+
"""Format a clean progress display."""
|
| 155 |
+
|
| 156 |
+
def icon(done: bool, active: bool = False) -> str:
|
| 157 |
+
if done:
|
| 158 |
+
return "✅"
|
| 159 |
+
elif active:
|
| 160 |
+
return "⏳"
|
| 161 |
+
else:
|
| 162 |
+
return "⬜"
|
| 163 |
+
|
| 164 |
+
current = status['current_agent']
|
| 165 |
+
|
| 166 |
+
lines = ["## Agent Progress\n"]
|
| 167 |
+
lines.append("| Agent | Status |")
|
| 168 |
+
lines.append("|-------|--------|")
|
| 169 |
+
lines.append(f"| Explorer | {icon(status['explorer_done'], current == 'Explorer')} {'Searching codebase...' if current == 'Explorer' and not status['explorer_done'] else 'Done' if status['explorer_done'] else 'Waiting'} |")
|
| 170 |
+
lines.append(f"| Planner | {icon(status['planner_done'], current == 'Planner')} {'Creating plan...' if current == 'Planner' and not status['planner_done'] else 'Done' if status['planner_done'] else 'Waiting'} |")
|
| 171 |
+
lines.append(f"| Coder | {icon(status['coder_done'], current == 'Coder')} {'Writing code...' if current == 'Coder' and not status['coder_done'] else 'Done' if status['coder_done'] else 'Waiting'} |")
|
| 172 |
+
|
| 173 |
+
reviewer_status = 'Waiting'
|
| 174 |
+
if current == 'Reviewer' and not status['reviewer_done']:
|
| 175 |
+
reviewer_status = 'Reviewing...'
|
| 176 |
+
elif status['reviewer_done']:
|
| 177 |
+
reviewer_status = '**APPROVED**' if status['approved'] else '**REJECTED**'
|
| 178 |
+
lines.append(f"| Reviewer | {icon(status['reviewer_done'], current == 'Reviewer')} {reviewer_status} |")
|
| 179 |
+
|
| 180 |
+
lines.append(f"\n**Cost:** ${total_cost:.4f}")
|
| 181 |
+
|
| 182 |
+
return "\n".join(lines)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def format_final_result(result: dict, total_cost: float) -> str:
|
| 186 |
+
"""Format the final result with test table and summary."""
|
| 187 |
+
lines = []
|
| 188 |
+
|
| 189 |
+
# Status header
|
| 190 |
+
success = result.get('success', False)
|
| 191 |
+
status_icon = "✅" if success else "❌"
|
| 192 |
+
lines.append(f"## Result: {status_icon} {'Success' if success else 'Failed'}\n")
|
| 193 |
+
|
| 194 |
+
# Review result table
|
| 195 |
+
lines.append("### Review Results\n")
|
| 196 |
+
lines.append("| Check | Result |")
|
| 197 |
+
lines.append("|-------|--------|")
|
| 198 |
+
|
| 199 |
+
if result.get('plan'):
|
| 200 |
+
lines.append("| Plan Created | ✅ Pass |")
|
| 201 |
+
else:
|
| 202 |
+
lines.append("| Plan Created | ❌ Fail |")
|
| 203 |
+
|
| 204 |
+
if result.get('code_changes'):
|
| 205 |
+
lines.append(f"| Code Written | ✅ Pass ({len(result['code_changes'])} files) |")
|
| 206 |
+
else:
|
| 207 |
+
lines.append("| Code Written | ❌ Fail |")
|
| 208 |
+
|
| 209 |
+
if result.get('review_feedback'):
|
| 210 |
+
if success:
|
| 211 |
+
lines.append("| Code Review | ✅ Approved |")
|
| 212 |
+
else:
|
| 213 |
+
lines.append("| Code Review | ❌ Rejected |")
|
| 214 |
+
else:
|
| 215 |
+
lines.append("| Code Review | ⬜ Not Run |")
|
| 216 |
+
|
| 217 |
+
lines.append("")
|
| 218 |
+
|
| 219 |
+
# Cost summary
|
| 220 |
+
lines.append("### Cost Summary\n")
|
| 221 |
+
lines.append(f"**Total Cost:** ${total_cost:.4f}")
|
| 222 |
+
lines.append(f"**Iterations:** {result.get('iterations', 'N/A')}")
|
| 223 |
+
|
| 224 |
+
return "\n".join(lines)
|
| 225 |
|
| 226 |
|
| 227 |
@cl.on_chat_start
|
| 228 |
async def start():
|
| 229 |
"""Initialize the agent system when chat starts."""
|
| 230 |
|
| 231 |
+
print("[CHAINLIT] on_chat_start triggered")
|
| 232 |
|
| 233 |
await cl.Message(
|
| 234 |
content=f"# CodePilot - Autonomous AI Coding Agent\n\n"
|
|
|
|
| 237 |
"**How to use:**\n"
|
| 238 |
"1. Paste a **public GitHub URL** and I'll clone and analyze it\n"
|
| 239 |
"2. Tell me what you want to build or fix\n"
|
| 240 |
+
"3. Watch my agents (Explorer → Planner → Coder → Reviewer) work!\n\n"
|
| 241 |
"**Example:**\n"
|
| 242 |
+
"```\nhttps://github.com/pallets/flask add a health check endpoint example\n```\n\n"
|
| 243 |
+
"**Ready!** Paste a GitHub URL with your task."
|
| 244 |
).send()
|
| 245 |
|
| 246 |
+
print("[CHAINLIT] Welcome message sent")
|
| 247 |
|
| 248 |
# Initialize session variables
|
| 249 |
cl.user_session.set("repo_path", None)
|
| 250 |
cl.user_session.set("repo_info", None)
|
|
|
|
|
|
|
|
|
|
| 251 |
cl.user_session.set("orchestrator", Orchestrator(max_iterations=10))
|
| 252 |
cl.user_session.set("ready", True)
|
| 253 |
print("[CHAINLIT] Orchestrator created, ready for GitHub repos")
|
|
|
|
| 256 |
@cl.on_chat_end
|
| 257 |
async def end():
|
| 258 |
"""Cleanup when chat ends."""
|
|
|
|
| 259 |
repo_path = cl.user_session.get("repo_path")
|
| 260 |
if repo_path:
|
| 261 |
print(f"[CHAINLIT] Cleaning up repo: {repo_path}")
|
| 262 |
cleanup_repository(repo_path)
|
| 263 |
|
| 264 |
|
| 265 |
+
async def run_workflow(orchestrator, task_or_answers, is_resume=False):
|
| 266 |
+
"""Run the orchestrator workflow and display clean progress."""
|
| 267 |
+
|
| 268 |
+
# Create progress message
|
| 269 |
+
progress_msg = cl.Message(content="## Agent Progress\n\nStarting...")
|
| 270 |
+
await progress_msg.send()
|
| 271 |
+
|
| 272 |
+
try:
|
| 273 |
+
captured_output = io.StringIO()
|
| 274 |
+
|
| 275 |
+
def run_task():
|
| 276 |
+
with redirect_stdout(captured_output), redirect_stderr(captured_output):
|
| 277 |
+
if is_resume:
|
| 278 |
+
return orchestrator.resume_after_clarification(task_or_answers)
|
| 279 |
+
else:
|
| 280 |
+
return orchestrator.run(task_or_answers)
|
| 281 |
+
|
| 282 |
+
loop = asyncio.get_event_loop()
|
| 283 |
+
executor = ThreadPoolExecutor(max_workers=1)
|
| 284 |
+
future = loop.run_in_executor(executor, run_task)
|
| 285 |
+
|
| 286 |
+
# Track tokens for cost calculation
|
| 287 |
+
total_prompt_tokens = 0
|
| 288 |
+
total_completion_tokens = 0
|
| 289 |
+
seen_token_lines = set()
|
| 290 |
+
|
| 291 |
+
# Stream progress updates
|
| 292 |
+
accumulated_logs = ""
|
| 293 |
+
while not future.done():
|
| 294 |
+
await asyncio.sleep(0.3)
|
| 295 |
+
|
| 296 |
+
current_output = captured_output.getvalue()
|
| 297 |
+
if current_output != accumulated_logs:
|
| 298 |
+
accumulated_logs = current_output
|
| 299 |
+
|
| 300 |
+
# Extract token usage
|
| 301 |
+
for line in accumulated_logs.split('\n'):
|
| 302 |
+
if 'Tokens:' in line and line not in seen_token_lines:
|
| 303 |
+
seen_token_lines.add(line)
|
| 304 |
+
try:
|
| 305 |
+
parts = line.split('Tokens:')[1].strip()
|
| 306 |
+
prompt = int(parts.split('prompt')[0].strip())
|
| 307 |
+
completion = int(parts.split('+')[1].split('completion')[0].strip())
|
| 308 |
+
total_prompt_tokens += prompt
|
| 309 |
+
total_completion_tokens += completion
|
| 310 |
+
except:
|
| 311 |
+
pass
|
| 312 |
+
|
| 313 |
+
# Calculate cost
|
| 314 |
+
total_cost = (total_prompt_tokens / 1000000) * 3.0 + (total_completion_tokens / 1000000) * 15.0
|
| 315 |
+
|
| 316 |
+
# Parse and display progress
|
| 317 |
+
status = parse_agent_status(accumulated_logs)
|
| 318 |
+
progress_msg.content = format_progress_display(status, total_cost)
|
| 319 |
+
await progress_msg.update()
|
| 320 |
+
|
| 321 |
+
# Get final result
|
| 322 |
+
result = await future
|
| 323 |
+
|
| 324 |
+
# Final token count
|
| 325 |
+
final_logs = captured_output.getvalue()
|
| 326 |
+
for line in final_logs.split('\n'):
|
| 327 |
+
if 'Tokens:' in line and line not in seen_token_lines:
|
| 328 |
+
seen_token_lines.add(line)
|
| 329 |
+
try:
|
| 330 |
+
parts = line.split('Tokens:')[1].strip()
|
| 331 |
+
prompt = int(parts.split('prompt')[0].strip())
|
| 332 |
+
completion = int(parts.split('+')[1].split('completion')[0].strip())
|
| 333 |
+
total_prompt_tokens += prompt
|
| 334 |
+
total_completion_tokens += completion
|
| 335 |
+
except:
|
| 336 |
+
pass
|
| 337 |
+
|
| 338 |
+
total_cost = (total_prompt_tokens / 1000000) * 3.0 + (total_completion_tokens / 1000000) * 15.0
|
| 339 |
+
|
| 340 |
+
# Update progress with final status
|
| 341 |
+
status = parse_agent_status(final_logs)
|
| 342 |
+
progress_msg.content = format_progress_display(status, total_cost)
|
| 343 |
+
await progress_msg.update()
|
| 344 |
+
|
| 345 |
+
return result, total_cost
|
| 346 |
+
|
| 347 |
+
except Exception as e:
|
| 348 |
+
await cl.Message(content=f"## Error\n\n```\n{str(e)}\n```").send()
|
| 349 |
+
return None, 0
|
| 350 |
+
|
| 351 |
+
|
| 352 |
@cl.on_message
|
| 353 |
async def main(message: cl.Message):
|
| 354 |
"""Handle user messages and run the agent workflow."""
|
| 355 |
|
|
|
|
| 356 |
if not cl.user_session.get("ready"):
|
| 357 |
await cl.Message(content="System is still initializing, please wait...").send()
|
| 358 |
return
|
| 359 |
|
|
|
|
| 360 |
orchestrator: Orchestrator = cl.user_session.get("orchestrator")
|
| 361 |
|
| 362 |
+
# Handle clarification answers
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
if cl.user_session.get("waiting_for_clarification"):
|
| 364 |
cl.user_session.set("waiting_for_clarification", False)
|
| 365 |
user_answers = message.content
|
| 366 |
|
| 367 |
+
await cl.Message(content="Thanks! Creating plan with your input...").send()
|
| 368 |
+
|
| 369 |
+
result, total_cost = await run_workflow(orchestrator, user_answers, is_resume=True)
|
| 370 |
+
|
| 371 |
+
if result:
|
| 372 |
+
# Show final result
|
| 373 |
+
await cl.Message(content=format_final_result(result, total_cost)).send()
|
| 374 |
+
|
| 375 |
+
# Show code if any
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
if result.get('code_changes'):
|
| 377 |
+
await cl.Message(content="## Generated Code\n\n" + format_code_output(result['code_changes'])).send()
|
| 378 |
+
return
|
|
|
|
|
|
|
| 379 |
|
| 380 |
+
# New task - reset orchestrator
|
| 381 |
+
orchestrator = Orchestrator(max_iterations=10)
|
| 382 |
+
cl.user_session.set("orchestrator", orchestrator)
|
| 383 |
+
print("[CHAINLIT] Created fresh orchestrator for new task")
|
| 384 |
|
| 385 |
+
# Check for GitHub URL
|
| 386 |
github_url = extract_github_url(message.content)
|
| 387 |
task_context = ""
|
| 388 |
|
| 389 |
if github_url:
|
| 390 |
+
clone_msg = await cl.Message(content=f"📦 Cloning `{github_url}`...").send()
|
|
|
|
| 391 |
|
| 392 |
success, result, repo_name = clone_repository(github_url)
|
| 393 |
|
|
|
|
| 395 |
repo_path = result
|
| 396 |
repo_info = get_repo_info(repo_path)
|
| 397 |
|
|
|
|
| 398 |
cl.user_session.set("repo_path", repo_path)
|
| 399 |
cl.user_session.set("repo_info", repo_info)
|
| 400 |
|
| 401 |
+
# Index repository
|
| 402 |
try:
|
| 403 |
+
index_codebase(repo_path)
|
|
|
|
| 404 |
except Exception as e:
|
| 405 |
+
print(f"[CHAINLIT] Indexing failed: {e}")
|
| 406 |
|
|
|
|
| 407 |
languages = ", ".join(repo_info["languages"][:5]) if repo_info["languages"] else "Unknown"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
|
| 409 |
task_context = f"""
|
| 410 |
[REPOSITORY CONTEXT]
|
|
|
|
| 412 |
Path: {repo_path}
|
| 413 |
Total Files: {repo_info['total_files']}
|
| 414 |
Languages: {languages}
|
|
|
|
|
|
|
| 415 |
|
| 416 |
AVAILABLE TOOLS:
|
| 417 |
+
- search_repository: Search using BM25
|
| 418 |
+
- read_file: Read file (use full path: {repo_path}/...)
|
| 419 |
+
- search_code: Grep for patterns
|
| 420 |
"""
|
| 421 |
+
clone_msg.content = f"✅ **Cloned:** {repo_name} ({repo_info['total_files']} files, {languages})"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
await clone_msg.update()
|
|
|
|
| 423 |
else:
|
| 424 |
+
clone_msg.content = f"❌ **Failed to clone:** {result}"
|
|
|
|
|
|
|
| 425 |
await clone_msg.update()
|
| 426 |
return
|
| 427 |
|
|
|
|
| 428 |
elif cl.user_session.get("repo_path"):
|
| 429 |
repo_path = cl.user_session.get("repo_path")
|
| 430 |
repo_info = cl.user_session.get("repo_info")
|
|
|
|
| 438 |
Languages: {languages}
|
| 439 |
|
| 440 |
AVAILABLE TOOLS:
|
| 441 |
+
- search_repository: Search using BM25
|
| 442 |
+
- read_file: Read file (use full path: {repo_path}/...)
|
| 443 |
+
- search_code: Grep for patterns
|
| 444 |
"""
|
| 445 |
|
| 446 |
+
# Extract user query (remove URL if present)
|
|
|
|
| 447 |
user_query = message.content
|
|
|
|
|
|
|
|
|
|
| 448 |
if github_url:
|
|
|
|
|
|
|
| 449 |
user_query = re.sub(r'https?://github\.com/[^\s]+', '', user_query).strip()
|
|
|
|
| 450 |
|
| 451 |
full_task = task_context + "\n\n" + user_query if task_context else user_query
|
| 452 |
|
| 453 |
+
# Run workflow
|
| 454 |
+
result, total_cost = await run_workflow(orchestrator, full_task, is_resume=False)
|
|
|
|
|
|
|
| 455 |
|
| 456 |
+
if not result:
|
| 457 |
+
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
|
| 459 |
+
# Check if clarification needed
|
| 460 |
+
if result.get('status') == 'clarifying' and result.get('clarifying_questions'):
|
| 461 |
+
cl.user_session.set("waiting_for_clarification", True)
|
| 462 |
+
await cl.Message(
|
| 463 |
+
content=f"## Before I proceed, I have some questions:\n\n{result['clarifying_questions']}\n\n**Please answer above to continue.**"
|
| 464 |
+
).send()
|
| 465 |
+
return
|
| 466 |
|
| 467 |
+
# Show final result
|
| 468 |
+
await cl.Message(content=format_final_result(result, total_cost)).send()
|
|
|
|
|
|
|
| 469 |
|
| 470 |
+
# Show generated code
|
| 471 |
+
if result.get('code_changes'):
|
| 472 |
+
await cl.Message(content="## Generated Code\n\n" + format_code_output(result['code_changes'])).send()
|
| 473 |
|
| 474 |
+
# Show plan if available
|
| 475 |
+
if result.get('plan') and not result.get('code_changes'):
|
| 476 |
+
await cl.Message(content=f"## Plan\n\n{result['plan']}").send()
|
| 477 |
|
| 478 |
|
| 479 |
if __name__ == "__main__":
|
|
|
|
| 480 |
sys.exit("Run with: chainlit run chainlit_app.py")
|