vibesurf 0.1.25__py3-none-any.whl → 0.1.27__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.
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.25'
32
- __version_tuple__ = version_tuple = (0, 1, 25)
31
+ __version__ = version = '0.1.27'
32
+ __version_tuple__ = version_tuple = (0, 1, 27)
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}'))
@@ -104,7 +104,7 @@ async def submit_task(
104
104
  logger.info("Using default empty MCP server configuration")
105
105
 
106
106
  # DEBUG: Log the type and content of mcp_server_config
107
- logger.info(f"mcp_server_config type: {type(mcp_server_config)}, value: {mcp_server_config}")
107
+ logger.debug(f"mcp_server_config type: {type(mcp_server_config)}, value: {mcp_server_config}")
108
108
 
109
109
  # Create initial task record in database
110
110
  from ..database.queries import TaskQueries
@@ -486,13 +486,13 @@ class TaskQueries:
486
486
  return existing_task
487
487
  else:
488
488
  # DEBUG: Log the type and content of mcp_server_config before saving
489
- logger.info(
489
+ logger.debug(
490
490
  f"Creating task with mcp_server_config type: {type(mcp_server_config)}, value: {mcp_server_config}")
491
491
 
492
492
  # Serialize mcp_server_config to JSON string if it's a dict
493
493
  if isinstance(mcp_server_config, dict):
494
494
  mcp_server_config_json = json.dumps(mcp_server_config)
495
- logger.info(f"Converted dict to JSON string: {mcp_server_config_json}")
495
+ logger.debug(f"Converted dict to JSON string: {mcp_server_config_json}")
496
496
  else:
497
497
  mcp_server_config_json = mcp_server_config
498
498
 
@@ -58,7 +58,7 @@ def create_llm_from_profile(llm_profile) -> BaseChatModel:
58
58
  "deepseek": ["temperature"],
59
59
  "aws_bedrock": ["temperature"],
60
60
  "anthropic_bedrock": ["temperature"],
61
- "openai_compatible": ["temperature"]
61
+ "openai_compatible": ["temperature", "max_tokens"]
62
62
  }
63
63
 
64
64
  # Build common parameters based on provider support
@@ -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
vibe_surf/cli.py CHANGED
@@ -325,7 +325,7 @@ def start_backend(port: int) -> None:
325
325
  console.print("[yellow]📝 Press Ctrl+C to stop the server[/yellow]\n")
326
326
 
327
327
  # Run the server
328
- uvicorn.run(app, host="127.0.0.1", port=port, log_level="info")
328
+ uvicorn.run(app, host="127.0.0.1", port=port, log_level="error")
329
329
 
330
330
  except KeyboardInterrupt:
331
331
  console.print("\n[yellow]🛑 Server stopped by user[/yellow]")
@@ -76,7 +76,7 @@ class ChatOpenAICompatible(ChatOpenAI):
76
76
  The class automatically detects the model type and applies appropriate fixes.
77
77
  """
78
78
 
79
- max_completion_tokens: int | None = 16000
79
+ max_completion_tokens: int | None = 8192
80
80
 
81
81
  def _is_gemini_model(self) -> bool:
82
82
  """Check if the current model is a Gemini model."""
@@ -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]}"