vibesurf 0.1.10__py3-none-any.whl → 0.1.11__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.

Files changed (51) hide show
  1. vibe_surf/_version.py +2 -2
  2. vibe_surf/agents/browser_use_agent.py +68 -45
  3. vibe_surf/agents/prompts/report_writer_prompt.py +73 -0
  4. vibe_surf/agents/prompts/vibe_surf_prompt.py +85 -172
  5. vibe_surf/agents/report_writer_agent.py +380 -226
  6. vibe_surf/agents/vibe_surf_agent.py +879 -825
  7. vibe_surf/agents/views.py +130 -0
  8. vibe_surf/backend/api/activity.py +3 -1
  9. vibe_surf/backend/api/browser.py +9 -5
  10. vibe_surf/backend/api/config.py +8 -5
  11. vibe_surf/backend/api/files.py +59 -50
  12. vibe_surf/backend/api/models.py +2 -2
  13. vibe_surf/backend/api/task.py +45 -12
  14. vibe_surf/backend/database/manager.py +24 -18
  15. vibe_surf/backend/database/queries.py +199 -192
  16. vibe_surf/backend/database/schemas.py +1 -1
  17. vibe_surf/backend/main.py +4 -2
  18. vibe_surf/backend/shared_state.py +28 -35
  19. vibe_surf/backend/utils/encryption.py +3 -1
  20. vibe_surf/backend/utils/llm_factory.py +41 -36
  21. vibe_surf/browser/agent_browser_session.py +0 -4
  22. vibe_surf/browser/browser_manager.py +14 -8
  23. vibe_surf/browser/utils.py +5 -3
  24. vibe_surf/browser/watchdogs/dom_watchdog.py +0 -45
  25. vibe_surf/chrome_extension/background.js +4 -0
  26. vibe_surf/chrome_extension/scripts/api-client.js +13 -0
  27. vibe_surf/chrome_extension/scripts/file-manager.js +27 -71
  28. vibe_surf/chrome_extension/scripts/session-manager.js +21 -3
  29. vibe_surf/chrome_extension/scripts/ui-manager.js +831 -48
  30. vibe_surf/chrome_extension/sidepanel.html +21 -4
  31. vibe_surf/chrome_extension/styles/activity.css +365 -5
  32. vibe_surf/chrome_extension/styles/input.css +139 -0
  33. vibe_surf/cli.py +4 -22
  34. vibe_surf/common.py +35 -0
  35. vibe_surf/llm/openai_compatible.py +148 -93
  36. vibe_surf/logger.py +99 -0
  37. vibe_surf/{controller/vibesurf_tools.py → tools/browser_use_tools.py} +233 -219
  38. vibe_surf/tools/file_system.py +415 -0
  39. vibe_surf/{controller → tools}/mcp_client.py +4 -3
  40. vibe_surf/tools/report_writer_tools.py +21 -0
  41. vibe_surf/tools/vibesurf_tools.py +657 -0
  42. vibe_surf/tools/views.py +120 -0
  43. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/METADATA +6 -2
  44. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/RECORD +49 -43
  45. vibe_surf/controller/file_system.py +0 -53
  46. vibe_surf/controller/views.py +0 -37
  47. /vibe_surf/{controller → tools}/__init__.py +0 -0
  48. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/WHEEL +0 -0
  49. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/entry_points.txt +0 -0
  50. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/licenses/LICENSE +0 -0
  51. {vibesurf-0.1.10.dist-info → vibesurf-0.1.11.dist-info}/top_level.txt +0 -0
@@ -31,8 +31,25 @@ from pydantic import BaseModel
31
31
 
32
32
  from browser_use.llm.openai.chat import ChatOpenAI
33
33
  from browser_use.llm.messages import BaseMessage
34
+ from collections.abc import Iterable, Mapping
35
+ from dataclasses import dataclass, field
36
+ from typing import Any, Literal, TypeVar, overload
37
+
38
+ import httpx
39
+ from openai import APIConnectionError, APIStatusError, AsyncOpenAI, RateLimitError
40
+ from openai.types.chat import ChatCompletionContentPartTextParam
41
+ from openai.types.chat.chat_completion import ChatCompletion
42
+ from openai.types.shared.chat_model import ChatModel
43
+ from openai.types.shared_params.reasoning_effort import ReasoningEffort
44
+ from openai.types.shared_params.response_format_json_schema import JSONSchema, ResponseFormatJSONSchema
45
+ from pydantic import BaseModel
46
+
47
+ from browser_use.llm.base import BaseChatModel
48
+ from browser_use.llm.exceptions import ModelProviderError
49
+ from browser_use.llm.messages import BaseMessage
50
+ from browser_use.llm.openai.serializer import OpenAIMessageSerializer
34
51
  from browser_use.llm.schema import SchemaOptimizer
35
- from browser_use.llm.views import ChatInvokeCompletion
52
+ from browser_use.llm.views import ChatInvokeCompletion, ChatInvokeUsage
36
53
 
37
54
  T = TypeVar('T', bound=BaseModel)
38
55
 
@@ -50,11 +67,11 @@ class ChatOpenAICompatible(ChatOpenAI):
50
67
  like "Unable to submit request because one or more response schemas specified
51
68
  other fields alongside any_of".
52
69
  """
53
-
70
+
54
71
  def _is_gemini_model(self) -> bool:
55
72
  """Check if the current model is a Gemini model."""
56
73
  return str(self.model).lower().startswith('gemini')
57
-
74
+
58
75
  def _fix_gemini_schema(self, schema: dict[str, Any]) -> dict[str, Any]:
59
76
  """
60
77
  Convert a Pydantic model to a Gemini-compatible schema.
@@ -64,11 +81,11 @@ class ChatOpenAICompatible(ChatOpenAI):
64
81
 
65
82
  Adapted from browser_use.llm.google.chat.ChatGoogle._fix_gemini_schema
66
83
  """
67
-
84
+
68
85
  # Handle $defs and $ref resolution
69
86
  if '$defs' in schema:
70
87
  defs = schema.pop('$defs')
71
-
88
+
72
89
  def resolve_refs(obj: Any) -> Any:
73
90
  if isinstance(obj, dict):
74
91
  if '$ref' in obj:
@@ -89,9 +106,9 @@ class ChatOpenAICompatible(ChatOpenAI):
89
106
  elif isinstance(obj, list):
90
107
  return [resolve_refs(item) for item in obj]
91
108
  return obj
92
-
109
+
93
110
  schema = resolve_refs(schema)
94
-
111
+
95
112
  # Remove unsupported properties
96
113
  def clean_schema(obj: Any) -> Any:
97
114
  if isinstance(obj, dict):
@@ -102,136 +119,174 @@ class ChatOpenAICompatible(ChatOpenAI):
102
119
  cleaned_value = clean_schema(value)
103
120
  # Handle empty object properties - Gemini doesn't allow empty OBJECT types
104
121
  if (
105
- key == 'properties'
106
- and isinstance(cleaned_value, dict)
107
- and len(cleaned_value) == 0
108
- and isinstance(obj.get('type', ''), str)
109
- and obj.get('type', '').upper() == 'OBJECT'
122
+ key == 'properties'
123
+ and isinstance(cleaned_value, dict)
124
+ and len(cleaned_value) == 0
125
+ and isinstance(obj.get('type', ''), str)
126
+ and obj.get('type', '').upper() == 'OBJECT'
110
127
  ):
111
128
  # Convert empty object to have at least one property
112
129
  cleaned['properties'] = {'_placeholder': {'type': 'string'}}
113
130
  else:
114
131
  cleaned[key] = cleaned_value
115
-
132
+
116
133
  # If this is an object type with empty properties, add a placeholder
117
134
  if (
118
- isinstance(cleaned.get('type', ''), str)
119
- and cleaned.get('type', '').upper() == 'OBJECT'
120
- and 'properties' in cleaned
121
- and isinstance(cleaned['properties'], dict)
122
- and len(cleaned['properties']) == 0
135
+ isinstance(cleaned.get('type', ''), str)
136
+ and cleaned.get('type', '').upper() == 'OBJECT'
137
+ and 'properties' in cleaned
138
+ and isinstance(cleaned['properties'], dict)
139
+ and len(cleaned['properties']) == 0
123
140
  ):
124
141
  cleaned['properties'] = {'_placeholder': {'type': 'string'}}
125
-
142
+
126
143
  return cleaned
127
144
  elif isinstance(obj, list):
128
145
  return [clean_schema(item) for item in obj]
129
146
  return obj
130
-
147
+
131
148
  return clean_schema(schema)
132
-
149
+
133
150
  @overload
134
- async def ainvoke(self, messages: list[BaseMessage], output_format: None = None) -> ChatInvokeCompletion[str]: ...
135
-
136
- @overload
137
- async def ainvoke(self, messages: list[BaseMessage], output_format: type[T]) -> ChatInvokeCompletion[T]: ...
138
-
151
+ async def ainvoke(self, messages: list[BaseMessage], output_format: None = None) -> ChatInvokeCompletion[str]:
152
+ ...
153
+
154
+ @overload
155
+ async def ainvoke(self, messages: list[BaseMessage], output_format: type[T]) -> ChatInvokeCompletion[T]:
156
+ ...
157
+
139
158
  async def ainvoke(
140
- self, messages: list[BaseMessage], output_format: type[T] | None = None
159
+ self, messages: list[BaseMessage], output_format: type[T] | None = None
141
160
  ) -> ChatInvokeCompletion[T] | ChatInvokeCompletion[str]:
142
161
  """
143
162
  Invoke the model with the given messages.
144
-
145
- Automatically applies Gemini schema fixes when using Gemini models.
146
-
163
+
147
164
  Args:
148
165
  messages: List of chat messages
149
166
  output_format: Optional Pydantic model class for structured output
150
-
167
+
151
168
  Returns:
152
169
  Either a string response or an instance of output_format
153
170
  """
154
-
155
171
  # If this is not a Gemini model or no structured output is requested,
156
172
  # use the parent implementation directly
157
173
  if not self._is_gemini_model() or output_format is None:
158
174
  return await super().ainvoke(messages, output_format)
159
-
160
- # For Gemini models with structured output, we need to intercept and fix the schema
161
- from browser_use.llm.openai.serializer import OpenAIMessageSerializer
162
- from browser_use.llm.exceptions import ModelProviderError
163
- from openai.types.shared_params.response_format_json_schema import JSONSchema, ResponseFormatJSONSchema
164
- from typing import Any
165
- from collections.abc import Iterable
166
- from openai.types.chat import ChatCompletionContentPartTextParam
167
-
175
+
168
176
  openai_messages = OpenAIMessageSerializer.serialize_messages(messages)
169
-
177
+
170
178
  try:
171
179
  model_params: dict[str, Any] = {}
172
-
180
+
173
181
  if self.temperature is not None:
174
182
  model_params['temperature'] = self.temperature
175
-
183
+
176
184
  if self.frequency_penalty is not None:
177
185
  model_params['frequency_penalty'] = self.frequency_penalty
178
-
186
+
179
187
  if self.max_completion_tokens is not None:
180
188
  model_params['max_completion_tokens'] = self.max_completion_tokens
181
-
189
+
182
190
  if self.top_p is not None:
183
191
  model_params['top_p'] = self.top_p
184
-
192
+
185
193
  if self.seed is not None:
186
194
  model_params['seed'] = self.seed
187
-
195
+
188
196
  if self.service_tier is not None:
189
197
  model_params['service_tier'] = self.service_tier
190
-
191
- # Create the JSON schema and apply Gemini fixes
192
- original_schema = SchemaOptimizer.create_optimized_json_schema(output_format)
193
- fixed_schema = self._fix_gemini_schema(original_schema)
194
-
195
- response_format: JSONSchema = {
196
- 'name': 'agent_output',
197
- 'strict': True,
198
- 'schema': fixed_schema,
199
- }
200
-
201
- # Add JSON schema to system prompt if requested
202
- if self.add_schema_to_system_prompt and openai_messages and openai_messages[0]['role'] == 'system':
203
- schema_text = f'\n<json_schema>\n{response_format}\n</json_schema>'
204
- if isinstance(openai_messages[0]['content'], str):
205
- openai_messages[0]['content'] += schema_text
206
- elif isinstance(openai_messages[0]['content'], Iterable):
207
- openai_messages[0]['content'] = list(openai_messages[0]['content']) + [
208
- ChatCompletionContentPartTextParam(text=schema_text, type='text')
209
- ]
210
-
211
- # Make the API call with the fixed schema
212
- response = await self.get_client().chat.completions.create(
213
- model=self.model,
214
- messages=openai_messages,
215
- response_format=ResponseFormatJSONSchema(json_schema=response_format, type='json_schema'),
216
- **model_params,
217
- )
218
-
219
- if response.choices[0].message.content is None:
220
- raise ModelProviderError(
221
- message='Failed to parse structured output from model response',
222
- status_code=500,
223
- model=self.name,
198
+
199
+ if self.reasoning_models and any(str(m).lower() in str(self.model).lower() for m in self.reasoning_models):
200
+ model_params['reasoning_effort'] = self.reasoning_effort
201
+ del model_params['temperature']
202
+ del model_params['frequency_penalty']
203
+
204
+ if output_format is None:
205
+ # Return string response
206
+ response = await self.get_client().chat.completions.create(
207
+ model=self.model,
208
+ messages=openai_messages,
209
+ **model_params,
210
+ )
211
+
212
+ usage = self._get_usage(response)
213
+ return ChatInvokeCompletion(
214
+ completion=response.choices[0].message.content or '',
215
+ usage=usage,
216
+ )
217
+
218
+ else:
219
+ original_schema = SchemaOptimizer.create_optimized_json_schema(output_format)
220
+ fixed_schema = self._fix_gemini_schema(original_schema)
221
+ response_format: JSONSchema = {
222
+ 'name': 'agent_output',
223
+ 'strict': True,
224
+ 'schema': fixed_schema,
225
+ }
226
+
227
+ # Add JSON schema to system prompt if requested
228
+ if self.add_schema_to_system_prompt and openai_messages and openai_messages[0]['role'] == 'system':
229
+ schema_text = f'\n<json_schema>\n{response_format}\n</json_schema>'
230
+ if isinstance(openai_messages[0]['content'], str):
231
+ openai_messages[0]['content'] += schema_text
232
+ elif isinstance(openai_messages[0]['content'], Iterable):
233
+ openai_messages[0]['content'] = list(openai_messages[0]['content']) + [
234
+ ChatCompletionContentPartTextParam(text=schema_text, type='text')
235
+ ]
236
+
237
+ # Return structured response
238
+ response = await self.get_client().chat.completions.create(
239
+ model=self.model,
240
+ messages=openai_messages,
241
+ response_format=ResponseFormatJSONSchema(json_schema=response_format, type='json_schema'),
242
+ **model_params,
224
243
  )
225
-
226
- usage = self._get_usage(response)
227
-
228
- parsed = output_format.model_validate_json(response.choices[0].message.content)
229
-
230
- return ChatInvokeCompletion(
231
- completion=parsed,
232
- usage=usage,
244
+
245
+ if response.choices[0].message.content is None:
246
+ raise ModelProviderError(
247
+ message='Failed to parse structured output from model response',
248
+ status_code=500,
249
+ model=self.name,
250
+ )
251
+
252
+ usage = self._get_usage(response)
253
+
254
+ parsed = output_format.model_validate_json(response.choices[0].message.content)
255
+
256
+ return ChatInvokeCompletion(
257
+ completion=parsed,
258
+ usage=usage,
259
+ )
260
+
261
+ except RateLimitError as e:
262
+ error_message = e.response.json().get('error', {})
263
+ error_message = (
264
+ error_message.get('message', 'Unknown model error') if isinstance(error_message,
265
+ dict) else error_message
233
266
  )
234
-
267
+ raise ModelProviderError(
268
+ message=error_message,
269
+ status_code=e.response.status_code,
270
+ model=self.name,
271
+ ) from e
272
+
273
+ except APIConnectionError as e:
274
+ raise ModelProviderError(message=str(e), model=self.name) from e
275
+
276
+ except APIStatusError as e:
277
+ try:
278
+ error_message = e.response.json().get('error', {})
279
+ except Exception:
280
+ error_message = e.response.text
281
+ error_message = (
282
+ error_message.get('message', 'Unknown model error') if isinstance(error_message,
283
+ dict) else error_message
284
+ )
285
+ raise ModelProviderError(
286
+ message=error_message,
287
+ status_code=e.response.status_code,
288
+ model=self.name,
289
+ ) from e
290
+
235
291
  except Exception as e:
236
- # Let parent class handle all exception types
237
- raise ModelProviderError(message=str(e), model=self.name) from e
292
+ raise ModelProviderError(message=str(e), model=self.name) from e
vibe_surf/logger.py ADDED
@@ -0,0 +1,99 @@
1
+ """
2
+ Logger configuration for VibeSurf.
3
+ """
4
+ import logging
5
+ import os
6
+ from datetime import datetime
7
+ from logging.handlers import RotatingFileHandler
8
+
9
+ from .common import get_workspace_dir
10
+
11
+
12
+ def setup_logger(name: str = "vibesurf") -> logging.Logger:
13
+ """
14
+ Set up and configure the logger for VibeSurf.
15
+
16
+ Args:
17
+ name (str): Logger name, defaults to "vibesurf"
18
+
19
+ Returns:
20
+ logging.Logger: Configured logger instance
21
+ """
22
+ # Get debug flag from environment variable
23
+ debug_mode = os.getenv("VIBESURF_DEBUG", "false").lower() in ("true", "1", "yes", "on")
24
+ log_level = logging.DEBUG if debug_mode else logging.INFO
25
+
26
+ # Create logger
27
+ logger = logging.getLogger(name)
28
+ logger.setLevel(log_level)
29
+
30
+ # Avoid adding handlers multiple times
31
+ if logger.handlers:
32
+ return logger
33
+
34
+ # Create formatter with file and line info
35
+ if log_level == logging.DEBUG:
36
+ formatter = logging.Formatter(
37
+ fmt='%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(funcName)s() - %(message)s',
38
+ datefmt='%Y-%m-%d %H:%M:%S'
39
+ )
40
+ else:
41
+ formatter = logging.Formatter(
42
+ fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
43
+ datefmt='%Y-%m-%d %H:%M:%S'
44
+ )
45
+
46
+ # Console handler - log to terminal
47
+ console_handler = logging.StreamHandler()
48
+ console_handler.setLevel(log_level)
49
+ console_handler.setFormatter(formatter)
50
+ logger.addHandler(console_handler)
51
+
52
+ # File handler - log to file
53
+ try:
54
+ workspace_dir = get_workspace_dir()
55
+ logs_dir = os.path.join(workspace_dir, "logs")
56
+ os.makedirs(logs_dir, exist_ok=True)
57
+
58
+ # Create log filename with current date
59
+ current_date = datetime.now().strftime("%Y-%m-%d")
60
+ log_filename = f"log_{current_date}.log"
61
+ log_filepath = os.path.join(logs_dir, log_filename)
62
+
63
+ # Use RotatingFileHandler to manage log file size
64
+ file_handler = RotatingFileHandler(
65
+ log_filepath,
66
+ maxBytes=10 * 1024 * 1024, # 10MB
67
+ backupCount=5,
68
+ encoding='utf-8'
69
+ )
70
+ file_handler.setLevel(log_level)
71
+ file_handler.setFormatter(formatter)
72
+ logger.addHandler(file_handler)
73
+
74
+ logger.info(f"Logger initialized. Log level: {logging.getLevelName(log_level)}")
75
+ logger.info(f"WorkSpace directory: {workspace_dir}")
76
+ logger.info(f"Log file: {log_filepath}")
77
+
78
+ except Exception as e:
79
+ logger.error(f"Failed to setup file logging: {e}")
80
+ logger.warning("Continuing with console logging only")
81
+
82
+ return logger
83
+
84
+
85
+ def get_logger(name: str = "vibesurf") -> logging.Logger:
86
+ """
87
+ Get or create a logger instance.
88
+
89
+ Args:
90
+ name (str): Logger name, defaults to "vibesurf"
91
+
92
+ Returns:
93
+ logging.Logger: Logger instance
94
+ """
95
+ return setup_logger(name)
96
+
97
+
98
+ # Create default logger instance
99
+ default_logger = get_logger()