File size: 11,528 Bytes
bb9baa9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
"""
Agent Execution Module

Executes analysis plans step-by-step with progress tracking and error handling.

The executor:
- Runs each step in the plan sequentially
- Tracks progress for each step
- Handles errors gracefully
- Logs execution details
- Stores results

UNIVERSAL DESIGN:
- Works with any plan generated by the planner
- Adapts to different restaurant types
- Provides real-time progress updates
"""

from typing import List, Dict, Any, Optional, Callable
from datetime import datetime
import time


class AgentExecutor:
    """
    Executes analysis plans step-by-step.
    
    Features:
    - Sequential step execution
    - Progress tracking (% complete, time remaining)
    - Error handling and recovery
    - Result storage
    - Real-time status updates
    
    Example:
        executor = AgentExecutor()
        
        # Execute a plan
        results = executor.execute_plan(
            plan=plan,
            progress_callback=lambda status: print(status)
        )
        
        # Check execution status
        if executor.execution_successful:
            print("All steps completed!")
    """
    
    def __init__(self):
        """Initialize the executor."""
        self.execution_log: List[str] = []
        self.step_results: Dict[int, Any] = {}
        self.execution_successful: bool = False
        self.current_step: int = 0
        self.total_steps: int = 0
        self.start_time: Optional[float] = None
        self.end_time: Optional[float] = None
    
    def execute_plan(
        self,
        plan: List[Dict[str, Any]],
        progress_callback: Optional[Callable[[str], None]] = None,
        context: Optional[Dict[str, Any]] = None
    ) -> Dict[str, Any]:
        """
        Execute an analysis plan step-by-step.
        
        D2-003: Method skeleton
        D2-004: Step-by-step execution logic
        D2-005: Progress tracking
        D2-006: Error handling
        
        Args:
            plan: List of steps to execute
            progress_callback: Optional callback for progress updates
            context: Optional context data (URLs, data, etc.)
        
        Returns:
            Dictionary with execution results:
                - success: Boolean indicating if execution completed
                - results: Results from each step
                - execution_time: Total time taken
                - logs: Execution log entries
        
        Example:
            def show_progress(status):
                print(f"Progress: {status}")
            
            results = executor.execute_plan(
                plan=my_plan,
                progress_callback=show_progress
            )
        """
        # D2-005: Initialize progress tracking
        self.total_steps = len(plan)
        self.current_step = 0
        self.start_time = time.time()
        self.execution_successful = False
        self.step_results = {}
        self.execution_log = []
        
        self._log(f"Starting execution of {self.total_steps}-step plan")
        
        if progress_callback:
            progress_callback(f"Initializing executor (0/{self.total_steps} steps)")
        
        # D2-004: Execute each step sequentially
        for step in plan:
            self.current_step = step['step']
            
            try:
                # D2-005: Update progress
                progress_pct = int((self.current_step / self.total_steps) * 100)
                self._log(f"Step {self.current_step}/{self.total_steps} ({progress_pct}%): {step['action']}")
                
                if progress_callback:
                    progress_callback(
                        f"Step {self.current_step}/{self.total_steps}: {step['action']}"
                    )
                
                # D2-004: Execute the step
                result = self._execute_step(step, context)
                
                # Store result
                self.step_results[self.current_step] = {
                    'action': step['action'],
                    'result': result,
                    'status': 'success',
                    'timestamp': datetime.now().isoformat()
                }
                
                self._log(f"โœ… Step {self.current_step} completed: {step['action']}")
                
            except Exception as e:
                # D2-006: Error handling
                self._log(f"โŒ Step {self.current_step} failed: {step['action']}")
                self._log(f"   Error: {str(e)}")
                
                # Store error
                self.step_results[self.current_step] = {
                    'action': step['action'],
                    'result': None,
                    'status': 'failed',
                    'error': str(e),
                    'timestamp': datetime.now().isoformat()
                }
                
                if progress_callback:
                    progress_callback(f"โš ๏ธ  Step {self.current_step} failed: {str(e)}")
                
                # Decide whether to continue or stop
                # For now, we'll log and continue (graceful degradation)
                self._log(f"โš ๏ธ  Continuing with remaining steps...")
        
        # Execution complete
        self.end_time = time.time()
        execution_time = self.end_time - self.start_time
        
        # Check if all steps succeeded
        failed_steps = [
            step_num for step_num, result in self.step_results.items()
            if result['status'] == 'failed'
        ]
        
        self.execution_successful = len(failed_steps) == 0
        
        if self.execution_successful:
            self._log(f"โœ… Execution completed successfully in {execution_time:.2f}s")
        else:
            self._log(f"โš ๏ธ  Execution completed with {len(failed_steps)} failed steps in {execution_time:.2f}s")
        
        if progress_callback:
            if self.execution_successful:
                progress_callback(f"โœ… All {self.total_steps} steps completed!")
            else:
                progress_callback(f"โš ๏ธ  {self.total_steps - len(failed_steps)}/{self.total_steps} steps completed")
        
        # Return results
        return {
            'success': self.execution_successful,
            'results': self.step_results,
            'execution_time': execution_time,
            'logs': self.execution_log,
            'failed_steps': failed_steps
        }
    
    def _execute_step(
        self,
        step: Dict[str, Any],
        context: Optional[Dict[str, Any]]
    ) -> Any:
        """
        Execute a single step.
        
        D2-004: Core step execution logic
        
        Args:
            step: Step dictionary with action, params, reason
            context: Execution context
        
        Returns:
            Result from executing the step
        
        Note:
            This is a placeholder. In future days, we'll implement
            actual logic for each action type (scrape, analyze, etc.)
        """
        action = step['action']
        params = step.get('params', {})
        
        # For now, simulate execution with a small delay
        # In future days, we'll add real implementations for each action
        time.sleep(0.1)  # Simulate work
        
        # Placeholder results based on action type
        if action == 'scrape_reviews':
            return {'status': 'simulated', 'reviews_count': 500}
        elif action == 'discover_menu_items':
            return {'status': 'simulated', 'items_found': 52}
        elif action == 'discover_aspects':
            return {'status': 'simulated', 'aspects_found': 7}
        elif action == 'analyze_sentiment':
            return {'status': 'simulated', 'overall_sentiment': 0.73}
        else:
            return {'status': 'simulated', 'action': action}
    
    def _log(self, message: str) -> None:
        """
        Log execution progress.
        
        Args:
            message: Log message
        """
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        log_entry = f"[{timestamp}] {message}"
        self.execution_log.append(log_entry)
        print(f"โš™๏ธ  {log_entry}")
    
    def get_execution_summary(self) -> Dict[str, Any]:
        """
        Get a summary of the execution.
        
        Returns:
            Dictionary with summary info
        """
        if not self.start_time:
            return {'status': 'not_started'}
        
        execution_time = (self.end_time - self.start_time) if self.end_time else 0
        
        return {
            'total_steps': self.total_steps,
            'completed_steps': len(self.step_results),
            'successful_steps': sum(1 for r in self.step_results.values() if r['status'] == 'success'),
            'failed_steps': sum(1 for r in self.step_results.values() if r['status'] == 'failed'),
            'execution_time': f"{execution_time:.2f}s",
            'success': self.execution_successful
        }


# D2-007: Test execution with sample plan
if __name__ == "__main__":
    print("=" * 70)
    print("D2-007: Testing Agent Executor with Sample Plan")
    print("=" * 70 + "\n")
    
    # Create a sample plan (similar to what planner generates)
    sample_plan = [
        {
            'step': 1,
            'action': 'scrape_reviews',
            'params': {'url': 'https://opentable.ca/r/test-restaurant'},
            'reason': 'Need review data'
        },
        {
            'step': 2,
            'action': 'discover_menu_items',
            'params': {'reviews': 'scraped_data'},
            'reason': 'Discover menu items dynamically'
        },
        {
            'step': 3,
            'action': 'discover_aspects',
            'params': {'reviews': 'scraped_data'},
            'reason': 'Discover relevant aspects'
        },
        {
            'step': 4,
            'action': 'analyze_sentiment',
            'params': {'reviews': 'scraped_data'},
            'reason': 'Calculate sentiment scores'
        },
        {
            'step': 5,
            'action': 'generate_insights_chef',
            'params': {'analysis': 'results'},
            'reason': 'Create chef summary'
        }
    ]
    
    # Create executor
    executor = AgentExecutor()
    
    # Define progress callback
    def show_progress(status):
        print(f"๐Ÿ“Š {status}")
    
    # Execute the plan
    print("Starting execution...\n")
    results = executor.execute_plan(
        plan=sample_plan,
        progress_callback=show_progress
    )
    
    # Display results
    print("\n" + "=" * 70)
    print("EXECUTION RESULTS")
    print("=" * 70)
    
    print(f"\nSuccess: {results['success']}")
    print(f"Execution time: {results['execution_time']:.2f}s")
    print(f"Steps completed: {len(results['results'])}/{len(sample_plan)}")
    
    if results['failed_steps']:
        print(f"Failed steps: {results['failed_steps']}")
    
    print("\nStep Results:")
    for step_num, result in results['results'].items():
        status_icon = "โœ…" if result['status'] == 'success' else "โŒ"
        print(f"  {status_icon} Step {step_num}: {result['action']} - {result['status']}")
    
    # Get summary
    print("\n" + "=" * 70)
    print("EXECUTION SUMMARY")
    print("=" * 70)
    summary = executor.get_execution_summary()
    for key, value in summary.items():
        print(f"  {key}: {value}")
    
    print("\n" + "=" * 70)
    print("๐ŸŽ‰ Executor test complete!")
    print("=" * 70)