vibesurf 0.1.10__py3-none-any.whl → 0.1.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of vibesurf might be problematic. Click here for more details.

Files changed (51) hide show
  1. vibe_surf/_version.py +2 -2
  2. vibe_surf/agents/browser_use_agent.py +68 -45
  3. vibe_surf/agents/prompts/report_writer_prompt.py +73 -0
  4. vibe_surf/agents/prompts/vibe_surf_prompt.py +85 -172
  5. vibe_surf/agents/report_writer_agent.py +380 -226
  6. vibe_surf/agents/vibe_surf_agent.py +879 -825
  7. vibe_surf/agents/views.py +130 -0
  8. vibe_surf/backend/api/activity.py +3 -1
  9. vibe_surf/backend/api/browser.py +9 -5
  10. vibe_surf/backend/api/config.py +8 -5
  11. vibe_surf/backend/api/files.py +59 -50
  12. vibe_surf/backend/api/models.py +2 -2
  13. vibe_surf/backend/api/task.py +45 -12
  14. vibe_surf/backend/database/manager.py +24 -18
  15. vibe_surf/backend/database/queries.py +199 -192
  16. vibe_surf/backend/database/schemas.py +1 -1
  17. vibe_surf/backend/main.py +4 -2
  18. vibe_surf/backend/shared_state.py +28 -35
  19. vibe_surf/backend/utils/encryption.py +3 -1
  20. vibe_surf/backend/utils/llm_factory.py +41 -36
  21. vibe_surf/browser/agent_browser_session.py +0 -4
  22. vibe_surf/browser/browser_manager.py +14 -8
  23. vibe_surf/browser/utils.py +5 -3
  24. vibe_surf/browser/watchdogs/dom_watchdog.py +0 -45
  25. vibe_surf/chrome_extension/background.js +4 -0
  26. vibe_surf/chrome_extension/scripts/api-client.js +13 -0
  27. vibe_surf/chrome_extension/scripts/file-manager.js +27 -71
  28. vibe_surf/chrome_extension/scripts/session-manager.js +21 -3
  29. vibe_surf/chrome_extension/scripts/ui-manager.js +831 -48
  30. vibe_surf/chrome_extension/sidepanel.html +21 -4
  31. vibe_surf/chrome_extension/styles/activity.css +365 -5
  32. vibe_surf/chrome_extension/styles/input.css +139 -0
  33. vibe_surf/cli.py +4 -22
  34. vibe_surf/common.py +35 -0
  35. vibe_surf/llm/openai_compatible.py +148 -93
  36. vibe_surf/logger.py +99 -0
  37. vibe_surf/{controller/vibesurf_tools.py → tools/browser_use_tools.py} +233 -219
  38. vibe_surf/tools/file_system.py +415 -0
  39. vibe_surf/{controller → tools}/mcp_client.py +4 -3
  40. vibe_surf/tools/report_writer_tools.py +21 -0
  41. vibe_surf/tools/vibesurf_tools.py +657 -0
  42. vibe_surf/tools/views.py +120 -0
  43. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/METADATA +6 -2
  44. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/RECORD +49 -43
  45. vibe_surf/controller/file_system.py +0 -53
  46. vibe_surf/controller/views.py +0 -37
  47. /vibe_surf/{controller → tools}/__init__.py +0 -0
  48. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/WHEEL +0 -0
  49. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/entry_points.txt +0 -0
  50. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/licenses/LICENSE +0 -0
  51. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/top_level.txt +0 -0
@@ -1,156 +1,348 @@
1
1
  import logging
2
2
  import os
3
3
  import time
4
+ import re
5
+ import asyncio
6
+ from datetime import datetime
4
7
  from typing import Any, Dict, List
8
+ import json
5
9
 
10
+ from pydantic import BaseModel
6
11
  from browser_use.llm.base import BaseChatModel
7
- from browser_use.llm.messages import UserMessage
12
+ from browser_use.llm.messages import UserMessage, SystemMessage, AssistantMessage
13
+ from browser_use.utils import SignalHandler
8
14
 
