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 +2 -2
- vibe_surf/agents/vibe_surf_agent.py +4 -5
- vibe_surf/backend/api/task.py +1 -1
- vibe_surf/backend/database/queries.py +2 -2
- vibe_surf/backend/utils/llm_factory.py +1 -1
- vibe_surf/browser/agent_browser_session.py +26 -0
- vibe_surf/cli.py +1 -1
- vibe_surf/llm/openai_compatible.py +1 -1
- vibe_surf/tools/browser_use_tools.py +168 -1
- vibe_surf/tools/vibesurf_tools.py +463 -21
- vibe_surf/tools/views.py +75 -0
- {vibesurf-0.1.25.dist-info → vibesurf-0.1.27.dist-info}/METADATA +26 -5
- {vibesurf-0.1.25.dist-info → vibesurf-0.1.27.dist-info}/RECORD +17 -17
- vibesurf-0.1.27.dist-info/licenses/LICENSE +22 -0
- vibesurf-0.1.25.dist-info/licenses/LICENSE +0 -201
- {vibesurf-0.1.25.dist-info → vibesurf-0.1.27.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.25.dist-info → vibesurf-0.1.27.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.25.dist-info → vibesurf-0.1.27.dist-info}/top_level.txt +0 -0
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.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
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
|
|
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
|
-
|
|
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}'))
|
vibe_surf/backend/api/task.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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="
|
|
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 =
|
|
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]}"
|