vibesurf 0.1.35__py3-none-any.whl → 0.1.37__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.
@@ -35,6 +35,7 @@ from browser_use.tools.views import (
35
35
  StructuredOutputAction,
36
36
  SwitchTabAction,
37
37
  UploadFileAction,
38
+ NavigateAction
38
39
  )
39
40
  from browser_use.llm.base import BaseChatModel
40
41
  from browser_use.llm.messages import UserMessage, ContentPartTextParam, ContentPartImageParam, ImageURL
@@ -72,7 +73,7 @@ class BrowserUseTools(Tools, VibeSurfTools):
72
73
  self.display_files_in_done_text = display_files_in_done_text
73
74
 
74
75
  @self.registry.action(
75
- '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',
76
+ 'Complete task with structured output.',
76
77
  param_model=StructuredOutputAction[output_model],
77
78
  )
78
79
  async def done(params: StructuredOutputAction):
@@ -94,7 +95,7 @@ class BrowserUseTools(Tools, VibeSurfTools):
94
95
  else:
95
96
 
96
97
  @self.registry.action(
97
- '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.',
98
+ 'Complete task.',
98
99
  param_model=DoneAction,
99
100
  )
100
101
  async def done(params: DoneAction, file_system: CustomFileSystem):
@@ -143,8 +144,8 @@ class BrowserUseTools(Tools, VibeSurfTools):
143
144
  def _register_browser_actions(self):
144
145
  """Register custom browser actions"""
145
146
 
146
- @self.registry.action('Upload file to interactive element with file path', param_model=UploadFileAction)
147
- async def upload_file_to_element(
147
+ @self.registry.action('', param_model=UploadFileAction)
148
+ async def upload_file(
148
149
  params: UploadFileAction, browser_session: BrowserSession, file_system: FileSystem
149
150
  ):
150
151
 
@@ -250,7 +251,8 @@ class BrowserUseTools(Tools, VibeSurfTools):
250
251
 
251
252
  # Dispatch upload file event with the file input node
252
253
  try:
253
- event = browser_session.event_bus.dispatch(UploadFileEvent(node=file_input_node, file_path=full_file_path))
254
+ event = browser_session.event_bus.dispatch(
255
+ UploadFileEvent(node=file_input_node, file_path=full_file_path))
254
256
  await event
255
257
  await event.event_result(raise_if_any=True, raise_if_none=False)
256
258
  msg = f'Successfully uploaded file to index {params.index}'
@@ -264,10 +266,10 @@ class BrowserUseTools(Tools, VibeSurfTools):
264
266
  raise BrowserError(f'Failed to upload file: {e}')
265
267
 
266
268
  @self.registry.action(
267
- 'Hover over an element',
269
+ '',
268
270
  param_model=HoverAction,
269
271
  )
270
- async def hover_element(params: HoverAction, browser_session: AgentBrowserSession):
272
+ async def hover(params: HoverAction, browser_session: AgentBrowserSession):
271
273
  """Hovers over the element specified by its index from the cached selector map or by XPath."""
272
274
  try:
273
275
  if params.xpath:
@@ -370,7 +372,7 @@ class BrowserUseTools(Tools, VibeSurfTools):
370
372
  # =======================
371
373
 
372
374
  @self.registry.action(
373
- 'Search the query using the specified search engine. Defaults to DuckDuckGo (recommended) to avoid reCAPTCHA. Options: duckduckgo, google, bing. Query should be concrete and not vague or super long.',
375
+ '',
374
376
  param_model=SearchAction,
375
377
  )
376
378
  async def search(params: SearchAction, browser_session: AgentBrowserSession):
@@ -386,11 +388,11 @@ class BrowserUseTools(Tools, VibeSurfTools):
386
388
  'bing': f'https://www.bing.com/search?q={encoded_query}',
387
389
  }
388
390
 
389
- if params.search_engine.lower() not in search_engines:
391
+ if params.engine.lower() not in search_engines:
390
392
  return ActionResult(
391
- error=f'Unsupported search engine: {params.search_engine}. Options: duckduckgo, google, bing')
393
+ error=f'Unsupported search engine: {params.engine}. Options: duckduckgo, google, bing')
392
394
 
393
- search_url = search_engines[params.search_engine.lower()]
395
+ search_url = search_engines[params.engine.lower()]
394
396
 
395
397
  try:
396
398
  # Use AgentBrowserSession's direct navigation method