9
- from vibe_surf.agents.prompts.vibe_surf_prompt import (
10
- REPORT_CONTENT_PROMPT,
11
- REPORT_FORMAT_PROMPT
12
- )
15
+ from vibe_surf.agents.prompts.report_writer_prompt import REPORT_WRITER_PROMPT
16
+ from vibe_surf.tools.file_system import CustomFileSystem
17
+ from vibe_surf.tools.report_writer_tools import ReportWriterTools
18
+ from vibe_surf.agents.views import CustomAgentOutput
13
19
 
14
- logger = logging.getLogger(__name__)
20
+ from vibe_surf.logger import get_logger
21
+
22
+ logger = get_logger(__name__)
23
+
24
+
25
+ class ReportTaskResult(BaseModel):
26
+ """Result of a report generation task"""
27
+ success: bool # True only if LLM completed successfully
28
+ msg: str # Success message or error details
29
+ report_path: str # Path to the generated report file
15
30
 
16
31
 
17
32
  class ReportWriterAgent:
18
- """Agent responsible for generating HTML reports using two-phase LLM generation"""
19
-
20
- def __init__(self, llm: BaseChatModel, workspace_dir: str):
33
+ """Agent responsible for generating HTML reports using LLM-controlled flow"""
34
+
35
+ def __init__(self, llm: BaseChatModel, workspace_dir: str, step_callback=None, thinking_mode: bool = True):
21
36
  """
22
37
  Initialize ReportWriterAgent
23
38
 
24
39
  Args:
25
40
  llm: Language model for generating report content
26
41
  workspace_dir: Directory to save reports
42
+ step_callback: Optional callback function to log each step
27
43
  """
28
44
  self.llm = llm
29
- self.workspace_dir = workspace_dir
45
+ self.workspace_dir = os.path.abspath(workspace_dir)
46
+ self.step_callback = step_callback
47
+ self.thinking_mode = thinking_mode
48
+
49
+ # Initialize file system and tools
50
+ self.file_system = CustomFileSystem(self.workspace_dir)
51
+ self.tools = ReportWriterTools()
52
+
53
+ # Setup action model and agent output
54
+ self.ActionModel = self.tools.registry.create_action_model()
55
+ if self.thinking_mode:
56
+ self.AgentOutput = CustomAgentOutput.type_with_custom_actions(self.ActionModel)
57
+ else:
58
+ self.AgentOutput = CustomAgentOutput.type_with_custom_actions_no_thinking(self.ActionModel)
59
+
60
+ # State management for pause/resume/stop control
61
+ self.paused = False
62
+ self.stopped = False
63
+ self.consecutive_failures = 0
64
+ self._external_pause_event = asyncio.Event()
65
+ self._external_pause_event.set()
30
66
 
