Skip to content

Commit b82a11d

Browse files
feat: Implement real-time progress updates for test generation
This commit introduces a real-time progress update feature to the UI. When a user clicks "Generate & Execute Test", the backend now streams progress updates to the frontend, which are displayed in the "Execution Results" panel. Key changes: - Modified the agentic workflow in `robot_generator.py` to be a generator that yields status updates at each step of the process. - Refactored the `/generate-and-run` endpoint in `main.py` to use Server-Sent Events (SSE) with a heartbeat mechanism. The heartbeat prevents proxy buffering issues and ensures real-time delivery of messages. - The synchronous agentic workflow is run in a separate thread to avoid blocking the async server. A queue is used to communicate between the thread and the main event loop. - Updated the frontend JavaScript in `index.html` to handle the SSE stream and display the progress messages in a running log format in the UI.
1 parent 5689e47 commit b82a11d

File tree

3 files changed

+116
-35
lines changed

3 files changed

+116
-35
lines changed

backend/main.py

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
from dotenv import load_dotenv
1010
from fastapi.middleware.cors import CORSMiddleware
1111
import json
12+
import asyncio
13+
from queue import Queue, Empty
14+
from threading import Thread
1215

1316
# Import the new agentic workflow orchestrator
1417
from backend.robot_generator import run_agentic_workflow
@@ -37,29 +40,72 @@ class Query(BaseModel):
3740
)
3841

3942
# --- Main Endpoint ---
40-
async def stream_generate_and_run(user_query: str, model_name: str):
41-
"""Generator function to stream logs and results."""
43+
def run_workflow_in_thread(queue, user_query, model_provider, model_name):
44+
"""Runs the synchronous agentic workflow and puts results in a queue."""
45+
try:
46+
for event in run_agentic_workflow(user_query, model_provider, model_name):
47+
queue.put(event)
48+
except Exception as e:
49+
logging.error(f"Exception in workflow thread: {e}")
50+
queue.put({"status": "error", "message": f"Workflow thread failed: {e}"})
4251

43-
# Stage 1: Generating Code
44-
yield f"data: {json.dumps({'stage': 'generation', 'status': 'running', 'message': 'AI agents are analyzing your query...'})}\n\n"
52+
async def stream_generate_and_run(user_query: str, model_name: str):
53+
"""
54+
Generator function that streams logs and results, with a heartbeat
55+
to prevent timeouts and buffer flushing issues.
56+
"""
57+
robot_code = None
58+
model_provider = os.getenv("MODEL_PROVIDER", "online").lower()
59+
q = Queue()
60+
61+
# Run the synchronous workflow in a separate thread
62+
workflow_thread = Thread(
63+
target=run_workflow_in_thread,
64+
args=(q, user_query, model_provider, model_name)
65+
)
66+
workflow_thread.start()
67+
68+
# --- Stage 1: Generating Code ---
69+
while workflow_thread.is_alive():
70+
try:
71+
event = q.get_nowait()
72+
event_data = {'stage': 'generation', **event}
73+
yield f"data: {json.dumps(event_data)}\n\n"
74+
75+
if event.get("status") == "complete" and "robot_code" in event:
76+
robot_code = event["robot_code"]
77+
# Generation is done, we can break this loop and move to execution
78+
workflow_thread.join() # Ensure thread is cleaned up
79+
break
80+
elif event.get("status") == "error":
81+
logging.error(f"Error during code generation: {event.get('message')}")
82+
workflow_thread.join()
83+
return
84+
except Empty:
85+
# Send a heartbeat comment to keep the connection open
86+
yield ": heartbeat\n\n"
87+
await asyncio.sleep(1)
88+
89+
# In case the thread finished but we didn't get the code
90+
if not robot_code:
91+
# Check the queue one last time
92+
while not q.empty():
93+
event = q.get_nowait()
94+
event_data = {'stage': 'generation', **event}
95+
yield f"data: {json.dumps(event_data)}\n\n"
96+
if event.get("status") == "complete" and "robot_code" in event:
97+
robot_code = event["robot_code"]
98+
elif event.get("status") == "error":
99+
return # Error was already sent
45100

46-
try:
47-
model_provider = os.getenv("MODEL_PROVIDER", "online").lower()
48-
robot_code = run_agentic_workflow(
49-
natural_language_query=user_query,
50-
model_provider=model_provider,
51-
model_name=model_name
52-
)
53101
if not robot_code:
54-
raise Exception("Agentic workflow failed to generate Robot Framework code.")
102+
final_error_message = "Agentic workflow finished without generating code."
103+
logging.error(final_error_message)
104+
yield f"data: {json.dumps({'stage': 'generation', 'status': 'error', 'message': final_error_message})}\n\n"
105+
return
55106

