vibesurf 0.1.22__py3-none-any.whl → 0.1.24__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 +2 -2
- vibe_surf/agents/prompts/vibe_surf_prompt.py +10 -0
- vibe_surf/agents/vibe_surf_agent.py +13 -2
- vibe_surf/backend/api/agent.py +38 -0
- vibe_surf/backend/api/config.py +3 -1
- vibe_surf/backend/api/task.py +1 -1
- vibe_surf/backend/main.py +2 -0
- vibe_surf/backend/utils/llm_factory.py +2 -1
- vibe_surf/browser/agent_browser_session.py +5 -5
- vibe_surf/chrome_extension/scripts/api-client.js +5 -0
- vibe_surf/chrome_extension/scripts/main.js +1 -1
- vibe_surf/chrome_extension/scripts/ui-manager.js +397 -20
- vibe_surf/chrome_extension/sidepanel.html +13 -1
- vibe_surf/chrome_extension/styles/input.css +115 -0
- vibe_surf/llm/openai_compatible.py +35 -10
- vibe_surf/tools/browser_use_tools.py +0 -90
- vibe_surf/tools/file_system.py +2 -2
- vibe_surf/tools/finance_tools.py +586 -0
- vibe_surf/tools/report_writer_tools.py +2 -1
- vibe_surf/tools/vibesurf_registry.py +52 -0
- vibe_surf/tools/vibesurf_tools.py +1044 -6
- vibe_surf/tools/views.py +93 -0
- {vibesurf-0.1.22.dist-info → vibesurf-0.1.24.dist-info}/METADATA +2 -1
- {vibesurf-0.1.22.dist-info → vibesurf-0.1.24.dist-info}/RECORD +28 -25
- {vibesurf-0.1.22.dist-info → vibesurf-0.1.24.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.22.dist-info → vibesurf-0.1.24.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.22.dist-info → vibesurf-0.1.24.dist-info}/licenses/LICENSE +0 -0
- {vibesurf-0.1.22.dist-info → vibesurf-0.1.24.dist-info}/top_level.txt +0 -0
|
@@ -51,6 +51,8 @@ from browser_use.llm.openai.serializer import OpenAIMessageSerializer
|
|
|
51
51
|
from browser_use.llm.schema import SchemaOptimizer
|
|
52
52
|
from browser_use.llm.views import ChatInvokeCompletion, ChatInvokeUsage
|
|
53
53
|
|
|
54
|
+
from json_repair import repair_json
|
|
55
|
+
|
|
54
56
|
T = TypeVar('T', bound=BaseModel)
|
|
55
57
|
|
|
56
58
|
from vibe_surf.logger import get_logger
|
|
@@ -74,6 +76,8 @@ class ChatOpenAICompatible(ChatOpenAI):
|
|
|
74
76
|
The class automatically detects the model type and applies appropriate fixes.
|
|
75
77
|
"""
|
|
76
78
|
|
|
79
|
+
max_completion_tokens: int | None = 16000
|
|
80
|
+
|
|
77
81
|
def _is_gemini_model(self) -> bool:
|
|
78
82
|
"""Check if the current model is a Gemini model."""
|
|
79
83
|
return str(self.model).lower().startswith('gemini')
|
|
@@ -82,6 +86,11 @@ class ChatOpenAICompatible(ChatOpenAI):
|
|
|
82
86
|
"""Check if the current model is a Kimi/Moonshot model."""
|
|
83
87
|
model_str = str(self.model).lower()
|
|
84
88
|
return 'kimi' in model_str or 'moonshot' in model_str
|
|
89
|
+
|
|
90
|
+
def _is_deepseek_model(self) -> bool:
|
|
91
|
+
"""Check if the current model is a Kimi/Moonshot model."""
|
|
92
|
+
model_str = str(self.model).lower()
|
|
93
|
+
return 'deepseek' in model_str
|
|
85
94
|
|
|
86
95
|
def _is_qwen_model(self) -> bool:
|
|
87
96
|
"""Check if the current model is a Qwen model."""
|
|
@@ -223,10 +232,10 @@ class ChatOpenAICompatible(ChatOpenAI):
|
|
|
223
232
|
"""
|
|
224
233
|
# If this is not a special model or no structured output is requested,
|
|
225
234
|
# use the parent implementation directly
|
|
226
|
-
if self._is_qwen_model() or self._is_kimi_model():
|
|
235
|
+
if self._is_qwen_model() or self._is_kimi_model() or self._is_deepseek_model() :
|
|
227
236
|
self.add_schema_to_system_prompt = True
|
|
228
237
|
|
|
229
|
-
if not (self._is_gemini_model() or self._is_kimi_model() or self._is_qwen_model()) or output_format is None:
|
|
238
|
+
if not (self._is_gemini_model() or self._is_kimi_model() or self._is_qwen_model() or self._is_deepseek_model()) or output_format is None:
|
|
230
239
|
return await super().ainvoke(messages, output_format)
|
|
231
240
|
openai_messages = OpenAIMessageSerializer.serialize_messages(messages)
|
|
232
241
|
|
|
@@ -241,6 +250,7 @@ class ChatOpenAICompatible(ChatOpenAI):
|
|
|
241
250
|
|
|
242
251
|
if self.max_completion_tokens is not None:
|
|
243
252
|
model_params['max_completion_tokens'] = self.max_completion_tokens
|
|
253
|
+
model_params['max_tokens'] = self.max_completion_tokens
|
|
244
254
|
|
|
245
255
|
if self.top_p is not None:
|
|
246
256
|
model_params['top_p'] = self.top_p
|
|
@@ -298,12 +308,22 @@ class ChatOpenAICompatible(ChatOpenAI):
|
|
|
298
308
|
]
|
|
299
309
|
|
|
300
310
|
# Return structured response
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
311
|
+
if self.add_schema_to_system_prompt:
|
|
312
|
+
response = await self.get_client().chat.completions.create(
|
|
313
|
+
model=self.model,
|
|
314
|
+
messages=openai_messages,
|
|
315
|
+
response_format={
|
|
316
|
+
'type': 'json_object'
|
|
317
|
+
},
|
|
318
|
+
**model_params,
|
|
319
|
+
)
|
|
320
|
+
else:
|
|
321
|
+
response = await self.get_client().chat.completions.create(
|
|
322
|
+
model=self.model,
|
|
323
|
+
messages=openai_messages,
|
|
324
|
+
response_format=ResponseFormatJSONSchema(json_schema=response_format, type='json_schema'),
|
|
325
|
+
**model_params,
|
|
326
|
+
)
|
|
307
327
|
|
|
308
328
|
if response.choices[0].message.content is None:
|
|
309
329
|
raise ModelProviderError(
|
|
@@ -313,8 +333,13 @@ class ChatOpenAICompatible(ChatOpenAI):
|
|
|
313
333
|
)
|
|
314
334
|
|
|
315
335
|
usage = self._get_usage(response)
|
|
316
|
-
|
|
317
|
-
|
|
336
|
+
output_content = response.choices[0].message.content
|
|
337
|
+
try:
|
|
338
|
+
parsed = output_format.model_validate_json(output_content)
|
|
339
|
+
except Exception as e:
|
|
340
|
+
pdb.set_trace()
|
|
341
|
+
repair_content = repair_json(output_content)
|
|
342
|
+
parsed = output_format.model_validate_json(repair_content)
|
|
318
343
|
|
|
319
344
|
return ChatInvokeCompletion(
|
|
320
345
|
completion=parsed,
|
|
@@ -462,96 +462,6 @@ class BrowserUseTools(Tools, VibeSurfTools):
|
|
|
462
462
|
logger.error(f'Failed to switch tab: {str(e)}')
|
|
463
463
|
return ActionResult(error=f'Failed to switch to tab {params.tab_id or params.url}: {str(e)}')
|
|
464
464
|
|
|
465
|
-
@self.registry.action(
|
|
466
|
-
"""Extract structured, semantic data (e.g. product description, price, all information about XYZ) from the current webpage based on a textual query.
|
|
467
|
-
This tool takes the entire markdown of the page and extracts the query from it.
|
|
468
|
-
Set extract_links=True ONLY if your query requires extracting links/URLs from the page.
|
|
469
|
-
Only use this for specific queries for information retrieval from the page. Don't use this to get interactive elements - the tool does not see HTML elements, only the markdown.
|
|
470
|
-
Note: Extracting from the same page will yield the same results unless more content is loaded (e.g., through scrolling for dynamic content, or new page is loaded) - so one extraction per page state is sufficient. If you want to scrape a listing of many elements always first scroll a lot until the page end to load everything and then call this tool in the end.
|
|
471
|
-
If you called extract_structured_data in the last step and the result was not good (e.g. because of antispam protection), use the current browser state and scrolling to get the information, dont call extract_structured_data again.
|
|
472
|
-
""",
|
|
473
|
-
param_model=ExtractionAction
|
|
474
|
-
)
|
|
475
|
-
async def extract_structured_data(
|
|
476
|
-
params: ExtractionAction,
|
|
477
|
-
browser_session: AgentBrowserSession,
|
|
478
|
-
page_extraction_llm: BaseChatModel,
|
|
479
|
-
file_system: FileSystem,
|
|
480
|
-
):
|
|
481
|
-
try:
|
|
482
|
-
# Use AgentBrowserSession's direct method to get HTML content
|
|
483
|
-
target_id = None
|
|
484
|
-
if params.tab_id:
|
|
485
|
-
target_id = await browser_session.get_target_id_from_tab_id(params.tab_id)
|
|
486
|
-
page_html = await browser_session.get_html_content(target_id)
|
|
487
|
-
|
|
488
|
-
# Simple markdown conversion
|
|
489
|
-
import re
|
|
490
|
-
import markdownify
|
|
491
|
-
|
|
492
|
-
if params.extract_links:
|
|
493
|
-
content = markdownify.markdownify(page_html, heading_style='ATX', bullets='-')
|
|
494
|
-
else:
|
|
495
|
-
content = markdownify.markdownify(page_html, heading_style='ATX', bullets='-', strip=['a'])
|
|
496
|
-
# Remove all markdown links and images, keep only the text
|
|
497
|
-
content = re.sub(r'!\[.*?\]\([^)]*\)', '', content, flags=re.MULTILINE | re.DOTALL) # Remove images
|
|
498
|
-
content = re.sub(
|
|
499
|
-
r'\[([^\]]*)\]\([^)]*\)', r'\1', content, flags=re.MULTILINE | re.DOTALL
|
|
500
|
-
) # Convert [text](url) -> text
|
|
501
|
-
|
|
502
|
-
# Remove weird positioning artifacts
|
|
503
|
-
content = re.sub(r'❓\s*\[\d+\]\s*\w+.*?Position:.*?Size:.*?\n?', '', content,
|
|
504
|
-
flags=re.MULTILINE | re.DOTALL)
|
|
505
|
-
content = re.sub(r'Primary: UNKNOWN\n\nNo specific evidence found', '', content,
|
|
506
|
-
flags=re.MULTILINE | re.DOTALL)
|
|
507
|
-
content = re.sub(r'UNKNOWN CONFIDENCE', '', content, flags=re.MULTILINE | re.DOTALL)
|
|
508
|
-
content = re.sub(r'!\[\]\(\)', '', content, flags=re.MULTILINE | re.DOTALL)
|
|
509
|
-
|
|
510
|
-
# Simple truncation to 30k characters
|
|
511
|
-
if len(content) > 30000:
|
|
512
|
-
content = content[:30000] + '\n\n... [Content truncated at 30k characters] ...'
|
|
513
|
-
|
|
514
|
-
# Simple prompt
|
|
515
|
-
prompt = f"""Extract the requested information from this webpage content.
|
|
516
|
-
|
|
517
|
-
Query: {params.query}
|
|
518
|
-
|
|
519
|
-
Webpage Content:
|
|
520
|
-
{content}
|
|
521
|
-
|
|
522
|
-
Provide the extracted information in a clear, structured format."""
|
|
523
|
-
|
|
524
|
-
from browser_use.llm.messages import UserMessage
|
|
525
|
-
|
|
526
|
-
response = await asyncio.wait_for(
|
|
527
|
-
page_extraction_llm.ainvoke([UserMessage(content=prompt)]),
|
|
528
|
-
timeout=120.0,
|
|
529
|
-
)
|
|
530
|
-
|
|
531
|
-
extracted_content = f'Query: {params.query}\nExtracted Content:\n{response.completion}'
|
|
532
|
-
|
|
533
|
-
# Simple memory handling
|
|
534
|
-
if len(extracted_content) < 1000:
|
|
535
|
-
memory = extracted_content
|
|
536
|
-
include_extracted_content_only_once = False
|
|
537
|
-
else:
|
|
538
|
-
save_result = await file_system.save_extracted_content(extracted_content)
|
|
539
|
-
current_url = await browser_session.get_current_page_url()
|
|
540
|
-
memory = (
|
|
541
|
-
f'Extracted content from {current_url} for query: {params.query}\nContent saved to file system: {save_result}'
|
|
542
|
-
)
|
|
543
|
-
include_extracted_content_only_once = True
|
|
544
|
-
|
|
545
|
-
logger.info(f'📄 {memory}')
|
|
546
|
-
return ActionResult(
|
|
547
|
-
extracted_content=extracted_content,
|
|
548
|
-
include_extracted_content_only_once=include_extracted_content_only_once,
|
|
549
|
-
long_term_memory=memory,
|
|
550
|
-
)
|
|
551
|
-
except Exception as e:
|
|
552
|
-
logger.debug(f'Error extracting content: {e}')
|
|
553
|
-
raise RuntimeError(str(e))
|
|
554
|
-
|
|
555
465
|
@self.registry.action(
|
|
556
466
|
'Take a screenshot of the current page and save it to the file system',
|
|
557
467
|
param_model=NoParamsAction
|
vibe_surf/tools/file_system.py
CHANGED
|
@@ -224,9 +224,9 @@ class CustomFileSystem(FileSystem):
|
|
|
224
224
|
def _is_valid_filename(self, file_name: str) -> bool:
|
|
225
225
|
"""Check if filename matches the required pattern: name.extension"""
|
|
226
226
|
# Build extensions pattern from _file_types
|
|
227
|
-
file_name = os.path.
|
|
227
|
+
file_name = os.path.splitext(file_name)[1]
|
|
228
228
|
extensions = '|'.join(self._file_types.keys())
|
|
229
|
-
pattern = rf'
|
|
229
|
+
pattern = rf'\.({extensions})$'
|
|
230
230
|
return bool(re.match(pattern, file_name))
|
|
231
231
|
|
|
232
232
|
async def append_file(self, full_filename: str, content: str) -> str:
|