31
- logger.info("📄 ReportWriterAgent initialized")
32
-
33
- async def generate_report(self, report_data: Dict[str, Any]) -> str:
67
+ # Initialize message history as instance variable
68
+ self.message_history = []
69
+
70
+ logger.info("📄 ReportWriterAgent initialized with LLM-controlled flow")
71
+
72
+ def pause(self) -> None:
73
+ """Pause the agent before the next step"""
74
+ logger.info('\n\n⏸️ Paused report writer agent.\n\tPress [Enter] to resume or [Ctrl+C] again to quit.')
75
+ self.paused = True
76
+ self._external_pause_event.clear()
77
+
78
+ def resume(self) -> None:
79
+ """Resume the agent"""
80
+ logger.info('▶️ Resuming report writer agent execution where it left off...\n')
81
+ self.paused = False
82
+ self._external_pause_event.set()
83
+
84
+ def stop(self) -> None:
85
+ """Stop the agent"""
86
+ logger.info('⏹️ Report writer agent stopping')
87
+ self.stopped = True
88
+ # Signal pause event to unblock any waiting code so it can check the stopped state
89
+ self._external_pause_event.set()
90
+
91
+ def add_new_task(self, new_task: str) -> None:
34
92
  """
35
- Generate HTML report using two-phase approach: content generation then formatting
93
+ Add a new task or guidance to the report writer agent during execution.
94
+ The new_task parameter contains a pre-formatted prompt from VibeSurfAgent.
95
+ """
96
+ # Add the pre-formatted prompt directly to message history
97
+ from browser_use.llm.messages import UserMessage
98
+ self.message_history.append(UserMessage(content=new_task))
99
+ logger.info(f"📝 Report writer agent received new task guidance")
100
+
101
+ async def generate_report(self, report_data: Dict[str, Any]) -> ReportTaskResult:
102
+ """
103
+ Generate HTML report using LLM-controlled flow
36
104
 
37
105
  Args:
38
106
  report_data: Dictionary containing:
39
- - original_task: The original user task
40
- - execution_results: List of BrowserTaskResult objects
41
- - report_type: Type of report ("summary", "detailed", "none")
42
- - upload_files: Optional list of uploaded files
107
+ - report_task: Report requirements, tips, and possible insights
108
+ - information: Collected information for the report
43
109
 
44
110
  Returns:
45
- str: Path to the generated report file
111
+ ReportTaskResult: Result containing success status, message, and report path
46
112
  """
47
- logger.info(f"📝 Generating {report_data.get('report_type', 'summary')} report...")
48
-
113
+ logger.info("📝 Starting LLM-controlled report generation...")
114
+
115
+ # Get current event loop
116
+ loop = asyncio.get_event_loop()
117
+
118
+ signal_handler = SignalHandler(
119
+ loop=loop,
120
+ pause_callback=self.pause,
121
+ resume_callback=self.resume,
122
+ exit_on_second_int=True,
123
+ )
124
+ signal_handler.register()
125
+
49
126
  try:
50
- # Phase 1: Generate report content
51
- logger.info("📖 Phase 1: Generating report content...")
52
- report_content = await self._generate_content(report_data)
53
-
54
- # Phase 2: Format content as HTML
55
- logger.info("🎨 Phase 2: Formatting as HTML...")
56
- html_content = await self._format_as_html(report_content)
57
-
58
- # Save report to file
59
- report_filename = f"report_{int(time.time())}.html"
60
- reports_dir = os.path.join(self.workspace_dir, "reports")
61
- os.makedirs(reports_dir, exist_ok=True)
62
- report_path = os.path.join(reports_dir, report_filename)
63
-
64
- with open(report_path, 'w', encoding='utf-8') as f:
65
- f.write(html_content)
66
-
67
- logger.info(f"✅ Report generated successfully: {report_path}")
68
- return report_path
127
+ # Extract task and information
128
+ report_task = report_data.get('report_task', 'Generate a comprehensive report')
129
+ report_information = report_data.get('report_information', 'No additional information provided')
130
+
131
+ # Create report file with timestamp
132
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
133
+ report_filename = f"reports/report-{timestamp}.html"
134
+
135
+ # Create the report file
136
+ create_result = await self.file_system.create_file(report_filename)
137
+ logger.info(f"Created report file: {create_result}")
138
+
139
+ max_iterations = 6 # Prevent infinite loops
140
+
141
+ # Add system message with unified prompt only if message history is empty
142
+ if not self.message_history:
143
+ self.message_history.append(SystemMessage(content=REPORT_WRITER_PROMPT))
144
+
145
+ # Add initial user message with task details
146
+ user_message = f"""Please generate a report within MAX {max_iterations} steps based on the following:
147
+
148
+ **Report Task:**
149
+ {report_task}
150
+
151
+ **Available Information:**
152
+ {json.dumps(report_information, indent=2, ensure_ascii=False)}
153
+
154
+ **Report File:**
155
+ {report_filename}
156
+
157
+ The report file '{report_filename}' has been created and is ready for you to write content.
158
+ Please analyze the task, determine if you need to read any additional files, then generate the complete report content and format it as professional HTML.
159
+ """
160
+ self.message_history.append(UserMessage(content=user_message))
161
+
162
+ # LLM-controlled loop
163
+ iteration = 0
164
+ agent_run_error = None
165
+ task_completed = False
166
+
167
+ while iteration < max_iterations:
168
+ # Use the consolidated pause state management
169
+ if self.paused:
170
+ logger.info(f'⏸️ Step {iteration}: Agent paused, waiting to resume...')
171
+ await self._external_pause_event.wait()
172
+ signal_handler.reset()
173
+
174
+ # Check control flags before each step
175
+ if self.stopped:
176
+ logger.info('🛑 Agent stopped')
177
+ agent_run_error = 'Agent stopped programmatically because user interrupted.'
178
+ break
179
+ iteration += 1
180
+ logger.info(f"🔄 LLM iteration {iteration}")
181
+ self.message_history.append(UserMessage(content=f"Current step: {iteration} / {max_iterations}"))
182
+ # Get LLM response
183
+ response = await self.llm.ainvoke(self.message_history, output_format=self.AgentOutput)
184
+ parsed = response.completion
185
+ actions = parsed.action
186
+
187
+ # Call step callback if provided to log thinking + action
188
+ if self.step_callback:
189
+ await self.step_callback(parsed, iteration)
190
+
191
+ # Add assistant message to history
192
+ self.message_history.append(AssistantMessage(
193
+ content=json.dumps(response.completion.model_dump(exclude_none=True, exclude_unset=True),
194
+ ensure_ascii=False)))
195
+
196
+ # Execute actions
197
+ results = []
198
+ time_start = time.time()
199
+
200
+ for i, action in enumerate(actions):
201
+ action_data = action.model_dump(exclude_unset=True)
202
+ action_name = next(iter(action_data.keys())) if action_data else 'unknown'
203
+ logger.info(f"🛠️ Executing action {i + 1}/{len(actions)}: {action_name}")
204
+
205
+ result = await self.tools.act(
206
+ action=action,
207
+ file_system=self.file_system,
208
+ llm=self.llm,
209
+ )
210
+
211
+ time_end = time.time()
212
+ time_elapsed = time_end - time_start
213
+ results.append(result)
214
+
215
+ logger.info(f"✅ Action completed in {time_elapsed:.2f}s")
216
+
217
+ # Check if task is done
218
+ if action_name == 'task_done':
219
+ logger.info("🎉 Report Writing Task completed")
220
+ task_completed = True
221
+ break
222
+
223
+ # Check if task is done - break out of main loop if task completed
224
+ if task_completed:
225
+ break
226
+
227
+ # Add results to message history using improved action result processing
228
+ action_results = ''
229
+ for idx, action_result in enumerate(results):
230
+ if hasattr(action_result, 'extracted_content') and action_result.extracted_content:
231
+ action_results += f'{action_result.extracted_content}\n'
232
+ logger.debug(f'Added extracted_content to action_results: {action_result.extracted_content}')
233
+
234
+ if hasattr(action_result, 'error') and action_result.error:
235
+ if len(action_result.error) > 200:
236
+ error_text = action_result.error[:100] + '......' + action_result.error[-100:]
237
+ else:
238
+ error_text = action_result.error
239
+ action_results += f'{error_text}\n'
240
+ logger.debug(f'Added error to action_results: {error_text}')
241
+
242
+ if action_results:
243
+ formatted_results = f'Result:\n{action_results}'
244
+ self.message_history.append(UserMessage(content=formatted_results))
245
+
246
+ # If no progress, add a prompt to continue
247
+ if not results:
248
+ self.message_history.append(UserMessage(content="Please continue with the report generation."))
249
+
250
+ # Handle different completion scenarios
251
+ report_path = await self._finalize_report(report_filename)
69
252
 
