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.

@@ -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
- response = await self.get_client().chat.completions.create(
302
- model=self.model,
303
- messages=openai_messages,
304
- response_format=ResponseFormatJSONSchema(json_schema=response_format, type='json_schema'),
305
- **model_params,
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
- parsed = output_format.model_validate_json(response.choices[0].message.content)
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
@@ -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.basename(file_name)
227
+ file_name = os.path.splitext(file_name)[1]
228
228
  extensions = '|'.join(self._file_types.keys())
229
- pattern = rf'^[a-zA-Z0-9_\-]+\.({extensions})$'
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: