Skip to content

Commit 1be88e8

Browse files
committed
feat: Enhance report generation and UI event handling for improved user feedback and phase detection
1 parent 642ff25 commit 1be88e8

5 files changed

Lines changed: 253 additions & 17 deletions

File tree

agent/agent.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ def _initialize_context_and_tools(self):
7878
if self.config.ui_mode and self.ui_manager:
7979
self.react_engine.set_ui_manager(self.ui_manager)
8080

81+
# Also set UI manager for all tools that support it
82+
from ui.events import UIEventEmitter
83+
for tool in self.tools:
84+
if isinstance(tool, UIEventEmitter):
85+
tool.set_ui_manager(self.ui_manager)
86+
logger.debug(f"Set UI manager for tool: {tool.name}")
87+
8188
self.agent_logger.info("Context manager, tools, and ReAct engine initialized")
8289

8390
def _initialize_tools(self) -> List:

main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121

2222
console = Console()
2323

24+
# Note: You may see "Exception ignored while finalizing... ValueError: I/O operation on closed file"
25+
# at the end of execution. This is a harmless cleanup issue from urllib3/docker-py during
26+
# garbage collection and does not affect functionality. Python already handles it gracefully.
27+
2428

2529
def detect_project_directory_in_container(orchestrator: DockerOrchestrator) -> Optional[str]:
2630
"""

tools/report_tool.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88

99
from .base import BaseTool, ToolResult
1010
from reporting import render_condensed_summary, truncate_list, format_percentage
11+
from ui.events import UIEventEmitter, EventType
1112

1213

13-
class ReportTool(BaseTool):
14+
class ReportTool(BaseTool, UIEventEmitter):
1415
"""
1516
Tool for generating comprehensive project setup reports and marking task completion.
1617
@@ -27,12 +28,14 @@ class ReportTool(BaseTool):
2728
"""
2829

2930
def __init__(self, docker_orchestrator=None, execution_history_callback=None, context_manager=None, physical_validator=None):
30-
super().__init__(
31+
BaseTool.__init__(
32+
self,
3133
name="report",
3234
description="Generate comprehensive project setup report and mark task as complete. "
3335
"Creates both console output and a Markdown file in /workspace. "
3436
"Use this tool when all main tasks are finished to summarize the work done.",
3537
)
38+
UIEventEmitter.__init__(self)
3639
self.docker_orchestrator = docker_orchestrator
3740
self.execution_history_callback = execution_history_callback
3841
self.context_manager = context_manager
@@ -167,6 +170,19 @@ def execute(
167170
report_snapshot,
168171
)
169172

173+
# Emit UI event for report generation
174+
self.emit(
175+
EventType.REPORT_GENERATED,
176+
message=f"Report generated: {report_filename}",
177+
report_path=f"/workspace/{report_filename}",
178+
status=verified_status,
179+
build_success=actual_accomplishments.get('build_success', False),
180+
test_success=actual_accomplishments.get('test_success', False),
181+
test_pass_rate=actual_accomplishments.get('physical_validation', {}).get('test_analysis', {}).get('pass_rate', 0),
182+
total_tests=actual_accomplishments.get('physical_validation', {}).get('test_analysis', {}).get('total_tests', 0),
183+
passed_tests=actual_accomplishments.get('physical_validation', {}).get('test_analysis', {}).get('passed_tests', 0)
184+
)
185+
170186
return ToolResult(
171187
success=True,
172188
output=condensed_output,

ui/events.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ class EventType(str, Enum):
5454
# Project events
5555
PROJECT_ANALYSIS = "project_analysis"
5656

57+
# Report events
58+
REPORT_GENERATED = "report_generated"
59+
5760
# Error events
5861
ERROR = "error"
5962
WARNING = "warning"

ui/ui_manager.py

Lines changed: 221 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ def __init__(self, project_name: str, console: Optional[Console] = None):
7575
self.is_complete = False
7676
self.final_status: Optional[str] = None
7777

78+
# Report information
79+
self.report_data: Optional[dict] = None # Report metadata (path, status, metrics)
80+
7881
# Live display
7982
self.live: Optional[Live] = None
8083

@@ -140,6 +143,178 @@ def _format_tool_params(self, tool_name: str, params: dict) -> str:
140143
return f"({', '.join(formatted_parts)})"
141144
return ""
142145

146+
def _detect_phase_from_action(self, tool_name: str, tool_params: dict) -> Optional[PhaseType]:
147+
"""
148+
Detect which phase the agent is in based on tool usage.
149+
150+
Args:
151+
tool_name: Name of the tool being used
152+
tool_params: Tool parameters
153+
154+
Returns:
155+
PhaseType if phase detected, None otherwise
156+
"""
157+
# Already in verification or all phases complete - don't transition back
158+
if self.current_phase == PhaseType.VERIFICATION:
159+
return None
160+
161+
# Report tool = verification phase
162+
if tool_name == "report":
163+
return PhaseType.VERIFICATION
164+
165+
# Check for test-related activities
166+
if tool_name in ["maven", "gradle"]:
167+
goal = tool_params.get("goal", "")
168+
task = tool_params.get("task", "")
169+
action = tool_params.get("action", "")
170+
171+
# Test phase indicators
172+
if "test" in goal.lower() or "test" in task.lower() or "test" in action.lower():
173+
return PhaseType.TEST
174+
175+
# Build phase indicators (compile, package, install)
176+
if any(keyword in goal.lower() or keyword in task.lower()
177+
for keyword in ["compile", "package", "install", "build", "assemble"]):
178+
return PhaseType.BUILD
179+
180+
# Bash commands
181+
if tool_name == "bash":
182+
command = tool_params.get("command", "")
183+
command_lower = command.lower()
184+
185+
# Test indicators in bash commands
186+
if any(keyword in command_lower for keyword in ["mvn test", "gradle test", "pytest", "npm test", "test"]):
187+
return PhaseType.TEST
188+
189+
# Build indicators in bash commands
190+
if any(keyword in command_lower for keyword in ["mvn compile", "mvn package", "mvn install",
191+
"gradle build", "gradle assemble", "make", "npm run build"]):
192+
return PhaseType.BUILD
193+
194+
return None
195+
196+
def _extract_thought_summary(self, thought: str) -> str:
197+
"""
198+
Extract a meaningful summary from agent thought.
199+
200+
Args:
201+
thought: Full thought content
202+
203+
Returns:
204+
Concise summary (30-80 chars) with ellipsis
205+
"""
206+
# Remove common prefixes
207+
thought = thought.strip()
208+
thought = thought.replace("I need to ", "").replace("I should ", "").replace("I will ", "")
209+
210+
# Find first sentence or meaningful chunk
211+
sentences = thought.split(". ")
212+
if sentences:
213+
summary = sentences[0].strip()
214+
# Limit length
215+
if len(summary) > 80:
216+
summary = summary[:77] + "..."
217+
elif len(summary) < 20:
218+
# If too short, include second sentence if available
219+
if len(sentences) > 1:
220+
summary = f"{summary}. {sentences[1][:40]}..."
221+
else:
222+
summary = summary + "..."
223+
224+
return summary
225+
226+
# Fallback
227+
return thought[:77] + "..." if len(thought) > 80 else thought
228+
229+
def _extract_observation_summary(self, observation: str) -> str:
230+
"""
231+
Extract a meaningful summary from agent observation.
232+
233+
Args:
234+
observation: Full observation content
235+
236+
Returns:
237+
Concise summary (50-100 chars) with ellipsis
238+
"""
239+
# Clean up observation
240+
observation = observation.strip()
241+
242+
# Look for key indicators of success/failure
243+
if "successfully" in observation.lower() or "success" in observation.lower():
244+
# Extract success message
245+
lines = observation.split("\n")
246+
for line in lines:
247+
if "success" in line.lower():
248+
summary = line.strip()
249+
if len(summary) > 100:
250+
return summary[:97] + "..."
251+
return summary + "..."
252+
253+
# Look for error indicators
254+
if "error" in observation.lower() or "failed" in observation.lower():
255+
lines = observation.split("\n")
256+
for line in lines:
257+
if "error" in line.lower() or "failed" in line.lower():
258+
summary = line.strip()
259+
if len(summary) > 100:
260+
return summary[:97] + "..."
261+
return summary + "..."
262+
263+
# Default: first meaningful line
264+
lines = observation.split("\n")
265+
for line in lines:
266+
line = line.strip()
267+
if len(line) > 10: # Skip very short lines
268+
if len(line) > 100:
269+
return line[:97] + "..."
270+
return line + "..."
271+
272+
# Fallback
273+
return observation[:97] + "..." if len(observation) > 100 else observation
274+
275+
def _format_report_summary(self) -> Panel:
276+
"""
277+
Format report summary panel for display.
278+
279+
Returns:
280+
Rich Panel with report information
281+
"""
282+
if not self.report_data:
283+
return Panel("No report data available", border_style="yellow")
284+
285+
# Extract report data
286+
report_path = self.report_data.get("report_path", "Unknown")
287+
status = self.report_data.get("status", "unknown")
288+
build_success = self.report_data.get("build_success", False)
289+
test_success = self.report_data.get("test_success", False)
290+
total_tests = self.report_data.get("total_tests", 0)
291+
passed_tests = self.report_data.get("passed_tests", 0)
292+
test_pass_rate = self.report_data.get("test_pass_rate", 0)
293+
294+
# Calculate pass rate if not provided or if it's 0 but we have test data
295+
if test_pass_rate == 0 and total_tests > 0:
296+
test_pass_rate = (passed_tests / total_tests) * 100
297+
298+
# Build content
299+
content = f"📄 [bold cyan]Final Report Generated[/bold cyan]\n\n"
300+
content += f" [cyan]Location:[/cyan] {report_path}\n"
301+
content += f" [cyan]Status:[/cyan] {status.upper()}\n\n"
302+
303+
content += " [bold]Results:[/bold]\n"
304+
build_icon = "✅" if build_success else "❌"
305+
content += f" {build_icon} Build: {'SUCCESS' if build_success else 'FAILED'}\n"
306+
307+
if total_tests > 0:
308+
test_icon = "✅" if test_success else "❌"
309+
content += f" {test_icon} Tests: {passed_tests}/{total_tests} passed ({test_pass_rate:.1f}%)\n"
310+
311+
return Panel(
312+
content,
313+
title="📊 Setup Report",
314+
border_style="green" if status == "success" else "yellow",
315+
padding=(1, 2)
316+
)
317+
143318
def handle_event(self, event: UIEvent):
144319
"""
145320
Handle a UI event and update the display
@@ -170,6 +345,8 @@ def handle_event(self, event: UIEvent):
170345
self._handle_success(event)
171346
elif event.event_type == EventType.FAILURE:
172347
self._handle_failure(event)
348+
elif event.event_type == EventType.REPORT_GENERATED:
349+
self._handle_report_generated(event)
173350
elif event.event_type in [EventType.AGENT_THOUGHT, EventType.AGENT_ACTION, EventType.AGENT_OBSERVATION]:
174351
self._handle_agent_event(event)
175352

@@ -263,6 +440,18 @@ def _handle_failure(self, event: UIEvent):
263440
self.final_status = "failure"
264441
self.current_status = event.message
265442

443+
def _handle_report_generated(self, event: UIEvent):
444+
"""Handle report generation event"""
445+
# Store report metadata
446+
self.report_data = event.metadata
447+
448+
# Complete verification phase
449+
if self.current_phase == PhaseType.VERIFICATION:
450+
self.phases_data[PhaseType.VERIFICATION]["status"] = "success"
451+
452+
# Update status
453+
self.current_status = "Report generated"
454+
266455
def _handle_agent_event(self, event: UIEvent):
267456
"""Handle agent ReAct events (thought, action, observation)"""
268457
# Track agent steps for collapsible display
@@ -271,8 +460,11 @@ def _handle_agent_event(self, event: UIEvent):
271460
self.agent_current_step_num = event.metadata.get("step_num", self.agent_current_step_num + 1)
272461
self.agent_current_action = "thinking"
273462
self.agent_current_tool = None
274-
self.agent_detail = f"Step {self.agent_current_step_num}: Analyzing situation..."
275-
self.current_status = "Agent thinking"
463+
464+
# Extract meaningful summary from thought
465+
thought_summary = self._extract_thought_summary(event.message)
466+
self.agent_detail = f"Step {self.agent_current_step_num}: {thought_summary}"
467+
self.current_status = thought_summary
276468

277469
self.current_agent_step = {
278470
"thought": event.message,
@@ -281,6 +473,7 @@ def _handle_agent_event(self, event: UIEvent):
281473
"status": "running"
282474
}
283475
self.agent_steps.append(self.current_agent_step)
476+
284477
elif event.event_type == EventType.AGENT_ACTION and self.current_agent_step:
285478
self.agent_current_action = "acting"
286479
tool_name = event.metadata.get("tool_name", "unknown")
@@ -289,6 +482,18 @@ def _handle_agent_event(self, event: UIEvent):
289482
self.agent_current_tool = tool_name
290483
self.agent_tool_params = tool_params
291484

485+
# Detect phase transition based on tool usage
486+
detected_phase = self._detect_phase_from_action(tool_name, tool_params)
487+
if detected_phase and detected_phase != self.current_phase:
488+
# Transition to new phase
489+
# Complete previous phase if it was running
490+
if self.current_phase and self.phases_data[self.current_phase]["status"] == "running":
491+
self.phases_data[self.current_phase]["status"] = "success"
492+
493+
# Start new phase
494+
self.current_phase = detected_phase
495+
self.phases_data[detected_phase]["status"] = "running"
496+
292497
# Format parameters for display
293498
params_str = self._format_tool_params(tool_name, tool_params)
294499