253
+ if agent_run_error:
254
+ # Agent was stopped
255
+ return ReportTaskResult(
256
+ success=False,
257
+ msg=agent_run_error,
258
+ report_path=report_path
259
+ )
260
+ elif task_completed:
261
+ # Task completed successfully by LLM
262
+ logger.info(f"✅ Report generated successfully: {report_path}")
263
+ return ReportTaskResult(
264
+ success=True,
265
+ msg="Report generated successfully by LLM",
266
+ report_path=report_path
267
+ )
268
+ elif iteration >= max_iterations:
269
+ # Maximum iterations reached
270
+ logger.warning("⚠️ Maximum iterations reached, finishing report generation")
271
+ return ReportTaskResult(
272
+ success=False,
273
+ msg="Maximum iterations reached without task completion",
274
+ report_path=report_path
275
+ )
276
+ else:
277
+ # Unexpected exit from loop
278
+ return ReportTaskResult(
279
+ success=False,
280
+ msg="Report generation ended unexpectedly",
281
+ report_path=report_path
282
+ )
283
+
70
284
  except Exception as e:
71
285
  logger.error(f"❌ Failed to generate report: {e}")
72
286
  # Generate a simple fallback report
73
287
  fallback_path = await self._generate_fallback_report(report_data)
74
- return fallback_path
75
-
76
- async def _generate_content(self, report_data: Dict[str, Any]) -> str:
77
- """Generate the textual content for the report"""
78
- # Format execution results for the prompt
79
- results_text = self._format_execution_results(report_data.get('execution_results', []))
80
-
81
- # Format upload files
82
- upload_files = report_data.get('upload_files', [])
83
- upload_files_text = "None" if not upload_files else ", ".join(upload_files)
84
-
85
- # Generate content using the content prompt
86
- content_prompt = REPORT_CONTENT_PROMPT.format(
87
- original_task=report_data.get('original_task', 'No task specified'),
88
- report_type=report_data.get('report_type', 'summary'),
89
- upload_files=upload_files_text,
90
- execution_results=results_text
91
- )
92
-
93
- response = await self.llm.ainvoke([UserMessage(content=content_prompt)])
94
- logger.debug(f"Content generation response type: {type(response)}")
95
- logger.debug(f"Content generation completion: {response.completion}")
96
- logger.debug(f"Content generation completion type: {type(response.completion)}")
97
-
98
- if response.completion is None:
99
- logger.error("❌ Content generation returned None completion")
100
- raise ValueError("LLM response completion is None - unable to generate report content")
101
-
102
- return response.completion
103
-
104
- async def _format_as_html(self, content: str) -> str:
105
- """Format the content as a professional HTML document"""
106
- format_prompt = REPORT_FORMAT_PROMPT.format(report_content=content)
107
-
108
- response = await self.llm.ainvoke([UserMessage(content=format_prompt)])
109
- logger.debug(f"Format generation response type: {type(response)}")
110
- logger.debug(f"Format generation completion: {response.completion}")
111
- logger.debug(f"Format generation completion type: {type(response.completion)}")
112
-
113
- if response.completion is None:
114
- logger.error("❌ Format generation returned None completion")
115
- raise ValueError("LLM response completion is None - unable to format report as HTML")
116
-
117
- html_content = response.completion
118
-
119
- # Clean up the HTML content if needed
120
- html_content = self._clean_html_content(html_content)
121
-
122
- return html_content
123
-
124
- def _format_execution_results(self, execution_results) -> str:
125
- """Format execution results for the LLM prompt"""
126
- if not execution_results:
127
- return "No execution results available."
288
+ return ReportTaskResult(
289
+ success=False,
290
+ msg=f"Error occurred during report generation: {str(e)}",
291
+ report_path=fallback_path
292
+ )
293
+ finally:
294
+ signal_handler.unregister()
295
+ self.stopped = False
296
+ self.paused = False
297
+
298
+ async def _finalize_report(self, report_filename: str) -> str:
299
+ """
300
+ Finalize the report by cleaning HTML and converting links
128
301
 
129
- formatted_results = []
130
- for i, result in enumerate(execution_results, 1):
131
- status = "✅ Success" if result.success else "❌ Failed"
302
+ Args:
303
+ report_filename: Name of the report file
132
304
 
133
- # Extract meaningful result content
134
- result_content = "No result available"
135
- if result.result:
136
- # Truncate very long results but keep meaningful content
137
- if len(result.result) > 500:
138
- result_content = result.result[:497] + "..."
305
+ Returns:
306
+ str: Absolute path to the finalized report
307
+ """
308
+ try:
309
+ # Read the current content
310
+ content = await self.file_system.read_file(report_filename)
311
+
312
+ # Extract HTML content from the read result
313
+ if content.startswith('Read from file'):
314
+ # Extract content between <content> tags
315
+ start_tag = '<content>'
316
+ end_tag = '</content>'
317
+ start_idx = content.find(start_tag)
318
+ end_idx = content.find(end_tag)
319
+
320
+ if start_idx != -1 and end_idx != -1:
321
+ html_content = content[start_idx + len(start_tag):end_idx].strip()
139
322
  else:
140
- result_content = result.result
141
- elif result.error:
142
- result_content = f"Error: {result.error}"
143
-
144
- formatted_results.append(f"""
145
- **Task {i}:** {result.task}
146
- **Status:** {status}
147
- **Agent:** {result.agent_id}
148
- **Result:** {result_content}
149
- **Success:** {'Yes' if result.success else 'No'}
150
- """)
151
-
152
- return "\n".join(formatted_results)
153
-
323
+ html_content = content
324
+ else:
325
+ html_content = content
326
+
327
+ # Clean HTML content
328
+ cleaned_html = self._clean_html_content(html_content)
329
+
330
+ # Convert relative file paths to absolute file:// URLs
331
+ final_html = self._convert_file_links(cleaned_html)
332
+
333
+ # Write the final content
334
+ await self.file_system.write_file(report_filename, final_html)
335
+
336
+ # Get absolute path
337
+ # absolute_path = self.file_system.get_absolute_path(report_filename)
338
+
339
+ return report_filename
340
+
341
+ except Exception as e:
342
+ logger.error(f"❌ Failed to finalize report: {e}")
343
+ # Return the path anyway
344
+ return self.file_system.get_absolute_path(report_filename)
345
+
154
346
  def _clean_html_content(self, html_content: str) -> str:
155
347
  """Clean and validate HTML content"""
156
348
  # Remove markdown code block markers if present
@@ -161,7 +353,7 @@ class ReportWriterAgent:
161
353
  html_content = html_content[3:].strip()
162
354
  if html_content.endswith("```"):
163
355
  html_content = html_content[:-3].strip()
164
-
356
+
165
357
  # Ensure it starts with <!DOCTYPE html> or <html>
166
358
  if not html_content.lower().startswith(('<!doctype', '<html')):
167
359
  html_content = f"""<!DOCTYPE html>
@@ -169,13 +361,16 @@ class ReportWriterAgent:
169
361
  <head>
170
362
  <meta charset="UTF-8">
171
363
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
172
- <title>VibeSurf Task Report</title>
364
+ <title>VibeSurf Report</title>
173
365
  <style>
174
- body {{ font-family: Arial, sans-serif; margin: 20px; line-height: 1.6; }}
175
- .container {{ max-width: 800px; margin: 0 auto; }}
366
+ body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; line-height: 1.6; color: #333; }}
367
+ .container {{ max-width: 1200px; margin: 0 auto; padding: 20px; }}
176
368
  h1 {{ color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }}
177
369
  h2 {{ color: #34495e; margin-top: 30px; }}
178
370
  .section {{ margin: 20px 0; padding: 15px; background: #f8f9fa; border-left: 4px solid #007bff; }}
371
+ table {{ width: 100%; border-collapse: collapse; margin: 20px 0; }}
372
+ th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }}
373
+ th {{ background-color: #f2f2f2; font-weight: bold; }}
179
374
  </style>
180
375
  </head>
181
376
  <body>
@@ -184,24 +379,57 @@ class ReportWriterAgent:
184
379
  </div>
185
380
  </body>
186
381
  </html>"""