56-
yield f"data: {json.dumps({'stage': 'generation', 'status': 'complete', 'message': 'Code generation complete.', 'robot_code': robot_code})}\n\n"
57-
except Exception as e:
58-
logging.error(f"Error during code generation: {e}")
59-
yield f"data: {json.dumps({'stage': 'generation', 'status': 'error', 'message': str(e)})}\n\n"
60-
return
61107

62-
# Stage 2: Docker Execution
108+
# --- Stage 2: Docker Execution ---
63109
run_id = str(uuid.uuid4())
64110
robot_tests_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'robot_tests', run_id)
65111
os.makedirs(robot_tests_dir, exist_ok=True)

backend/robot_generator.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -288,53 +288,65 @@ def agent_code_validator(code: str, model_provider: str, model_name: str) -> Val
288288

289289
# --- Orchestrator ---
290290

291-
def run_agentic_workflow(natural_language_query: str, model_provider: str, model_name: str) -> Optional[str]:
291+
def run_agentic_workflow(natural_language_query: str, model_provider: str, model_name: str):
292292
"""
293293
Orchestrates the multi-agent workflow to generate Robot Framework code,
294-
including a self-correction loop.
294+
yielding progress updates and the final code.
295295
"""
296296
logging.info("--- Starting Multi-Agent Workflow ---")
297+
yield {"status": "running", "message": "Starting agentic workflow..."}
297298
MAX_ATTEMPTS = 3
298299

299300
# Configure online provider if used
300301
if model_provider == "online":
301302
api_key = os.getenv("GEMINI_API_KEY")
302303
if not api_key:
303304
logging.error("Orchestrator: GEMINI_API_KEY not found for online provider.")
304-
raise ValueError("GEMINI_API_KEY not found in environment variables.")
305+
yield {"status": "error", "message": "GEMINI_API_KEY not found."}
306+
return
305307
genai.configure(api_key=api_key)
306308

307-
# The initial query for the first attempt
308309
current_query = natural_language_query
309310

310311
for attempt in range(MAX_ATTEMPTS):
311312
logging.info(f"--- Attempt {attempt + 1} of {MAX_ATTEMPTS} ---")
313+
yield {"status": "running", "message": f"Starting attempt {attempt + 1}/{MAX_ATTEMPTS}..."}
312314

313315
# Agent 1: Plan
316+
yield {"status": "running", "message": "Agent 1/4: Planning test steps..."}
314317
planned_steps = agent_step_planner(current_query, model_provider, model_name)
315318
if not planned_steps:
316319
logging.error("Orchestrator: Step Planner failed. Aborting.")
317-
return None
320+
yield {"status": "error", "message": "Failed to generate a test plan."}
321+
return
322+
yield {"status": "running", "message": "Agent 1/4: Test step planning complete."}
318323

319324
# Agent 2: Identify Locators
325+
yield {"status": "running", "message": "Agent 2/4: Identifying UI element locators..."}
320326
located_steps = agent_element_identifier(planned_steps, model_provider, model_name)
321327
if not located_steps:
322328
logging.error("Orchestrator: Element Identifier failed. Aborting.")
323-
return None
329+
yield {"status": "error", "message": "Failed to identify UI element locators."}
330+
return
331+
yield {"status": "running", "message": "Agent 2/4: UI element locator identification complete."}
324332

325333
# Agent 3: Assemble Code
334+
yield {"status": "running", "message": "Agent 3/4: Assembling Robot Framework code..."}
326335
robot_code = agent_code_assembler(located_steps, natural_language_query)
336+
yield {"status": "running", "message": "Agent 3/4: Code assembly complete."}
327337

328338
# Agent 4: Validate
339+
yield {"status": "running", "message": "Agent 4/4: Validating generated code..."}
329340
validation = agent_code_validator(robot_code, model_provider, model_name)
341+
yield {"status": "running", "message": "Agent 4/4: Code validation complete."}
330342