@@ -404,10 +406,10 @@ class BrowserUseTools(Tools, VibeSurfTools):
404
406
  return ActionResult(error=f'Failed to search Google for "{params.query}": {str(e)}')
405
407
 
406
408
  @self.registry.action(
407
- 'Navigate to URL, set new_tab=True to open in new tab, False to navigate in current tab',
408
- param_model=GoToUrlAction
409
+ '',
410
+ param_model=NavigateAction
409
411
  )
410
- async def go_to_url(params: GoToUrlAction, browser_session: AgentBrowserSession):
412
+ async def navigate(params: NavigateAction, browser_session: AgentBrowserSession):
411
413
  try:
412
414
  # Use AgentBrowserSession's direct navigation method
413
415
  await browser_session.navigate_to_url(params.url, new_tab=params.new_tab)
@@ -426,9 +428,10 @@ class BrowserUseTools(Tools, VibeSurfTools):
426
428
  return ActionResult(error=f'Navigation failed: {str(e)}')
427
429
 
428
430
  @self.registry.action(
429
- 'Go back',
431
+ '',
432
+ param_model=NoParamsAction
430
433
  )
431
- async def go_back(browser_session: AgentBrowserSession):
434
+ async def go_back(_: NoParamsAction, browser_session: AgentBrowserSession):
432
435
  try:
433
436
  cdp_session = await browser_session.get_or_create_cdp_session()
434
437
  history = await cdp_session.cdp_client.send.Page.getNavigationHistory(session_id=cdp_session.session_id)
@@ -458,18 +461,12 @@ class BrowserUseTools(Tools, VibeSurfTools):
458
461
  return ActionResult(error=f'Failed to go back: {str(e)}')
459
462
 
460
463
  @self.registry.action(
461
- 'Switch tab',
464
+ '',
462
465
  param_model=SwitchTabAction
463
466
  )
464
- async def switch_tab(params: SwitchTabAction, browser_session: AgentBrowserSession):
467
+ async def switch(params: SwitchTabAction, browser_session: AgentBrowserSession):
465
468
  try:
466
-
467
- if params.tab_id:
468
- target_id = await browser_session.get_target_id_from_tab_id(params.tab_id)
469
- elif params.url:
470
- target_id = await browser_session.get_target_id_from_url(params.url)
471
- else:
472
- target_id = await browser_session.get_most_recently_opened_target_id()
469
+ target_id = await browser_session.get_target_id_from_tab_id(params.tab_id)
473
470
 
474
471
  # Switch to target using CDP
475
472
  await browser_session.get_or_create_cdp_session(target_id, focus=True)
@@ -488,7 +485,7 @@ class BrowserUseTools(Tools, VibeSurfTools):
488
485
  async def take_screenshot(_: NoParamsAction, browser_session: AgentBrowserSession, file_system: FileSystem):
489
486
  try:
490
487
  # Take screenshot using browser session
491
- screenshot = await browser_session.take_screenshot()
488
+ screenshot_bytes = await browser_session.take_screenshot()
492
489
 
493
490
  # Generate timestamp for filename
494
491
  timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
@@ -507,7 +504,7 @@ class BrowserUseTools(Tools, VibeSurfTools):
507
504
  filepath = screenshots_dir / filename
508
505
 
509
506
  with open(filepath, "wb") as f:
510
- f.write(base64.b64decode(screenshot))
507
+ f.write(screenshot_bytes)
511
508
 
512
509
  msg = f'📸 Screenshot saved to path: {str(filepath.relative_to(fs_dir))}'
513
510
  logger.info(msg)
@@ -530,23 +527,23 @@ class BrowserUseTools(Tools, VibeSurfTools):
530
527
  try:
531
528
  # Get file system directory path (Path type)
532
529
  fs_dir = file_system.get_dir()
533
-
530
+
534
531
  # Create downloads directory if it doesn't exist
535
532
  downloads_dir = fs_dir / "downloads"
536
533
  downloads_dir.mkdir(exist_ok=True)
537
-
534
+
538
535
  # Download the file and detect format
539
536
  async with aiohttp.ClientSession() as session:
540
537
  async with session.get(params.url) as response:
541
538
  if response.status != 200:
542
539
  raise Exception(f"HTTP {response.status}: Failed to download from {params.url}")
543
-
540
+
544
541
  # Get content
545
542
  content = await response.read()
546
-
543
+ headers_dict = dict(response.headers)
547
544
  # Detect file format and extension
