vibesurf 0.1.26__py3-none-any.whl → 0.1.28__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 CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.1.26'
32
- __version_tuple__ = version_tuple = (0, 1, 26)
31
+ __version__ = version = '0.1.28'
32
+ __version_tuple__ = version_tuple = (0, 1, 28)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -479,17 +479,16 @@ async def _vibesurf_agent_node_impl(state: VibeSurfState) -> VibeSurfState:
479
479
  llm=vibesurf_agent.llm,
480
480
  file_system=vibesurf_agent.file_system,
481
481
  )
482
- if action_name in ["skill_search", "skill_crawl", "skill_summary", "skill_finance"]:
482
+ if action_name.startswith("skill_"):
483
483
  state.current_step = "END"
484
484
  # Format final response
485
- final_response = f"{result.extracted_content}"
486
- await log_agent_activity(state, agent_name, "result", final_response)
485
+ final_response = f"{result.extracted_content}" or f"{result.error}"
487
486
  state.final_response = final_response
488
487
  logger.debug(final_response)
489
488
  state.is_complete = True
490
- return state
489
+ else:
490
+ state.current_step = "vibesurf_agent"
491
491
 
492
- state.current_step = "vibesurf_agent"
493
492
  if result.extracted_content:
494
493
  vibesurf_agent.message_history.append(
495
494
  UserMessage(content=f'Action result:\n{result.extracted_content}'))
@@ -384,6 +384,32 @@ class AgentBrowserSession(BrowserSession):
384
384
  )
385
385
  ]
386
386
 
387
+ def model_post_init(self, __context) -> None:
388
+ """Register event handlers after model initialization."""
389
+ # Check if handlers are already registered to prevent duplicates
390
+
391
+ from browser_use.browser.watchdog_base import BaseWatchdog
392
+
393
+ start_handlers = self.event_bus.handlers.get('BrowserStartEvent', [])
394
+ start_handler_names = [getattr(h, '__name__', str(h)) for h in start_handlers]
395
+
396
+ if any('on_BrowserStartEvent' in name for name in start_handler_names):
397
+ raise RuntimeError(
398
+ '[BrowserSession] Duplicate handler registration attempted! '
399
+ 'on_BrowserStartEvent is already registered. '
400
+ 'This likely means BrowserSession was initialized multiple times with the same EventBus.'
401
+ )
402
+
403
+ BaseWatchdog.attach_handler_to_session(self, BrowserStartEvent, self.on_BrowserStartEvent)
404
+ BaseWatchdog.attach_handler_to_session(self, BrowserStopEvent, self.on_BrowserStopEvent)
405
+ BaseWatchdog.attach_handler_to_session(self, NavigateToUrlEvent, self.on_NavigateToUrlEvent)
406
+ BaseWatchdog.attach_handler_to_session(self, SwitchTabEvent, self.on_SwitchTabEvent)
407
+ BaseWatchdog.attach_handler_to_session(self, TabCreatedEvent, self.on_TabCreatedEvent)
408
+ BaseWatchdog.attach_handler_to_session(self, TabClosedEvent, self.on_TabClosedEvent)
409
+ BaseWatchdog.attach_handler_to_session(self, AgentFocusChangedEvent, self.on_AgentFocusChangedEvent)
410
+ # BaseWatchdog.attach_handler_to_session(self, FileDownloadedEvent, self.on_FileDownloadedEvent)
411
+ BaseWatchdog.attach_handler_to_session(self, CloseTabEvent, self.on_CloseTabEvent)
412
+
387
413
  async def attach_all_watchdogs(self) -> None:
388
414
  """Initialize and attach all watchdogs EXCEPT AboutBlankWatchdog to disable DVD animation."""
389
415
  # Prevent duplicate watchdog attachment
@@ -6,6 +6,9 @@ import enum
6
6
  import base64
7
7
  import mimetypes
8
8
  import datetime
