vibesurf 0.1.36__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.
- vibe_surf/_version.py +2 -2
- vibe_surf/agents/browser_use_agent.py +14 -276
- vibe_surf/agents/report_writer_agent.py +21 -1
- vibe_surf/agents/vibe_surf_agent.py +61 -2
- vibe_surf/backend/llm_config.py +27 -0
- vibe_surf/backend/shared_state.py +26 -26
- vibe_surf/backend/utils/encryption.py +40 -4
- vibe_surf/backend/utils/llm_factory.py +16 -0
- vibe_surf/browser/agen_browser_profile.py +5 -0
- vibe_surf/browser/agent_browser_session.py +116 -25
- vibe_surf/browser/watchdogs/action_watchdog.py +1 -83
- vibe_surf/browser/watchdogs/dom_watchdog.py +9 -6
- vibe_surf/cli.py +2 -0
- vibe_surf/llm/openai_compatible.py +2 -9
- vibe_surf/telemetry/views.py +32 -0
- vibe_surf/tools/browser_use_tools.py +39 -42
- vibe_surf/tools/file_system.py +5 -2
- vibe_surf/tools/utils.py +118 -0
- vibe_surf/tools/vibesurf_tools.py +44 -236
- vibe_surf/tools/views.py +1 -1
- {vibesurf-0.1.36.dist-info → vibesurf-0.1.37.dist-info}/METADATA +12 -2
- {vibesurf-0.1.36.dist-info → vibesurf-0.1.37.dist-info}/RECORD +26 -25
- {vibesurf-0.1.36.dist-info → vibesurf-0.1.37.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.36.dist-info → vibesurf-0.1.37.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.36.dist-info → vibesurf-0.1.37.dist-info}/licenses/LICENSE +0 -0
- {vibesurf-0.1.36.dist-info → vibesurf-0.1.37.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
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
|
|
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('
|
|
147
|
-
async def
|
|
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(
|
|
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
|
-
'
|
|
269
|
+
'',
|
|
268
270
|
param_model=HoverAction,
|
|
269
271
|
)
|
|
270
|
-
async def
|
|
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
|
-
'
|
|
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.
|
|
391
|
+
if params.engine.lower() not in search_engines:
|
|
390
392
|
return ActionResult(
|
|
391
|
-
error=f'Unsupported search engine: {params.
|
|
393
|
+
error=f'Unsupported search engine: {params.engine}. Options: duckduckgo, google, bing')
|
|
392
394
|
|
|
393
|
-
search_url = search_engines[params.
|
|
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
|
-
'
|
|
408
|
-
param_model=
|
|
409
|
+
'',
|
|
410
|
+
param_model=NavigateAction
|
|
409
411
|
)
|
|
410
|
-
async def
|
|
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
|
-
'
|
|
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
|
-
'
|
|
464
|
+
'',
|
|
462
465
|
param_model=SwitchTabAction
|
|
463
466
|
)
|
|
464
|
-
async def
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
vibe_surf/tools/file_system.py
CHANGED
|
@@ -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(
|
|
320
|
+
write_result = await self.write_file(extracted_filename, content)
|
|
321
|
+
logger.info(write_result)
|
|
319
322
|
self.extracted_content_count += 1
|
|
320
|
-
return
|
|
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)"""
|
vibe_surf/tools/utils.py
ADDED
|
@@ -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
|
+
}
|