548
- file_extension = await self._detect_file_format(params.url, response.headers, content)
549
-
545
+ file_extension = await self._detect_file_format(params.url, headers_dict, content)
546
+
550
547
  # Generate filename
551
548
  if params.filename:
552
549
  # Use provided filename, add extension if missing
@@ -557,7 +554,7 @@ class BrowserUseTools(Tools, VibeSurfTools):
557
554
  # Generate filename from URL or timestamp
558
555
  url_path = urllib.parse.urlparse(params.url).path
559
556
  url_filename = os.path.basename(url_path)
560
-
557
+
561
558
  if url_filename and not url_filename.startswith('.'):
562
559
  # Use URL filename, ensure correct extension
563
560
  filename = url_filename
@@ -568,19 +565,19 @@ class BrowserUseTools(Tools, VibeSurfTools):
568
565
  # Generate timestamp-based filename
569
566
  timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
570
567
  filename = f"media_{timestamp}{file_extension}"
571
-
568
+
572
569
  # Sanitize filename
573
570
  filename = sanitize_filename(filename)
574
571
  filepath = downloads_dir / filename
575
-
572
+
576
573
  # Save file
577
574
  with open(filepath, "wb") as f:
578
575
  f.write(content)
579
-
576
+
580
577
  # Calculate file size for display
581
578
  file_size = len(content)
582
579
  size_str = self._format_file_size(file_size)
583
-
580
+
584
581
  msg = f'📥 Downloaded media to: {str(filepath.relative_to(fs_dir))} ({size_str})'
585
582
  logger.info(msg)
586
583
  return ActionResult(
@@ -588,7 +585,7 @@ class BrowserUseTools(Tools, VibeSurfTools):
588
585
  include_in_memory=True,
589
586
  long_term_memory=f'Downloaded media from {params.url} to {str(filepath.relative_to(fs_dir))}',
590
587
  )
591
-
588
+
592
589
  except Exception as e:
593
590
  error_msg = f'❌ Failed to download media: {str(e)}'
594
591
  logger.error(error_msg)
@@ -666,9 +663,9 @@ class BrowserUseTools(Tools, VibeSurfTools):
666
663
  if url_path:
667
664
  ext = os.path.splitext(url_path)[1].lower()
668
665
  if ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.tiff',
669
- '.mp4', '.webm', '.avi', '.mov', '.wmv', '.flv',
670
- '.mp3', '.wav', '.ogg', '.aac', '.flac',
671
- '.pdf', '.doc', '.docx', '.txt']:
666
+ '.mp4', '.webm', '.avi', '.mov', '.wmv', '.flv',
667
+ '.mp3', '.wav', '.ogg', '.aac', '.flac',
668
+ '.pdf', '.doc', '.docx', '.txt']:
672
669
  return ext
673
670
 
674
671
  # Default fallback
@@ -6,7 +6,9 @@ from pathlib import Path
6
6
  from browser_use.filesystem.file_system import FileSystem, FileSystemError, INVALID_FILENAME_ERROR_MESSAGE, \
7
7
  FileSystemState
8
8
  from browser_use.filesystem.file_system import BaseFile, MarkdownFile, TxtFile, JsonFile, CsvFile, PdfFile
9
+ from vibe_surf.logger import get_logger
9
10
 
11
+ logger = get_logger(__name__)
10
12
 
11
13
  class PythonFile(BaseFile):
12
14
  """Plain text file implementation"""
@@ -315,9 +317,10 @@ class CustomFileSystem(FileSystem):
315
317
  """Save extracted content to a numbered file"""
316
318
  initial_filename = f'extracted_content_{self.extracted_content_count}'
317
319
  extracted_filename = f'{initial_filename}.md'
318
- await self.write_file(initial_filename, content)
320
+ write_result = await self.write_file(extracted_filename, content)
321
+ logger.info(write_result)
319
322
  self.extracted_content_count += 1
320
- return f'Extracted content saved to file {extracted_filename} successfully.'
323
+ return extracted_filename
321
324
 
322
325
  async def list_directory(self, directory_path: str = "") -> str:
323
326
  """List contents of a directory within the file system (data_dir only)"""
@@ -0,0 +1,118 @@
1
+ from bs4 import BeautifulSoup
2
+ from browser_use.dom.service import EnhancedDOMTreeNode
3
+
4
+ def clean_html_basic(page_html_content, max_text_length=100):
5
+ soup = BeautifulSoup(page_html_content, 'html.parser')
6
+
7
+ for script in soup(["script", "style"]):
8
+ script.decompose()
9
+
10
+ from bs4 import Comment
11
+ comments = soup.findAll(text=lambda text: isinstance(text, Comment))
12
+ for comment in comments:
13
+ comment.extract()
14
+
15
+ for text_node in soup.find_all(string=True):
16
+ if text_node.parent.name not in ['script', 'style']:
17
+ clean_text = ' '.join(text_node.split())
18
+
19
+ if len(clean_text) > max_text_length:
20
+ clean_text = clean_text[:max_text_length].rstrip() + "..."
21
+
22
+ if clean_text != text_node:
23
+ text_node.replace_with(clean_text)
24
+
25
+ important_attrs = ['id', 'class', 'name', 'role', 'type',
26
+ 'colspan', 'rowspan', 'headers', 'scope',
27
+ 'href', 'src', 'alt', 'title']
28
+
29
+ for tag in soup.find_all():
30
+ attrs_to_keep = {}
31
+ for attr in list(tag.attrs.keys()):
32
+ if (attr in important_attrs or
33
+ attr.startswith('data-') or
34
+ attr.startswith('aria-')):
35
+ attrs_to_keep[attr] = tag.attrs[attr]
36
+ tag.attrs = attrs_to_keep
37
+
38
+ return str(soup)
39
+
40
+
41
+ def get_sibling_position(node: EnhancedDOMTreeNode) -> int:
42
+ """Get the position of node among its siblings with the same tag"""
43
+ if not node.parent_node:
44
+ return 1
45
+
46
+ tag_name = node.tag_name
47
+ position = 1
48
+
49
+ # Find siblings with same tag name before this node
50
+ for sibling in node.parent_node.children:
51
+ if sibling == node:
52
+ break
53
+ if sibling.tag_name == tag_name:
54
+ position += 1
55
+
56
+ return position
57
+
58
+
59
+ def extract_css_hints(node: EnhancedDOMTreeNode) -> dict:
60
+ """Extract CSS selector construction hints"""
61
+ hints = {}
62
+
63
+ if "id" in node.attributes:
64
+ hints["id"] = f"#{node.attributes['id']}"
65
+
66
+ if "class" in node.attributes:
67
+ classes = node.attributes["class"].split()
68
+ hints["class"] = f".{'.'.join(classes[:3])}" # Limit class count
69
+
70
+ # Attribute selector hints
71
+ for attr in ["name", "data-testid", "type"]:
72
+ if attr in node.attributes:
73
+ hints[f"attr_{attr}"] = f"[{attr}='{node.attributes[attr]}']"
74
+
75
+ return hints
76
+
77
+
78
+ def convert_selector_map_for_llm(selector_map) -> dict:
79
+ """
80
+ Convert complex selector_map to simplified format suitable for LLM understanding and JS code writing
81
+ """
82
+ simplified_elements = []
83
+
84
+ for element_index, node in selector_map.items():
85
+ if node.is_visible and node.element_index is not None: # Only include visible interactive elements
86
+ element_info = {
87
+ "tag": node.tag_name,
88
+ "text": node.get_meaningful_text_for_llm()[:200], # Limit text length
89
+
90
+ # Selector information - most needed for JS code
91
+ "selectors": {
92
+ "xpath": node.xpath,
93
+ "css_hints": extract_css_hints(node), # Extract id, class etc
94
+ },
95
+
96
+ # Element semantics
97
+ "role": node.ax_node.role if node.ax_node else None,
98
+ "type": node.attributes.get("type"),
99
+ "aria_label": node.attributes.get("aria-label"),
100
+
101
+ # Key attributes
102
+ "attributes": {k: v for k, v in node.attributes.items()
103
+ if k in ["id", "class", "name", "href", "src", "value", "placeholder", "data-testid"]},
104
+
105
+ # Interactivity
106
+ "is_clickable": node.snapshot_node.is_clickable if node.snapshot_node else False,
107
+ "is_input": node.tag_name.lower() in ["input", "textarea", "select"],
108
+
109
+ # Structure information
110
+ "parent_tag": node.parent_node.tag_name if node.parent_node else None,
111
+ "position_info": f"{node.tag_name}[{get_sibling_position(node)}]"
112
+ }
113
+ simplified_elements.append(element_info)
114
+
115
+ return {
116
+ "page_elements": simplified_elements,
117
+ "total_elements": len(simplified_elements)
118
+ }