9
+ import aiohttp
10
+ import re
11
+ import urllib.parse
9
12
  from pathvalidate import sanitize_filename
10
13
  from typing import Optional, Type, Callable, Dict, Any, Union, Awaitable, TypeVar
11
14
  from pydantic import BaseModel
@@ -40,7 +43,7 @@ from browser_use.browser.views import BrowserError
40
43
  from browser_use.mcp.client import MCPClient
41
44
 
42
45
  from vibe_surf.browser.agent_browser_session import AgentBrowserSession
43
- from vibe_surf.tools.views import HoverAction, ExtractionAction, FileExtractionAction
46
+ from vibe_surf.tools.views import HoverAction, ExtractionAction, FileExtractionAction, DownloadMediaAction
44
47
  from vibe_surf.tools.mcp_client import CustomMCPClient
45
48
  from vibe_surf.tools.file_system import CustomFileSystem
46
49
  from vibe_surf.logger import get_logger
@@ -501,3 +504,167 @@ class BrowserUseTools(Tools, VibeSurfTools):
501
504
  error_msg = f'❌ Failed to take screenshot: {str(e)}'
502
505
  logger.error(error_msg)
503
506
  return ActionResult(error=error_msg)
507
+
508
+ @self.registry.action(
509
+ 'Download media from URL and save to filesystem downloads folder',
510
+ param_model=DownloadMediaAction
511
+ )
512
+ async def download_media(params: DownloadMediaAction, file_system: FileSystem):
513
+ """Download media from URL with automatic file format detection"""
514
+ try:
515
+ # Get file system directory path (Path type)
516
+ fs_dir = file_system.get_dir()
517
+
518
+ # Create downloads directory if it doesn't exist
519
+ downloads_dir = fs_dir / "downloads"
520
+ downloads_dir.mkdir(exist_ok=True)
521
+
522
+ # Download the file and detect format
523
+ async with aiohttp.ClientSession() as session:
524
+ async with session.get(params.url) as response:
525
+ if response.status != 200:
526
+ raise Exception(f"HTTP {response.status}: Failed to download from {params.url}")
527
+
528
+ # Get content
529
+ content = await response.read()
530
+
531
+ # Detect file format and extension
532
+ file_extension = await self._detect_file_format(params.url, response.headers, content)
533
+
534
+ # Generate filename
535
+ if params.filename:
536
+ # Use provided filename, add extension if missing
537
+ filename = params.filename
538
+ if not filename.endswith(file_extension):
539
+ filename = f"{filename}{file_extension}"
540
+ else:
541
+ # Generate filename from URL or timestamp
542
+ url_path = urllib.parse.urlparse(params.url).path
543
+ url_filename = os.path.basename(url_path)
544
+
545
+ if url_filename and not url_filename.startswith('.'):
546
+ # Use URL filename, ensure correct extension
547
+ filename = url_filename
548
+ if not filename.endswith(file_extension):
549
+ base_name = os.path.splitext(filename)[0]
550
+ filename = f"{base_name}{file_extension}"
551
+ else:
552
+ # Generate timestamp-based filename
553
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
554
+ filename = f"media_{timestamp}{file_extension}"
555
+
556
+ # Sanitize filename
557
+ filename = sanitize_filename(filename)
558
+ filepath = downloads_dir / filename
559
+
560
+ # Save file
561
+ with open(filepath, "wb") as f:
562
+ f.write(content)
563
+
564
+ # Calculate file size for display
565
+ file_size = len(content)
566
+ size_str = self._format_file_size(file_size)
567
+
568
+ msg = f'📥 Downloaded media to: {str(filepath.relative_to(fs_dir))} ({size_str})'
569
+ logger.info(msg)
570
+ return ActionResult(
571
+ extracted_content=msg,
572
+ include_in_memory=True,
573
+ long_term_memory=f'Downloaded media from {params.url} to {str(filepath.relative_to(fs_dir))}',
574
+ )
575
+
576
+ except Exception as e:
577
+ error_msg = f'❌ Failed to download media: {str(e)}'
578
+ logger.error(error_msg)
579
+ return ActionResult(error=error_msg)
580
+
581
+ async def _detect_file_format(self, url: str, headers: dict, content: bytes) -> str:
582
+ """Detect file format from URL, headers, and content"""
583
+
584
+ # Try Content-Type header first
585
+ content_type = headers.get('content-type', '').lower()
586
+ if content_type:
587
+ # Common image formats
588
+ if 'image/jpeg' in content_type or 'image/jpg' in content_type:
589
+ return '.jpg'
590
+ elif 'image/png' in content_type:
591
+ return '.png'
592
+ elif 'image/gif' in content_type:
593
+ return '.gif'
594
+ elif 'image/webp' in content_type:
595
+ return '.webp'
596
+ elif 'image/svg' in content_type:
597
+ return '.svg'
598
+ elif 'image/bmp' in content_type:
599
+ return '.bmp'
600
+ elif 'image/tiff' in content_type:
601
+ return '.tiff'
602
+ # Video formats
603
+ elif 'video/mp4' in content_type:
604
+ return '.mp4'
605
+ elif 'video/webm' in content_type:
606
+ return '.webm'
607
+ elif 'video/avi' in content_type:
608
+ return '.avi'
609
+ elif 'video/mov' in content_type or 'video/quicktime' in content_type:
610
+ return '.mov'
611
+ # Audio formats
612
+ elif 'audio/mpeg' in content_type or 'audio/mp3' in content_type:
613
+ return '.mp3'
614
+ elif 'audio/wav' in content_type:
615
+ return '.wav'
616
+ elif 'audio/ogg' in content_type:
617
+ return '.ogg'
618
+ elif 'audio/webm' in content_type:
619
+ return '.webm'
620
+
621
+ # Try magic number detection
622
+ if len(content) >= 8:
623
+ # JPEG
624
+ if content.startswith(b'\xff\xd8\xff'):
625
+ return '.jpg'
626
+ # PNG
627
+ elif content.startswith(b'\x89PNG\r\n\x1a\n'):
628
+ return '.png'
629
+ # GIF
630
+ elif content.startswith(b'GIF87a') or content.startswith(b'GIF89a'):
631
+ return '.gif'
632
+ # WebP
633
+ elif content[8:12] == b'WEBP':
634
+ return '.webp'
635
+ # BMP
636
+ elif content.startswith(b'BM'):
637
+ return '.bmp'
638
+ # TIFF
639
+ elif content.startswith(b'II*\x00') or content.startswith(b'MM\x00*'):
640
+ return '.tiff'
641
+ # MP4
642
+ elif b'ftyp' in content[4:12]:
643
+ return '.mp4'
644
+ # PDF
645
+ elif content.startswith(b'%PDF'):
646
+ return '.pdf'
647
+
648
+ # Try URL path extension
649
+ url_path = urllib.parse.urlparse(url).path
650
+ if url_path:
651
+ ext = os.path.splitext(url_path)[1].lower()
652
+ if ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.tiff',
653
+ '.mp4', '.webm', '.avi', '.mov', '.wmv', '.flv',
654
+ '.mp3', '.wav', '.ogg', '.aac', '.flac',
655
+ '.pdf', '.doc', '.docx', '.txt']:
656
+ return ext
657
+
658
+ # Default fallback
659
+ return '.bin'
660
+
661
+ def _format_file_size(self, size_bytes: int) -> str:
662
+ """Format file size in human readable format"""
663
+ if size_bytes == 0:
664
+ return "0 B"
665
+ size_names = ["B", "KB", "MB", "GB", "TB"]
666
+ i = 0
667
+ while size_bytes >= 1024.0 and i < len(size_names) - 1:
668
+ size_bytes /= 1024.0
669
+ i += 1
670
+ return f"{size_bytes:.1f} {size_names[i]}"