@@ -301,10 +506,14 @@ def _handle_agent_event(self, event: UIEvent):
301506
self.current_status = f"Using {tool_name}"
302507

303508
self.current_agent_step["action"] = event.message
509+
304510
elif event.event_type == EventType.AGENT_OBSERVATION and self.current_agent_step:
305511
self.agent_current_action = "observing"
306-
self.agent_detail = f"Step {self.agent_current_step_num}: Processing results..."
307-
self.current_status = "Processing observation"
512+
513+
# Extract meaningful summary from observation
514+
observation_summary = self._extract_observation_summary(event.message)
515+
self.agent_detail = f"Step {self.agent_current_step_num}: {observation_summary}"
516+
self.current_status = observation_summary
308517

309518
self.current_agent_step["observation"] = event.message
310519
self.current_agent_step["status"] = "complete"
@@ -394,17 +603,8 @@ def _render_display(self):
394603
)
395604
elements.append(warning_panel)
396605

397-
# 5. Final status if complete
398-
if self.is_complete and self.final_status == "success":
399-
elements.append("") # Spacing
400-
success_panel = create_success_panel(
401-
self.current_status,
402-
summary_items=[
403-
("Total time", elapsed_str),
404-
("Phases completed", f"{sum(1 for p in self.phases_data.values() if p['status'] == 'success')}/4")
405-
]
406-
)
407-
elements.append(success_panel)
606+
# Note: Final success panel is NOT shown here to avoid duplication
607+
# It will be shown in display_final_summary() instead
408608

409609
return Group(*elements)
410610

@@ -443,6 +643,12 @@ def display_final_summary(self):
443643
)
444644
self.console.print(error_panel)
445645

646+
# Print report information if available
647+
if self.report_data:
648+
self.console.print()
649+
report_info = self._format_report_summary()
650+
self.console.print(report_info)
651+
446652
# Print detailed phase tree with all steps expanded
447653
self.console.print()
448654
self.console.print(Panel("📋 Detailed Execution Log", border_style="cyan"))

0 commit comments

Comments
 (0)