vibesurf 0.1.10__py3-none-any.whl → 0.1.12__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.
- vibe_surf/_version.py +2 -2
- vibe_surf/agents/browser_use_agent.py +68 -45
- vibe_surf/agents/prompts/report_writer_prompt.py +73 -0
- vibe_surf/agents/prompts/vibe_surf_prompt.py +85 -172
- vibe_surf/agents/report_writer_agent.py +380 -226
- vibe_surf/agents/vibe_surf_agent.py +880 -825
- vibe_surf/agents/views.py +130 -0
- vibe_surf/backend/api/activity.py +3 -1
- vibe_surf/backend/api/browser.py +9 -5
- vibe_surf/backend/api/config.py +8 -5
- vibe_surf/backend/api/files.py +59 -50
- vibe_surf/backend/api/models.py +2 -2
- vibe_surf/backend/api/task.py +46 -13
- vibe_surf/backend/database/manager.py +24 -18
- vibe_surf/backend/database/queries.py +199 -192
- vibe_surf/backend/database/schemas.py +1 -1
- vibe_surf/backend/main.py +4 -2
- vibe_surf/backend/shared_state.py +28 -35
- vibe_surf/backend/utils/encryption.py +3 -1
- vibe_surf/backend/utils/llm_factory.py +41 -36
- vibe_surf/browser/agent_browser_session.py +0 -4
- vibe_surf/browser/browser_manager.py +14 -8
- vibe_surf/browser/utils.py +5 -3
- vibe_surf/browser/watchdogs/dom_watchdog.py +0 -45
- vibe_surf/chrome_extension/background.js +4 -0
- vibe_surf/chrome_extension/scripts/api-client.js +13 -0
- vibe_surf/chrome_extension/scripts/file-manager.js +27 -71
- vibe_surf/chrome_extension/scripts/session-manager.js +21 -3
- vibe_surf/chrome_extension/scripts/ui-manager.js +831 -48
- vibe_surf/chrome_extension/sidepanel.html +21 -4
- vibe_surf/chrome_extension/styles/activity.css +365 -5
- vibe_surf/chrome_extension/styles/input.css +139 -0
- vibe_surf/cli.py +5 -22
- vibe_surf/common.py +35 -0
- vibe_surf/llm/openai_compatible.py +217 -99
- vibe_surf/logger.py +99 -0
- vibe_surf/{controller/vibesurf_tools.py → tools/browser_use_tools.py} +233 -219
- vibe_surf/tools/file_system.py +437 -0
- vibe_surf/{controller → tools}/mcp_client.py +4 -3
- vibe_surf/tools/report_writer_tools.py +21 -0
- vibe_surf/tools/vibesurf_tools.py +657 -0
- vibe_surf/tools/views.py +120 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.dist-info}/METADATA +6 -2
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.dist-info}/RECORD +49 -43
- vibe_surf/controller/file_system.py +0 -53
- vibe_surf/controller/views.py +0 -37
- /vibe_surf/{controller → tools}/__init__.py +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.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.
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
40
|
-
-
|
|
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
|
-
|
|
111
|
+
ReportTaskResult: Result containing success status, message, and report path
|
|
46
112
|
"""
|
|
47
|
-
logger.info(
|
|
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
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
with
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
status = "✅ Success" if result.success else "❌ Failed"
|
|
302
|
+
Args:
|
|
303
|
+
report_filename: Name of the report file
|
|
132
304
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
364
|
+
<title>VibeSurf Report</title>
|
|
173
365
|
<style>
|
|
174
|
-
body {{ font-family:
|
|
175
|
-
.container {{ max-width:
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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 {
|
|
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
|
|
303
|
-
<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>
|
|
310
|
-
<
|
|
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>
|
|
355
|
-
<p>This
|
|
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
|
-
#
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|