331343
if validation.valid:
332344
logging.info("Code validation successful. Workflow complete.")
333-
logging.info("--- Multi-Agent Workflow Complete ---")
334-
return robot_code
345+
yield {"status": "complete", "robot_code": robot_code, "message": "Code generation successful."}
346+
return
335347
else:
336348
logging.warning(f"Code validation failed. Reason: {validation.reason}")
337-
# Prepare for the next attempt by creating a corrective query
349+
yield {"status": "running", "message": f"Validation failed: {validation.reason}. Attempting self-correction..."}
338350
current_query = f"""
339351
The previous attempt to generate a test plan failed validation.
340352
The user's original query was: "{natural_language_query}"
@@ -349,5 +361,4 @@ def run_agentic_workflow(natural_language_query: str, model_provider: str, model
349361
logging.info("Attempting self-correction...")
350362

351363
logging.error("Orchestrator: Failed to generate valid code after multiple attempts.")
352-
logging.info("--- Multi-Agent Workflow Failed ---")
353-
return None
364+
yield {"status": "error", "message": "Failed to generate valid code after multiple attempts."}

frontend/index.html

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -626,8 +626,8 @@ <h1>AI Test Automation Platform</h1>
626626
}
627627

628628
setButtonLoading(true);
629-
robotCodeEl.innerHTML = '';
630-
executionLogsEl.innerHTML = '';
629+
robotCodeEl.innerHTML = '<div class="empty-state"><div class="empty-state-icon">⚡</div><p>Generated Robot Framework code will appear here</p></div>';
630+
executionLogsEl.innerHTML = ''; // Clear previous logs
631631
downloadBtn.style.display = 'none';
632632
robotCodeContent = '';
633633

@@ -687,19 +687,40 @@ <h1>AI Test Automation Platform</h1>
687687
updateStatus('processing', data.message);
688688

689689
if (data.stage === 'generation') {
690-
if (data.status === 'complete' && data.robot_code) {
690+
// Display running logs for generation stage
691+
if (data.status === 'running') {
692+
const logEntry = document.createElement('div');
693+
logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${data.message}`;
694+
executionLogsEl.appendChild(logEntry);
695+
executionLogsEl.scrollTop = executionLogsEl.scrollHeight; // Auto-scroll
696+
} else if (data.status === 'complete' && data.robot_code) {
691697
robotCodeContent = data.robot_code;
692698
robotCodeEl.textContent = robotCodeContent;
693699
downloadBtn.style.display = 'inline-flex';
700+
// Clear generation logs and prepare for execution logs
701+
executionLogsEl.innerHTML = '<div class="empty-state"><div class="empty-state-icon">📋</div><p>Test execution logs will appear here</p></div>';
694702
} else if (data.status === 'error') {
695703
updateStatus('error', 'Generation failed');
696704
robotCodeEl.innerHTML = `<div class="empty-state"><div class="empty-state-icon">⚠️</div><p>${data.message}</p></div>`;
705+
const errorEntry = document.createElement('div');
706+
errorEntry.style.color = 'var(--error)';
707+
errorEntry.textContent = `[${new Date().toLocaleTimeString()}] ERROR: ${data.message}`;
708+
executionLogsEl.appendChild(errorEntry);
697709
hideStatus();
698710
}
699711
} else if (data.stage === 'execution') {
700-
if (data.status === 'complete' && data.result) {
712+
// When execution starts, clear the placeholder and show the first real log
713+
if (executionLogsEl.querySelector('.empty-state')) {
714+
executionLogsEl.innerHTML = '';
715+
}
716+
if (data.status === 'running') {
717+
const logEntry = document.createElement('div');
718+
logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${data.message}`;
719+
executionLogsEl.appendChild(logEntry);
720+
executionLogsEl.scrollTop = executionLogsEl.scrollHeight;
721+
} else if (data.status === 'complete' && data.result) {
701722
const logs = data.result.logs || 'No execution logs available';
702-
executionLogsEl.textContent = logs;
723+
executionLogsEl.textContent = logs; // Replace with final, full logs
703724
if (logs.includes('PASSED')) {
704725
updateStatus('success', 'Test passed');
705726
} else {
@@ -708,7 +729,10 @@ <h1>AI Test Automation Platform</h1>
708729
hideStatus();
709730
} else if (data.status === 'error') {
710731
updateStatus('error', 'Execution failed');
711-
executionLogsEl.textContent = data.message;
732+
const errorEntry = document.createElement('div');
733+
errorEntry.style.color = 'var(--error)';
734+
errorEntry.textContent = `[${new Date().toLocaleTimeString()}] ERROR: ${data.message}`;
735+
executionLogsEl.appendChild(errorEntry);
712736
hideStatus();
713737
}
714738
}

0 commit comments

Comments
 (0)