382
+
383
+ return html_content
384
+
385
+ def _convert_file_links(self, html_content: str) -> str:
386
+ """
387
+ Convert relative file paths to absolute file:// URLs
187
388
 
389
+ Args:
390
+ html_content: HTML content with relative file paths
391
+
392
+ Returns:
393
+ str: HTML content with converted file:// URLs
394
+ """
395
+ # Pattern to match HTML href and src attributes with relative paths
396
+ patterns = [
397
+ (r'href\s*=\s*["\']([^"\']+)["\']', 'href'), # <a href="path">
398
+ (r'src\s*=\s*["\']([^"\']+)["\']', 'src'), # <img src="path">
399
+ ]
400
+
401
+ for pattern, attr_name in patterns:
402
+ def replace_path(match):
403
+ full_match = match.group(0)
404
+ file_path = match.group(1)
405
+
406
+ # Check if it's already a URL or absolute path
407
+ if file_path.startswith(('http://', 'https://', 'file://', '#', 'mailto:', 'tel:')):
408
+ return full_match # Return unchanged
409
+
410
+ # Convert to absolute path
411
+ if not os.path.isabs(file_path):
412
+ absolute_path = os.path.abspath(os.path.join(self.workspace_dir, file_path))
413
+ else:
414
+ absolute_path = file_path
415
+ normalized_path = absolute_path.replace(os.path.sep, '/')
416
+ file_url = f"file:///{normalized_path}"
417
+
418
+ # Return the updated attribute
419
+ quote = '"' if '"' in full_match else "'"
420
+ return f'{attr_name}={quote}{file_url}{quote}'
421
+
422
+ html_content = re.sub(pattern, replace_path, html_content)
423
+
188
424
  return html_content
189
-
425
+
190
426
  async def _generate_fallback_report(self, report_data: Dict[str, Any]) -> str:
191
427
  """Generate a simple fallback report when LLM generation fails"""
192
428
  logger.info("📝 Generating fallback report...")
193
-
194
- upload_files = report_data.get('upload_files', [])
195
- upload_files_section = ""
196
- if upload_files:
197
- upload_files_section = f"""
198
- <div class="section">
199
- <h2>Upload Files</h2>
200
- <ul>
201
- {"".join([f"<li>{file}</li>" for file in upload_files])}
202
- </ul>
203
- </div>"""
204
-
429
+
430
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
431
+ report_filename = f"vibesurf_fallback_report-{timestamp}.html"
432
+
205
433
  # Create a simple HTML report
