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
|
@@ -5,10 +5,11 @@ import json
|
|
|
5
5
|
import enum
|
|
6
6
|
import base64
|
|
7
7
|
import mimetypes
|
|
8
|
-
|
|
8
|
+
import datetime
|
|
9
|
+
from pathvalidate import sanitize_filename
|
|
9
10
|
from typing import Optional, Type, Callable, Dict, Any, Union, Awaitable, TypeVar
|
|
10
11
|
from pydantic import BaseModel
|
|
11
|
-
from browser_use.tools.service import
|
|
12
|
+
from browser_use.tools.service import Tools
|
|
12
13
|
import logging
|
|
13
14
|
from browser_use.agent.views import ActionModel, ActionResult
|
|
14
15
|
from browser_use.utils import time_execution_sync
|
|
@@ -38,34 +39,227 @@ from browser_use.dom.service import EnhancedDOMTreeNode
|
|
|
38
39
|
from browser_use.browser.views import BrowserError
|
|
39
40
|
from browser_use.mcp.client import MCPClient
|
|
40
41
|
|
|
41
|
-
|
|
42
42
|
from vibe_surf.browser.agent_browser_session import AgentBrowserSession
|
|
43
|
-
from vibe_surf.
|
|
44
|
-
from vibe_surf.
|
|
43
|
+
from vibe_surf.tools.views import HoverAction, ExtractionAction, FileExtractionAction
|
|
44
|
+
from vibe_surf.tools.mcp_client import CustomMCPClient
|
|
45
|
+
from vibe_surf.tools.file_system import CustomFileSystem
|
|
46
|
+
from vibe_surf.logger import get_logger
|
|
47
|
+
from vibe_surf.tools.vibesurf_tools import VibeSurfTools
|
|
45
48
|
|
|
46
|
-
logger =
|
|
49
|
+
logger = get_logger(__name__)
|
|
47
50
|
|
|
48
51
|
Context = TypeVar('Context')
|
|
49
52
|
|
|
50
53
|
T = TypeVar('T', bound=BaseModel)
|
|
51
54
|
|
|
52
55
|
|
|
53
|
-
class
|
|
56
|
+
class BrowserUseTools(Tools, VibeSurfTools):
|
|
54
57
|
def __init__(self,
|
|
55
58
|
exclude_actions: list[str] = [],
|
|
56
59
|
output_model: type[T] | None = None,
|
|
57
60
|
display_files_in_done_text: bool = True,
|
|
58
|
-
mcp_server_config: Optional[Dict[str, Any]] = None
|
|
59
61
|
):
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
Tools.__init__(self, exclude_actions=exclude_actions, output_model=output_model,
|
|
63
|
+
display_files_in_done_text=display_files_in_done_text)
|
|
62
64
|
self._register_browser_actions()
|
|
63
|
-
self.
|
|
64
|
-
|
|
65
|
+
self._register_file_actions()
|
|
66
|
+
|
|
67
|
+
def _register_done_action(self, output_model: type[T] | None, display_files_in_done_text: bool = True):
|
|
68
|
+
if output_model is not None:
|
|
69
|
+
self.display_files_in_done_text = display_files_in_done_text
|
|
70
|
+
|
|
71
|
+
@self.registry.action(
|
|
72
|
+
'Complete task - with return text and if the task is finished (success=True) or not yet completely finished (success=False), because last step is reached',
|
|
73
|
+
param_model=StructuredOutputAction[output_model],
|
|
74
|
+
)
|
|
75
|
+
async def done(params: StructuredOutputAction):
|
|
76
|
+
# Exclude success from the output JSON since it's an internal parameter
|
|
77
|
+
output_dict = params.data.model_dump()
|
|
78
|
+
|
|
79
|
+
# Enums are not serializable, convert to string
|
|
80
|
+
for key, value in output_dict.items():
|
|
81
|
+
if isinstance(value, enum.Enum):
|
|
82
|
+
output_dict[key] = value.value
|
|
83
|
+
|
|
84
|
+
return ActionResult(
|
|
85
|
+
is_done=True,
|
|
86
|
+
success=params.success,
|
|
87
|
+
extracted_content=json.dumps(output_dict),
|
|
88
|
+
long_term_memory=f'Task completed. Success Status: {params.success}',
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
else:
|
|
92
|
+
|
|
93
|
+
@self.registry.action(
|
|
94
|
+
'Complete task - provide a summary of results for the user. Set success=True if task completed successfully, false otherwise. Text should be your response to the user summarizing results. Include files in files_to_display if you would like to display to the user or there files are important for the task result.',
|
|
95
|
+
param_model=DoneAction,
|
|
96
|
+
)
|
|
97
|
+
async def done(params: DoneAction, file_system: CustomFileSystem):
|
|
98
|
+
user_message = params.text
|
|
99
|
+
|
|
100
|
+
len_text = len(params.text)
|
|
101
|
+
len_max_memory = 100
|
|
102
|
+
memory = f'Task completed: {params.success} - {params.text[:len_max_memory]}'
|
|
103
|
+
if len_text > len_max_memory:
|
|
104
|
+
memory += f' - {len_text - len_max_memory} more characters'
|
|
105
|
+
|
|
106
|
+
attachments = []
|
|
107
|
+
if params.files_to_display:
|
|
108
|
+
if self.display_files_in_done_text:
|
|
109
|
+
file_msg = ''
|
|
110
|
+
for file_name in params.files_to_display:
|
|
111
|
+
if file_name == 'todo.md':
|
|
112
|
+
continue
|
|
113
|
+
file_content = await file_system.display_file(file_name)
|
|
114
|
+
if file_content:
|
|
115
|
+
file_msg += f'\n\n{file_name}:\n{file_content}'
|
|
116
|
+
attachments.append(file_name)
|
|
117
|
+
if file_msg:
|
|
118
|
+
user_message += '\n\nAttachments:'
|
|
119
|
+
user_message += file_msg
|
|
120
|
+
else:
|
|
121
|
+
logger.warning('Agent wanted to display files but none were found')
|
|
122
|
+
else:
|
|
123
|
+
for file_name in params.files_to_display:
|
|
124
|
+
if file_name == 'todo.md':
|
|
125
|
+
continue
|
|
126
|
+
file_content = await file_system.display_file(file_name)
|
|
127
|
+
if file_content:
|
|
128
|
+
attachments.append(file_name)
|
|
129
|
+
|
|
130
|
+
attachments = [file_name for file_name in attachments]
|
|
131
|
+
|
|
132
|
+
return ActionResult(
|
|
133
|
+
is_done=True,
|
|
134
|
+
success=params.success,
|
|
135
|
+
extracted_content=user_message,
|
|
136
|
+
long_term_memory=memory,
|
|
137
|
+
attachments=attachments,
|
|
138
|
+
)
|
|
65
139
|
|
|
66
140
|
def _register_browser_actions(self):
|
|
67
141
|
"""Register custom browser actions"""
|
|
68
142
|
|
|
143
|
+
@self.registry.action('Upload file to interactive element with file path', param_model=UploadFileAction)
|
|
144
|
+
async def upload_file_to_element(
|
|
145
|
+
params: UploadFileAction, browser_session: BrowserSession, file_system: FileSystem
|
|
146
|
+
):
|
|
147
|
+
|
|
148
|
+
# For local browsers, ensure the file exists on the local filesystem
|
|
149
|
+
full_file_path = params.path
|
|
150
|
+
if not os.path.exists(full_file_path):
|
|
151
|
+
full_file_path = str(file_system.get_dir() / params.path)
|
|
152
|
+
if not os.path.exists(full_file_path):
|
|
153
|
+
msg = f'File {params.path} does not exist'
|
|
154
|
+
return ActionResult(error=msg)
|
|
155
|
+
|
|
156
|
+
# Get the selector map to find the node
|
|
157
|
+
selector_map = await browser_session.get_selector_map()
|
|
158
|
+
if params.index not in selector_map:
|
|
159
|
+
msg = f'Element with index {params.index} does not exist.'
|
|
160
|
+
return ActionResult(error=msg)
|
|
161
|
+
|
|
162
|
+
node = selector_map[params.index]
|
|
163
|
+
|
|
164
|
+
# Helper function to find file input near the selected element
|
|
165
|
+
def find_file_input_near_element(
|
|
166
|
+
node: EnhancedDOMTreeNode, max_height: int = 3, max_descendant_depth: int = 3
|
|
167
|
+
) -> EnhancedDOMTreeNode | None:
|
|
168
|
+
"""Find the closest file input to the selected element."""
|
|
169
|
+
|
|
170
|
+
def find_file_input_in_descendants(n: EnhancedDOMTreeNode, depth: int) -> EnhancedDOMTreeNode | None:
|
|
171
|
+
if depth < 0:
|
|
172
|
+
return None
|
|
173
|
+
if browser_session.is_file_input(n):
|
|
174
|
+
return n
|
|
175
|
+
for child in n.children_nodes or []:
|
|
176
|
+
result = find_file_input_in_descendants(child, depth - 1)
|
|
177
|
+
if result:
|
|
178
|
+
return result
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
current = node
|
|
182
|
+
for _ in range(max_height + 1):
|
|
183
|
+
# Check the current node itself
|
|
184
|
+
if browser_session.is_file_input(current):
|
|
185
|
+
return current
|
|
186
|
+
# Check all descendants of the current node
|
|
187
|
+
result = find_file_input_in_descendants(current, max_descendant_depth)
|
|
188
|
+
if result:
|
|
189
|
+
return result
|
|
190
|
+
# Check all siblings and their descendants
|
|
191
|
+
if current.parent_node:
|
|
192
|
+
for sibling in current.parent_node.children_nodes or []:
|
|
193
|
+
if sibling is current:
|
|
194
|
+
continue
|
|
195
|
+
if browser_session.is_file_input(sibling):
|
|
196
|
+
return sibling
|
|
197
|
+
result = find_file_input_in_descendants(sibling, max_descendant_depth)
|
|
198
|
+
if result:
|
|
199
|
+
return result
|
|
200
|
+
current = current.parent_node
|
|
201
|
+
if not current:
|
|
202
|
+
break
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
# Try to find a file input element near the selected element
|
|
206
|
+
file_input_node = find_file_input_near_element(node)
|
|
207
|
+
|
|
208
|
+
# If not found near the selected element, fallback to finding the closest file input to current scroll position
|
|
209
|
+
if file_input_node is None:
|
|
210
|
+
logger.info(
|
|
211
|
+
f'No file upload element found near index {params.index}, searching for closest file input to scroll position'
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Get current scroll position
|
|
215
|
+
cdp_session = await browser_session.get_or_create_cdp_session()
|
|
216
|
+
try:
|
|
217
|
+
scroll_info = await cdp_session.cdp_client.send.Runtime.evaluate(
|
|
218
|
+
params={'expression': 'window.scrollY || window.pageYOffset || 0'},
|
|
219
|
+
session_id=cdp_session.session_id
|
|
220
|
+
)
|
|
221
|
+
current_scroll_y = scroll_info.get('result', {}).get('value', 0)
|
|
222
|
+
except Exception:
|
|
223
|
+
current_scroll_y = 0
|
|
224
|
+
|
|
225
|
+
# Find all file inputs in the selector map and pick the closest one to scroll position
|
|
226
|
+
closest_file_input = None
|
|
227
|
+
min_distance = float('inf')
|
|
228
|
+
|
|
229
|
+
for idx, element in selector_map.items():
|
|
230
|
+
if browser_session.is_file_input(element):
|
|
231
|
+
# Get element's Y position
|
|
232
|
+
if element.absolute_position:
|
|
233
|
+
element_y = element.absolute_position.y
|
|
234
|
+
distance = abs(element_y - current_scroll_y)
|
|
235
|
+
if distance < min_distance:
|
|
236
|
+
min_distance = distance
|
|
237
|
+
closest_file_input = element
|
|
238
|
+
|
|
239
|
+
if closest_file_input:
|
|
240
|
+
file_input_node = closest_file_input
|
|
241
|
+
logger.info(f'Found file input closest to scroll position (distance: {min_distance}px)')
|
|
242
|
+
else:
|
|
243
|
+
msg = 'No file upload element found on the page'
|
|
244
|
+
logger.error(msg)
|
|
245
|
+
raise BrowserError(msg)
|
|
246
|
+
# TODO: figure out why this fails sometimes + add fallback hail mary, just look for any file input on page
|
|
247
|
+
|
|
248
|
+
# Dispatch upload file event with the file input node
|
|
249
|
+
try:
|
|
250
|
+
event = browser_session.event_bus.dispatch(UploadFileEvent(node=file_input_node, file_path=full_file_path))
|
|
251
|
+
await event
|
|
252
|
+
await event.event_result(raise_if_any=True, raise_if_none=False)
|
|
253
|
+
msg = f'Successfully uploaded file to index {params.index}'
|
|
254
|
+
logger.info(f'📁 {msg}')
|
|
255
|
+
return ActionResult(
|
|
256
|
+
extracted_content=msg,
|
|
257
|
+
long_term_memory=f'Uploaded file {params.path} to element {params.index}',
|
|
258
|
+
)
|
|
259
|
+
except Exception as e:
|
|
260
|
+
logger.error(f'Failed to upload file: {e}')
|
|
261
|
+
raise BrowserError(f'Failed to upload file: {e}')
|
|
262
|
+
|
|
69
263
|
@self.registry.action(
|
|
70
264
|
'Hover over an element',
|
|
71
265
|
param_model=HoverAction,
|
|
@@ -358,222 +552,42 @@ Provide the extracted information in a clear, structured format."""
|
|
|
358
552
|
logger.debug(f'Error extracting content: {e}')
|
|
359
553
|
raise RuntimeError(str(e))
|
|
360
554
|
|
|
361
|
-
@self.registry.action('Read file_name from file system. If this is a file not in Current workspace dir or with a absolute path, Set external_file=True.')
|
|
362
|
-
async def read_file(file_name: str, external_file: bool, file_system: FileSystem):
|
|
363
|
-
if not os.path.exists(file_name):
|
|
364
|
-
# if not exists, assume it is external_file
|
|
365
|
-
external_file = True
|
|
366
|
-
result = await file_system.read_file(file_name, external_file=external_file)
|
|
367
|
-
|
|
368
|
-
MAX_MEMORY_SIZE = 1000
|
|
369
|
-
if len(result) > MAX_MEMORY_SIZE:
|
|
370
|
-
lines = result.splitlines()
|
|
371
|
-
display = ''
|
|
372
|
-
lines_count = 0
|
|
373
|
-
for line in lines:
|
|
374
|
-
if len(display) + len(line) < MAX_MEMORY_SIZE:
|
|
375
|
-
display += line + '\n'
|
|
376
|
-
lines_count += 1
|
|
377
|
-
else:
|
|
378
|
-
break
|
|
379
|
-
remaining_lines = len(lines) - lines_count
|
|
380
|
-
memory = f'{display}{remaining_lines} more lines...' if remaining_lines > 0 else display
|
|
381
|
-
else:
|
|
382
|
-
memory = result
|
|
383
|
-
logger.info(f'💾 {memory}')
|
|
384
|
-
return ActionResult(
|
|
385
|
-
extracted_content=result,
|
|
386
|
-
include_in_memory=True,
|
|
387
|
-
long_term_memory=memory,
|
|
388
|
-
include_extracted_content_only_once=True,
|
|
389
|
-
)
|
|
390
|
-
|
|
391
555
|
@self.registry.action(
|
|
392
|
-
'
|
|
393
|
-
param_model=
|
|
556
|
+
'Take a screenshot of the current page and save it to the file system',
|
|
557
|
+
param_model=NoParamsAction
|
|
394
558
|
)
|
|
395
|
-
async def
|
|
396
|
-
params: FileExtractionAction,
|
|
397
|
-
page_extraction_llm: BaseChatModel,
|
|
398
|
-
file_system: FileSystem,
|
|
399
|
-
):
|
|
559
|
+
async def take_screenshot(_: NoParamsAction, browser_session: AgentBrowserSession, file_system: FileSystem):
|
|
400
560
|
try:
|
|
401
|
-
#
|
|
402
|
-
|
|
561
|
+
# Take screenshot using browser session
|
|
562
|
+
screenshot = await browser_session.take_screenshot()
|
|
403
563
|
|
|
404
|
-
#
|
|
405
|
-
|
|
406
|
-
file_path = os.path.join(file_system.get_dir(), file_path)
|
|
407
|
-
|
|
408
|
-
# Determine if file is an image based on MIME type
|
|
409
|
-
mime_type, _ = mimetypes.guess_type(file_path)
|
|
410
|
-
is_image = mime_type and mime_type.startswith('image/')
|
|
411
|
-
|
|
412
|
-
if is_image:
|
|
413
|
-
# Handle image files with LLM vision
|
|
414
|
-
try:
|
|
415
|
-
# Read image file and encode to base64
|
|
416
|
-
with open(file_path, 'rb') as image_file:
|
|
417
|
-
image_data = image_file.read()
|
|
418
|
-
image_base64 = base64.b64encode(image_data).decode('utf-8')
|
|
419
|
-
|
|
420
|
-
# Create content parts similar to the user's example
|
|
421
|
-
content_parts: list[ContentPartTextParam | ContentPartImageParam] = [
|
|
422
|
-
ContentPartTextParam(text=f"Query: {params.query}")
|
|
423
|
-
]
|
|
424
|
-
|
|
425
|
-
# Add the image
|
|
426
|
-
content_parts.append(
|
|
427
|
-
ContentPartImageParam(
|
|
428
|
-
image_url=ImageURL(
|
|
429
|
-
url=f'data:{mime_type};base64,{image_base64}',
|
|
430
|
-
media_type=mime_type,
|
|
431
|
-
detail='high',
|
|
432
|
-
),
|
|
433
|
-
)
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
# Create user message and invoke LLM
|
|
437
|
-
user_message = UserMessage(content=content_parts, cache=True)
|
|
438
|
-
response = await asyncio.wait_for(
|
|
439
|
-
page_extraction_llm.ainvoke([user_message]),
|
|
440
|
-
timeout=120.0,
|
|
441
|
-
)
|
|
442
|
-
|
|
443
|
-
extracted_content = f'File: {file_path}\nQuery: {params.query}\nExtracted Content:\n{response.completion}'
|
|
444
|
-
|
|
445
|
-
except Exception as e:
|
|
446
|
-
raise Exception(f'Failed to process image file {file_path}: {str(e)}')
|
|
447
|
-
|
|
448
|
-
else:
|
|
449
|
-
# Handle non-image files by reading content
|
|
450
|
-
try:
|
|
451
|
-
file_content = await file_system.read_file(file_path, external_file=True)
|
|
452
|
-
|
|
453
|
-
# Create a simple prompt for text extraction
|
|
454
|
-
prompt = f"""Extract the requested information from this file content.
|
|
564
|
+
# Generate timestamp for filename
|
|
565
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
455
566
|
|
|
456
|
-
|
|
567
|
+
# Get file system directory path (Path type)
|
|
568
|
+
fs_dir = file_system.get_dir()
|
|
457
569
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
570
|
+
# Create screenshots directory if it doesn't exist
|
|
571
|
+
screenshots_dir = fs_dir / "screenshots"
|
|
572
|
+
screenshots_dir.mkdir(exist_ok=True)
|
|
461
573
|
|
|
462
|
-
|
|
574
|
+
# Save screenshot to file system
|
|
575
|
+
page_title = await browser_session.get_current_page_title()
|
|
576
|
+
page_title = sanitize_filename(page_title)
|
|
577
|
+
filename = f"{page_title}-{timestamp}.png"
|
|
578
|
+
filepath = screenshots_dir / filename
|
|
463
579
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
timeout=120.0,
|
|
467
|
-
)
|
|
468
|
-
|
|
469
|
-
extracted_content = f'File: {file_path}\nQuery: {params.query}\nExtracted Content:\n{response.completion}'
|
|
470
|
-
|
|
471
|
-
except Exception as e:
|
|
472
|
-
raise Exception(f'Failed to read file {file_path}: {str(e)}')
|
|
473
|
-
|
|
474
|
-
# Handle memory storage
|
|
475
|
-
if len(extracted_content) < 1000:
|
|
476
|
-
memory = extracted_content
|
|
477
|
-
include_extracted_content_only_once = False
|
|
478
|
-
else:
|
|
479
|
-
save_result = await file_system.save_extracted_content(extracted_content)
|
|
480
|
-
memory = (
|
|
481
|
-
f'Extracted content from file {file_path} for query: {params.query}\nContent saved to file system: {save_result}'
|
|
482
|
-
)
|
|
483
|
-
include_extracted_content_only_once = True
|
|
580
|
+
with open(filepath, "wb") as f:
|
|
581
|
+
f.write(base64.b64decode(screenshot))
|
|
484
582
|
|
|
485
|
-
|
|
583
|
+
msg = f'📸 Screenshot saved to path: {str(filepath.relative_to(fs_dir))}'
|
|
584
|
+
logger.info(msg)
|
|
486
585
|
return ActionResult(
|
|
487
|
-
extracted_content=
|
|
488
|
-
|
|
489
|
-
long_term_memory=
|
|
490
|
-
)
|
|
491
|
-
|
|
492
|
-
except Exception as e:
|
|
493
|
-
logger.debug(f'Error extracting content from file: {e}')
|
|
494
|
-
raise RuntimeError(str(e))
|
|
495
|
-
|
|
496
|
-
async def register_mcp_clients(self, mcp_server_config: Optional[Dict[str, Any]] = None):
|
|
497
|
-
self.mcp_server_config = mcp_server_config or self.mcp_server_config
|
|
498
|
-
if self.mcp_server_config:
|
|
499
|
-
await self.unregister_mcp_clients()
|
|
500
|
-
await self.register_mcp_tools()
|
|
501
|
-
|
|
502
|
-
async def register_mcp_tools(self):
|
|
503
|
-
"""
|
|
504
|
-
Register the MCP tools used by this controller.
|
|
505
|
-
"""
|
|
506
|
-
if not self.mcp_server_config:
|
|
507
|
-
return
|
|
508
|
-
|
|
509
|
-
# Handle both formats: with or without "mcpServers" key
|
|
510
|
-
mcp_servers = self.mcp_server_config.get('mcpServers', self.mcp_server_config)
|
|
511
|
-
|
|
512
|
-
if not mcp_servers:
|
|
513
|
-
return
|
|
514
|
-
|
|
515
|
-
for server_name, server_config in mcp_servers.items():
|
|
516
|
-
try:
|
|
517
|
-
logger.info(f'Connecting to MCP server: {server_name}')
|
|
518
|
-
|
|
519
|
-
# Create MCP client
|
|
520
|
-
client = VibeSurfMCPClient(
|
|
521
|
-
server_name=server_name,
|
|
522
|
-
command=server_config['command'],
|
|
523
|
-
args=server_config['args'],
|
|
524
|
-
env=server_config.get('env', None)
|
|
586
|
+
extracted_content=msg,
|
|
587
|
+
include_in_memory=True,
|
|
588
|
+
long_term_memory=f'Screenshot saved to {str(filepath.relative_to(fs_dir))}',
|
|
525
589
|
)
|
|
526
|
-
|
|
527
|
-
# Connect to the MCP server
|
|
528
|
-
await client.connect(timeout=200)
|
|
529
|
-
|
|
530
|
-
# Register tools to controller with prefix
|
|
531
|
-
prefix = f"mcp.{server_name}."
|
|
532
|
-
await client.register_to_tools(
|
|
533
|
-
tools=self,
|
|
534
|
-
prefix=prefix
|
|
535
|
-
)
|
|
536
|
-
|
|
537
|
-
# Store client for later cleanup
|
|
538
|
-
self.mcp_clients[server_name] = client
|
|
539
|
-
|
|
540
|
-
logger.info(f'Successfully registered MCP server: {server_name} with prefix: {prefix}')
|
|
541
|
-
|
|
542
|
-
except Exception as e:
|
|
543
|
-
logger.error(f'Failed to register MCP server {server_name}: {str(e)}')
|
|
544
|
-
# Continue with other servers even if one fails
|
|
545
|
-
|
|
546
|
-
async def unregister_mcp_clients(self):
|
|
547
|
-
"""
|
|
548
|
-
Unregister and disconnect all MCP clients.
|
|
549
|
-
"""
|
|
550
|
-
# Disconnect all MCP clients
|
|
551
|
-
for server_name, client in self.mcp_clients.items():
|
|
552
|
-
try:
|
|
553
|
-
logger.info(f'Disconnecting MCP server: {server_name}')
|
|
554
|
-
await client.disconnect()
|
|
555
590
|
except Exception as e:
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
try:
|
|
560
|
-
# Get all registered actions
|
|
561
|
-
actions_to_remove = []
|
|
562
|
-
for action_name in list(self.registry.registry.actions.keys()):
|
|
563
|
-
if action_name.startswith('mcp.'):
|
|
564
|
-
actions_to_remove.append(action_name)
|
|
565
|
-
|
|
566
|
-
# Remove MCP actions from registry
|
|
567
|
-
for action_name in actions_to_remove:
|
|
568
|
-
if action_name in self.registry.registry.actions:
|
|
569
|
-
del self.registry.registry.actions[action_name]
|
|
570
|
-
logger.info(f'Removed MCP action: {action_name}')
|
|
571
|
-
|
|
572
|
-
except Exception as e:
|
|
573
|
-
logger.error(f'Failed to remove MCP actions from registry: {str(e)}')
|
|
574
|
-
|
|
575
|
-
# Clear the clients dictionary
|
|
576
|
-
self.mcp_clients.clear()
|
|
577
|
-
logger.info('All MCP clients unregistered and disconnected')
|
|
578
|
-
|
|
579
|
-
VibeSurfController = VibeSurfTools
|
|
591
|
+
error_msg = f'❌ Failed to take screenshot: {str(e)}'
|
|
592
|
+
logger.error(error_msg)
|
|
593
|
+
return ActionResult(error=error_msg)
|