206
434
  html_content = f"""<!DOCTYPE html>
207
435
  <html lang="en">
@@ -252,36 +480,6 @@ class ReportWriterAgent:
252
480
  border-left: 4px solid #3498db;
253
481
  padding-left: 15px;
254
482
  }}
255
- .success {{
256
- color: #27ae60;
257
- font-weight: 600;
258
- }}
259
- .error {{
260
- color: #e74c3c;
261
- font-weight: 600;
262
- }}
263
- table {{
264
- width: 100%;
265
- border-collapse: collapse;
266
- margin-top: 15px;
267
- background: white;
268
- border-radius: 6px;
269
- overflow: hidden;
270
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
271
- }}
272
- th, td {{
273
- padding: 15px;
274
- text-align: left;
275
- border-bottom: 1px solid #eee;
276
- }}
277
- th {{
278
- background: #34495e;
279
- color: white;
280
- font-weight: 600;
281
- }}
282
- tr:hover {{
283
- background-color: #f8f9fa;
284
- }}
285
483
  .meta {{
286
484
  background: #ecf0f1;
287
485
  color: #7f8c8d;
@@ -295,82 +493,38 @@ class ReportWriterAgent:
295
493
  <div class="container">
296
494
  <div class="header">
297
495
  <h1>VibeSurf Task Report</h1>
298
- <p>Generated on {time.strftime('%B %d, %Y at %H:%M:%S')}</p>
496
+ <p>Generated on {datetime.now().strftime('%B %d, %Y at %H:%M:%S')}</p>
299
497
  </div>
300
498
 
301
499
  <div class="section">
302
- <h2>Task Overview</h2>
303
- <p><strong>Original Task:</strong> {report_data.get('original_task', 'No task specified')}</p>
304
- <p><strong>Report Type:</strong> {report_data.get('report_type', 'summary').title()}</p>
500
+ <h2>Report Task</h2>
501
+ <p>{report_data.get('report_task', 'No task specified')}</p>
305
502
  </div>
306
- {upload_files_section}
307
503
 
308
504
  <div class="section">
309
- <h2>Execution Results</h2>
310
- <table>
311
- <thead>
312
- <tr>
313
- <th>Task</th>
314
- <th>Status</th>
315
- <th>Agent</th>
316
- <th>Result</th>
317
- </tr>
318
- </thead>
319
- <tbody>
320
- """
321
-
322
- # Add execution results to table
323
- execution_results = report_data.get('execution_results', [])
324
- if execution_results:
325
- for result in execution_results:
326
- status_class = "success" if result.success else "error"
327
- status_text = "✅ Success" if result.success else "❌ Failed"
328
- result_text = result.result or result.error or "No result"
329
- # Truncate long results
330
- if len(result_text) > 150:
331
- result_text = result_text[:147] + "..."
332
-
333
- html_content += f"""
334
- <tr>
335
- <td>{result.task}</td>
336
- <td class="{status_class}">{status_text}</td>
337
- <td>{result.agent_id}</td>
338
- <td>{result_text}</td>
339
- </tr>
340
- """
341
- else:
342
- html_content += """
343
- <tr>
344
- <td colspan="4" style="text-align: center; color: #7f8c8d; font-style: italic;">No execution results available</td>
345
- </tr>
346
- """
347
-
348
- html_content += """
349
- </tbody>
350
- </table>
505
+ <h2>Available Information</h2>
506
+ <p>{report_data.get('information', 'No information provided')}</p>
351
507
  </div>
352
508
 
353
509
  <div class="section">
354
- <h2>Summary</h2>
355
- <p>This report was automatically generated by VibeSurf as a fallback when the advanced report generation encountered an issue. The report contains basic information about the task execution and results.</p>
510
+ <h2>Notice</h2>
511
+ <p>This is a fallback report generated when the advanced LLM-controlled report generation encountered an issue. The report contains basic information provided for the task.</p>
356
512
  <p>For future runs, ensure that the LLM service is properly configured and accessible for enhanced report generation capabilities.</p>
357
513
  </div>
358
514
 
359
515
  <div class="meta">
360
- Generated by VibeSurf Agent Framework
516
+ Generated by VibeSurf Agent Framework - Fallback Mode
361
517
  </div>
362
518
  </div>
363
519
  </body>
364
520
  </html>"""
365
-
366
- # Save fallback report
367
- report_filename = f"fallback_report_{int(time.time())}.html"
368
- reports_dir = os.path.join(self.workspace_dir, "reports")
369
- os.makedirs(reports_dir, exist_ok=True)
370
- report_path = os.path.join(reports_dir, report_filename)
371
-
372
- with open(report_path, 'w', encoding='utf-8') as f:
373
- f.write(html_content)
374
-
375
- logger.info(f"✅ Fallback report generated: {report_path}")
376
- return report_path
521
+
522
+ # Create and write fallback report
523
+ await self.file_system.create_file(report_filename)
524
+ await self.file_system.write_file(report_filename, html_content)
525
+
526
+ # Get absolute path
527
+ # absolute_path = self.file_system.get_absolute_path(report_filename)
528
+
529
+ logger.info(f"✅ Fallback report generated: {report_filename}")
530
+ return report_filename