vibesurf 0.1.23__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 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.23'
32
- __version_tuple__ = version_tuple = (0, 1, 23)
31
+ __version__ = version = '0.1.24'
32
+ __version_tuple__ = version_tuple = (0, 1, 24)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -479,7 +479,7 @@ 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 "skill_search" in action_name or "skill_crawl" in action_name or "skill_summary" in action_name:
482
+ if action_name in ["skill_search", "skill_crawl", "skill_summary", "skill_finance"]:
483
483
  state.current_step = "END"
484
484
  # Format final response
485
485
  final_response = f"{result.extracted_content}"
@@ -234,7 +234,9 @@ async def update_llm_profile(
234
234
 
235
235
  # Return updated profile
236
236
  updated_profile = await LLMProfileQueries.get_profile(db, profile_name)
237
-
237
+ from ..shared_state import current_llm_profile_name
238
+ if current_llm_profile_name != profile_name:
239
+ current_llm_profile_name = None
238
240
  # Use safe extraction to avoid greenlet issues
239
241
  return LLMProfileResponse(**_profile_to_response_dict(updated_profile))
240
242
 
@@ -142,8 +142,9 @@ def create_llm_from_profile(llm_profile) -> BaseChatModel:
142
142
  )
143
143
 
144
144
  elif provider == "deepseek":
145
- return ChatDeepSeek(
145
+ return ChatOpenAICompatible(
146
146
  model=model,
147
+ base_url="https://api.deepseek.com",
147
148
  api_key=api_key,
148
149
  **common_params
149
150
  )
@@ -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,
@@ -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:
@@ -0,0 +1,586 @@
1
+ """
2
+ Comprehensive finance tools using Yahoo Finance API.
3
+ Provides access to stock market data, company financials, and trading information.
4
+ """
5
+ import pdb
6
+ from enum import Enum
7
+ from typing import Dict, List, Any, Optional, Union
8
+ from datetime import datetime, timedelta
9
+ import yfinance as yf
10
+ import pandas as pd
11
+ from vibe_surf.logger import get_logger
12
+
13
+ logger = get_logger(__name__)
14
+
15
+
16
+ class FinanceMethod(Enum):
17
+ """Available Yahoo Finance data methods"""
18
+ # Basic Information
19
+ GET_INFO = "get_info" # Company basic information
20
+ GET_FAST_INFO = "get_fast_info" # Quick stats like current price, volume
21
+
22
+ # Market Data & History
23
+ GET_HISTORY = "get_history" # Historical price and volume data
24
+ GET_ACTIONS = "get_actions" # Dividends and stock splits
25
+ GET_DIVIDENDS = "get_dividends" # Dividend history
26
+ GET_SPLITS = "get_splits" # Stock split history
27
+ GET_CAPITAL_GAINS = "get_capital_gains" # Capital gains distributions
28
+
29
+ # Financial Statements
30
+ GET_FINANCIALS = "get_financials" # Income statement (annual)
31
+ GET_QUARTERLY_FINANCIALS = "get_quarterly_financials" # Income statement (quarterly)
32
+ GET_BALANCE_SHEET = "get_balance_sheet" # Balance sheet (annual)
33
+ GET_QUARTERLY_BALANCE_SHEET = "get_quarterly_balance_sheet" # Balance sheet (quarterly)
34
+ GET_CASHFLOW = "get_cashflow" # Cash flow statement (annual)
35
+ GET_QUARTERLY_CASHFLOW = "get_quarterly_cashflow" # Cash flow statement (quarterly)
36
+
37
+ # Earnings & Analysis
38
+ GET_EARNINGS = "get_earnings" # Historical earnings data
39
+ GET_QUARTERLY_EARNINGS = "get_quarterly_earnings" # Quarterly earnings
40
+ GET_EARNINGS_DATES = "get_earnings_dates" # Upcoming earnings dates
41
+ GET_CALENDAR = "get_calendar" # Earnings calendar
42
+
43
+ # Recommendations & Analysis
44
+ GET_RECOMMENDATIONS = "get_recommendations" # Analyst recommendations
45
+ GET_RECOMMENDATIONS_SUMMARY = "get_recommendations_summary" # Summary of recommendations
46
+ GET_UPGRADES_DOWNGRADES = "get_upgrades_downgrades" # Rating changes
47
+ GET_ANALYSIS = "get_analysis" # Analyst analysis
48
+
49
+ # Ownership & Holdings
50
+ GET_MAJOR_HOLDERS = "get_major_holders" # Major shareholders
51
+ GET_INSTITUTIONAL_HOLDERS = "get_institutional_holders" # Institutional holdings
52
+ GET_MUTUALFUND_HOLDERS = "get_mutualfund_holders" # Mutual fund holdings
53
+ GET_INSIDER_PURCHASES = "get_insider_purchases" # Insider purchases
54
+ GET_INSIDER_TRANSACTIONS = "get_insider_transactions" # Insider transactions
55
+ GET_INSIDER_ROSTER_HOLDERS = "get_insider_roster_holders" # Insider roster
56
+
57
+ # Additional Data
58
+ GET_NEWS = "get_news" # Latest news
59
+ GET_SUSTAINABILITY = "get_sustainability" # ESG scores
60
+ GET_SEC_FILINGS = "get_sec_filings" # SEC filings
61
+ GET_SHARES = "get_shares" # Share count data
62
+
63
+ # Options (if applicable)
64
+ GET_OPTIONS = "get_options" # Option chain data
65
+
66
+
67
+ class FinanceDataRetriever:
68
+ """Main class for retrieving and formatting Yahoo Finance data"""
69
+
70
+ def __init__(self, symbol: str):
71
+ """Initialize with stock symbol"""
72
+ self.symbol = symbol.upper()
73
+ self.ticker = yf.Ticker(self.symbol)
74
+
75
+ def get_finance_data(self, methods: List[str], **kwargs) -> Dict[str, Any]:
76
+ """
77
+ Retrieve finance data using specified methods
78
+
79
+ Args:
80
+ methods: List of method names (FinanceMethod enum values)
81
+ **kwargs: Additional parameters (e.g., period, start_date, end_date, num_news)
82
+
83
+ Returns:
84
+ Dictionary with method names as keys and data as values
85
+ """
86
+ results = {}
87
+
88
+ for method in methods:
89
+ try:
90
+ if hasattr(self, f"_{method}"):
91
+ method_func = getattr(self, f"_{method}")
92
+ results[method] = method_func(**kwargs)
93
+ else:
94
+ results[method] = f"Error: Method {method} not implemented"
95
+ logger.warning(f"Method {method} not implemented for {self.symbol}")
96
+ except Exception as e:
97
+ error_msg = f"Error retrieving {method}: {str(e)}"
98
+ results[method] = error_msg
99
+ logger.error(f"Error retrieving {method} for {self.symbol}: {e}")
100
+
101
+ return results
102
+
103
+ # Basic Information Methods
104
+ def _get_info(self, **kwargs) -> Dict:
105
+ """Get basic company information"""
106
+ return self.ticker.info
107
+
108
+ def _get_fast_info(self, **kwargs) -> Dict:
109
+ """Get quick statistics"""
110
+ try:
111
+ fast_info = self.ticker.fast_info
112
+ return dict(fast_info) if hasattr(fast_info, '__dict__') else fast_info
113
+ except:
114
+ return self.ticker.get_fast_info()
115
+
116
+ # Market Data & History Methods
117
+ def _get_history(self, **kwargs) -> pd.DataFrame:
118
+ """Get historical price and volume data"""
119
+ period = kwargs.get('period', '1y')
120
+ start_date = kwargs.get('start_date')
121
+ end_date = kwargs.get('end_date')
122
+ interval = kwargs.get('interval', '1d')
123
+
124
+ if start_date and end_date:
125
+ return self.ticker.history(start=start_date, end=end_date, interval=interval)
126
+ else:
127
+ return self.ticker.history(period=period, interval=interval)
128
+
129
+ def _get_actions(self, **kwargs) -> pd.DataFrame:
130
+ """Get dividend and stock split history"""
131
+ return self.ticker.actions
132
+
133
+ def _get_dividends(self, **kwargs) -> pd.Series:
134
+ """Get dividend history"""
135
+ return self.ticker.dividends
136
+
137
+ def _get_splits(self, **kwargs) -> pd.Series:
138
+ """Get stock split history"""
139
+ return self.ticker.splits
140
+
141
+ def _get_capital_gains(self, **kwargs) -> pd.Series:
142
+ """Get capital gains distributions"""
143
+ return self.ticker.capital_gains
144
+
145
+ # Financial Statements Methods
146
+ def _get_financials(self, **kwargs) -> pd.DataFrame:
147
+ """Get annual income statement"""
148
+ return self.ticker.financials
149
+
150
+ def _get_quarterly_financials(self, **kwargs) -> pd.DataFrame:
151
+ """Get quarterly income statement"""
152
+ return self.ticker.quarterly_financials
153
+
154
+ def _get_balance_sheet(self, **kwargs) -> pd.DataFrame:
155
+ """Get annual balance sheet"""
156
+ return self.ticker.balance_sheet
157
+
158
+ def _get_quarterly_balance_sheet(self, **kwargs) -> pd.DataFrame:
159
+ """Get quarterly balance sheet"""
160
+ return self.ticker.quarterly_balance_sheet
161
+
162
+ def _get_cashflow(self, **kwargs) -> pd.DataFrame:
163
+ """Get annual cash flow statement"""
164
+ return self.ticker.cashflow
165
+
166
+ def _get_quarterly_cashflow(self, **kwargs) -> pd.DataFrame:
167
+ """Get quarterly cash flow statement"""
168
+ return self.ticker.quarterly_cashflow
169
+
170
+ # Earnings & Analysis Methods
171
+ def _get_earnings(self, **kwargs) -> pd.DataFrame:
172
+ """Get historical earnings data"""
173
+ return self.ticker.earnings
174
+
175
+ def _get_quarterly_earnings(self, **kwargs) -> pd.DataFrame:
176
+ """Get quarterly earnings data"""
177
+ return self.ticker.quarterly_earnings
178
+
179
+ def _get_earnings_dates(self, **kwargs) -> pd.DataFrame:
180
+ """Get earnings dates and estimates"""
181
+ return self.ticker.earnings_dates
182
+
183
+ def _get_calendar(self, **kwargs) -> Dict:
184
+ """Get earnings calendar"""
185
+ return self.ticker.calendar
186
+
187
+ # Recommendations & Analysis Methods
188
+ def _get_recommendations(self, **kwargs) -> pd.DataFrame:
189
+ """Get analyst recommendations history"""
190
+ return self.ticker.recommendations
191
+
192
+ def _get_recommendations_summary(self, **kwargs) -> pd.DataFrame:
193
+ """Get summary of analyst recommendations"""
194
+ return self.ticker.recommendations_summary
195
+
196
+ def _get_upgrades_downgrades(self, **kwargs) -> pd.DataFrame:
197
+ """Get analyst upgrades and downgrades"""
198
+ return self.ticker.upgrades_downgrades
199
+
200
+ def _get_analysis(self, **kwargs) -> pd.DataFrame:
201
+ """Get analyst analysis"""
202
+ return getattr(self.ticker, 'analysis', pd.DataFrame())
203
+
204
+ # Ownership & Holdings Methods
205
+ def _get_major_holders(self, **kwargs) -> pd.DataFrame:
206
+ """Get major shareholders"""
207
+ return self.ticker.major_holders
208
+
209
+ def _get_institutional_holders(self, **kwargs) -> pd.DataFrame:
210
+ """Get institutional holdings"""
211
+ return self.ticker.institutional_holders
212
+
213
+ def _get_mutualfund_holders(self, **kwargs) -> pd.DataFrame:
214
+ """Get mutual fund holdings"""
215
+ return self.ticker.mutualfund_holders
216
+
217
+ def _get_insider_purchases(self, **kwargs) -> pd.DataFrame:
218
+ """Get insider purchases"""
219
+ return getattr(self.ticker, 'insider_purchases', pd.DataFrame())
220
+
221
+ def _get_insider_transactions(self, **kwargs) -> pd.DataFrame:
222
+ """Get insider transactions"""
223
+ return getattr(self.ticker, 'insider_transactions', pd.DataFrame())
224
+
225
+ def _get_insider_roster_holders(self, **kwargs) -> pd.DataFrame:
226
+ """Get insider roster"""
227
+ return getattr(self.ticker, 'insider_roster_holders', pd.DataFrame())
228
+
229
+ # Additional Data Methods
230
+ def _get_news(self, **kwargs) -> List[Dict]:
231
+ """Get latest news"""
232
+ num_news = kwargs.get('num_news', 5)
233
+ news = self.ticker.news
234
+ return news[:num_news] if news else []
235
+
236
+ def _get_sustainability(self, **kwargs) -> pd.DataFrame:
237
+ """Get ESG sustainability data"""
238
+ return getattr(self.ticker, 'sustainability', pd.DataFrame())
239
+
240
+ def _get_sec_filings(self, **kwargs) -> pd.DataFrame:
241
+ """Get SEC filings"""
242
+ return getattr(self.ticker, 'sec_filings', pd.DataFrame())
243
+
244
+ def _get_shares(self, **kwargs) -> pd.DataFrame:
245
+ """Get share count data"""
246
+ return getattr(self.ticker, 'shares', pd.DataFrame())
247
+
248
+ def _get_options(self, **kwargs) -> Dict:
249
+ """Get options data"""
250
+ try:
251
+ option_dates = self.ticker.options
252
+ if option_dates:
253
+ # Get the first available expiration date
254
+ first_expiry = option_dates[0]
255
+ opt_chain = self.ticker.option_chain(first_expiry)
256
+ return {
257
+ 'expiration_dates': list(option_dates),
258
+ 'calls': opt_chain.calls,
259
+ 'puts': opt_chain.puts,
260
+ 'selected_expiry': first_expiry
261
+ }
262
+ return {'expiration_dates': [], 'calls': pd.DataFrame(), 'puts': pd.DataFrame()}
263
+ except:
264
+ return {'error': 'Options data not available for this ticker'}
265
+
266
+
267
+ class FinanceMarkdownFormatter:
268
+ """Formats finance data into markdown"""
269
+
270
+ @staticmethod
271
+ def format_finance_data(symbol: str, results: Dict[str, Any], methods: List[str]) -> str:
272
+ """Format all finance data as markdown"""
273
+ markdown = f"# 💹 Financial Data for {symbol.upper()}\n\n"
274
+
275
+ for method in methods:
276
+ data = results.get(method)
277
+
278
+ if isinstance(data, str) and data.startswith('Error'):
279
+ markdown += f"## ❌ {method.replace('_', ' ').title()}\n{data}\n\n"
280
+ continue
281
+
282
+ markdown += f"## 📊 {method.replace('_', ' ').title()}\n\n"
283
+
284
+ # Route to appropriate formatter
285
+ formatter_method = f"_format_{method}"
286
+ if hasattr(FinanceMarkdownFormatter, formatter_method):
287
+ formatter = getattr(FinanceMarkdownFormatter, formatter_method)
288
+ markdown += formatter(data)
289
+ else:
290
+ # Generic formatter for unhandled methods
291
+ markdown += FinanceMarkdownFormatter._format_generic(data)
292
+
293
+ markdown += "\n\n"
294
+
295
+ return markdown.strip()
296
+
297
+ @staticmethod
298
+ def _format_generic(data: Any) -> str:
299
+ """Generic formatter for any data type"""
300
+ if data is None or (hasattr(data, 'empty') and data.empty):
301
+ return "No data available.\n"
302
+
303
+ if isinstance(data, pd.DataFrame):
304
+ if len(data) == 0:
305
+ return "No data available.\n"
306
+ return f"```\n{data.to_string()}\n```\n"
307
+ elif isinstance(data, pd.Series):
308
+ if len(data) == 0:
309
+ return "No data available.\n"
310
+ return f"```\n{data.to_string()}\n```\n"
311
+ elif isinstance(data, (list, dict)):
312
+ import json
313
+ return f"```json\n{json.dumps(data, indent=2, default=str)}\n```\n"
314
+ else:
315
+ return f"```\n{str(data)}\n```\n"
316
+
317
+ @staticmethod
318
+ def _format_get_info(info: Dict) -> str:
319
+ """Format company info as markdown"""
320
+ if not info:
321
+ return "No company information available.\n"
322
+
323
+ markdown = ""
324
+
325
+ # Basic company info
326
+ if 'longName' in info:
327
+ markdown += f"**Company Name:** {info['longName']}\n"
328
+ if 'sector' in info:
329
+ markdown += f"**Sector:** {info['sector']}\n"
330
+ if 'industry' in info:
331
+ markdown += f"**Industry:** {info['industry']}\n"
332
+ if 'website' in info:
333
+ markdown += f"**Website:** {info['website']}\n"
334
+ if 'country' in info:
335
+ markdown += f"**Country:** {info['country']}\n"
336
+
337
+ markdown += "\n### 💰 Financial Metrics\n"
338
+
339
+ # Financial metrics
340
+ if 'marketCap' in info and info['marketCap']:
341
+ markdown += f"**Market Cap:** ${info['marketCap']:,.0f}\n"
342
+ if 'enterpriseValue' in info and info['enterpriseValue']:
343
+ markdown += f"**Enterprise Value:** ${info['enterpriseValue']:,.0f}\n"
344
+ if 'totalRevenue' in info and info['totalRevenue']:
345
+ markdown += f"**Total Revenue:** ${info['totalRevenue']:,.0f}\n"
346
+ if 'grossMargins' in info and info['grossMargins']:
347
+ markdown += f"**Gross Margin:** {info['grossMargins']:.2%}\n"
348
+ if 'profitMargins' in info and info['profitMargins']:
349
+ markdown += f"**Profit Margin:** {info['profitMargins']:.2%}\n"
350
+
351
+ markdown += "\n### 📈 Stock Price Info\n"
352
+
353
+ # Stock price info
354
+ if 'currentPrice' in info and info['currentPrice']:
355
+ markdown += f"**Current Price:** ${info['currentPrice']:.2f}\n"
356
+ if 'previousClose' in info and info['previousClose']:
357
+ markdown += f"**Previous Close:** ${info['previousClose']:.2f}\n"
358
+ if 'fiftyTwoWeekHigh' in info and info['fiftyTwoWeekHigh']:
359
+ markdown += f"**52 Week High:** ${info['fiftyTwoWeekHigh']:.2f}\n"
360
+ if 'fiftyTwoWeekLow' in info and info['fiftyTwoWeekLow']:
361
+ markdown += f"**52 Week Low:** ${info['fiftyTwoWeekLow']:.2f}\n"
362
+ if 'dividendYield' in info and info['dividendYield']:
363
+ markdown += f"**Dividend Yield:** {info['dividendYield']:.2%}\n"
364
+
365
+ # Business summary
366
+ if 'longBusinessSummary' in info:
367
+ summary = info['longBusinessSummary'][:500]
368
+ if len(info['longBusinessSummary']) > 500:
369
+ summary += "..."
370
+ markdown += f"\n### 📋 Business Summary\n{summary}\n"
371
+
372
+ return markdown
373
+
374
+ @staticmethod
375
+ def _format_get_fast_info(fast_info) -> str:
376
+ """Format fast info as markdown"""
377
+ if not fast_info:
378
+ return "No fast info available.\n"
379
+
380
+ markdown = ""
381
+
382
+ # Convert to dict if needed
383
+ if hasattr(fast_info, '__dict__'):
384
+ data = fast_info.__dict__
385
+ elif isinstance(fast_info, dict):
386
+ data = fast_info
387
+ else:
388
+ return f"Fast info data: {str(fast_info)}\n"
389
+
390
+ # Format key metrics
391
+ for key, value in data.items():
392
+ if value is not None:
393
+ key_formatted = key.replace('_', ' ').title()
394
+ if isinstance(value, (int, float)):
395
+ if 'price' in key.lower() or 'value' in key.lower():
396
+ markdown += f"**{key_formatted}:** ${value:,.2f}\n"
397
+ elif 'volume' in key.lower():
398
+ markdown += f"**{key_formatted}:** {value:,}\n"
399
+ else:
400
+ markdown += f"**{key_formatted}:** {value}\n"
401
+ else:
402
+ markdown += f"**{key_formatted}:** {value}\n"
403
+
404
+ return markdown
405
+
406
+ @staticmethod
407
+ def _format_get_history(history: pd.DataFrame) -> str:
408
+ """Format historical data as markdown"""
409
+ if history.empty:
410
+ return "No historical data available.\n"
411
+
412
+ markdown = f"**Period:** {history.index.min().strftime('%Y-%m-%d')} to {history.index.max().strftime('%Y-%m-%d')}\n"
413
+ markdown += f"**Total Records:** {len(history)}\n\n"
414
+
415
+ # Determine how much data to show based on total records
416
+ total_records = len(history)
417
+ if total_records <= 30:
418
+ # Show all data if 30 records or less
419
+ display_data = history
420
+ markdown += f"### 📈 Historical Data (All {total_records} Records)\n\n"
421
+ else:
422
+ # Show recent 30 records for larger datasets
423
+ display_data = history.tail(30)
424
+ markdown += f"### 📈 Recent Data (Last 30 Records)\n\n"
425
+
426
+ markdown += "| Date | Open | High | Low | Close | Volume |\n"
427
+ markdown += "|------|------|------|-----|-------|--------|\n"
428
+
429
+ for date, row in display_data.iterrows():
430
+ markdown += f"| {date.strftime('%Y-%m-%d')} | ${row['Open']:.2f} | ${row['High']:.2f} | ${row['Low']:.2f} | ${row['Close']:.2f} | {row['Volume']:,} |\n"
431
+
432
+ # Summary statistics
433
+ markdown += "\n### 📊 Summary Statistics\n"
434
+ markdown += f"**Highest Price:** ${history['High'].max():.2f}\n"
435
+ markdown += f"**Lowest Price:** ${history['Low'].min():.2f}\n"
436
+ markdown += f"**Average Volume:** {history['Volume'].mean():,.0f}\n"
437
+ markdown += f"**Total Volume:** {history['Volume'].sum():,}\n"
438
+
439
+ return markdown
440
+
441
+ @staticmethod
442
+ def _format_get_news(news: List[Dict]) -> str:
443
+ """Format news data as markdown"""
444
+ if not news:
445
+ return "No news available.\n"
446
+
447
+ markdown = f"**Total News Articles:** {len(news)}\n\n"
448
+ pdb.set_trace()
449
+ for i, article in enumerate(news, 1):
450
+ if isinstance(article, dict):
451
+ # Try different possible field names for title
452
+ title = (article.get('title') or
453
+ article.get('headline') or
454
+ article.get('summary') or
455
+ 'No title available')
456
+
457
+ # Try different possible field names for link/URL
458
+ link = (article.get('link') or
459
+ article.get('url') or
460
+ article.get('guid') or '')
461
+
462
+ # Try different possible field names for publisher
463
+ publisher = (article.get('publisher') or
464
+ article.get('source') or
465
+ article.get('author') or
466
+ 'Unknown')
467
+
468
+ # Try different possible field names for timestamp
469
+ publish_time = (article.get('providerPublishTime') or
470
+ article.get('timestamp') or
471
+ article.get('pubDate') or
472
+ article.get('published') or '')
473
+
474
+ markdown += f"### {i}. {title}\n"
475
+ markdown += f"**Publisher:** {publisher}\n"
476
+
477
+ if publish_time:
478
+ try:
479
+ # Handle different timestamp formats
480
+ if isinstance(publish_time, (int, float)):
481
+ dt = datetime.fromtimestamp(publish_time)
482
+ markdown += f"**Published:** {dt.strftime('%Y-%m-%d %H:%M')}\n"
483
+ elif isinstance(publish_time, str):
484
+ # Try to parse string timestamp
485
+ try:
486
+ publish_time_int = int(float(publish_time))
487
+ dt = datetime.fromtimestamp(publish_time_int)
488
+ markdown += f"**Published:** {dt.strftime('%Y-%m-%d %H:%M')}\n"
489
+ except:
490
+ markdown += f"**Published:** {publish_time}\n"
491
+ except Exception as e:
492
+ # If timestamp parsing fails, show raw value
493
+ markdown += f"**Published:** {publish_time}\n"
494
+
495
+ if link:
496
+ markdown += f"**Link:** {link}\n"
497
+
498
+ # Add summary or description if available
499
+ summary = (article.get('summary') or
500
+ article.get('description') or
501
+ article.get('snippet') or '')
502
+ if summary and summary != title:
503
+ # Limit summary length
504
+ if len(summary) > 200:
505
+ summary = summary[:200] + "..."
506
+ markdown += f"**Summary:** {summary}\n"
507
+
508
+ markdown += "\n"
509
+
510
+ return markdown
511
+
512
+ @staticmethod
513
+ def _format_get_dividends(dividends: pd.Series) -> str:
514
+ """Format dividend data as markdown"""
515
+ if dividends.empty:
516
+ return "No dividend data available.\n"
517
+
518
+ markdown = f"**Total Dividends Recorded:** {len(dividends)}\n"
519
+ markdown += f"**Date Range:** {dividends.index.min().strftime('%Y-%m-%d')} to {dividends.index.max().strftime('%Y-%m-%d')}\n\n"
520
+
521
+ # Recent dividends (last 10)
522
+ recent_dividends = dividends.tail(10)
523
+ markdown += "### 💰 Recent Dividends\n\n"
524
+ markdown += "| Date | Dividend Amount |\n"
525
+ markdown += "|------|----------------|\n"
526
+
527
+ for date, amount in recent_dividends.items():
528
+ markdown += f"| {date.strftime('%Y-%m-%d')} | ${amount:.4f} |\n"
529
+
530
+ # Summary
531
+ markdown += f"\n**Total Dividends Paid:** ${dividends.sum():.4f}\n"
532
+ markdown += f"**Average Dividend:** ${dividends.mean():.4f}\n"
533
+ if len(dividends) > 1:
534
+ yearly_frequency = len(dividends) / ((dividends.index.max() - dividends.index.min()).days / 365.25)
535
+ markdown += f"**Estimated Annual Frequency:** {yearly_frequency:.1f} times per year\n"
536
+
537
+ return markdown
538
+
539
+ @staticmethod
540
+ def _format_get_recommendations(recommendations: pd.DataFrame) -> str:
541
+ """Format recommendations as markdown"""
542
+ if recommendations.empty:
543
+ return "No recommendations available.\n"
544
+
545
+ markdown = f"**Total Recommendations:** {len(recommendations)}\n\n"
546
+
547
+ # Recent recommendations (last 15)
548
+ recent_recs = recommendations.tail(15)
549
+ markdown += "### 📊 Recent Analyst Recommendations\n\n"
550
+ markdown += "| Date | Firm | To Grade | From Grade | Action |\n"
551
+ markdown += "|------|------|----------|------------|--------|\n"
552
+
553
+ for _, rec in recent_recs.iterrows():
554
+ date = rec.get('Date', 'N/A')
555
+ firm = rec.get('Firm', 'N/A')
556
+ to_grade = rec.get('To Grade', 'N/A')
557
+ from_grade = rec.get('From Grade', 'N/A')
558
+ action = rec.get('Action', 'N/A')
559
+
560
+ markdown += f"| {date} | {firm} | {to_grade} | {from_grade} | {action} |\n"
561
+
562
+ return markdown
563
+
564
+ @staticmethod
565
+ def _format_get_earnings(earnings: pd.DataFrame) -> str:
566
+ """Format earnings as markdown"""
567
+ if earnings.empty:
568
+ return "No earnings data available.\n"
569
+
570
+ markdown = "### 💼 Annual Earnings History\n\n"
571
+ markdown += "| Year | Revenue | Earnings |\n"
572
+ markdown += "|------|---------|----------|\n"
573
+
574
+ for year, row in earnings.iterrows():
575
+ revenue = row.get('Revenue', 'N/A')
576
+ earnings_val = row.get('Earnings', 'N/A')
577
+
578
+ # Format numbers if they're numeric
579
+ if isinstance(revenue, (int, float)):
580
+ revenue = f"${revenue:,.0f}"
581
+ if isinstance(earnings_val, (int, float)):
582
+ earnings_val = f"${earnings_val:,.0f}"
583
+
584
+ markdown += f"| {year} | {revenue} | {earnings_val} |\n"
585
+
586
+ return markdown
@@ -2,11 +2,12 @@ from browser_use.tools.registry.service import Registry
2
2
  from vibe_surf.tools.vibesurf_tools import VibeSurfTools
3
3
  from vibe_surf.tools.file_system import CustomFileSystem
4
4
  from browser_use.tools.views import NoParamsAction
5
+ from vibe_surf.tools.vibesurf_registry import VibeSurfRegistry
5
6
 
6
7
 
7
8
  class ReportWriterTools(VibeSurfTools):
8
9
  def __init__(self, exclude_actions: list[str] = []):
9
- self.registry = Registry(exclude_actions)
10
+ self.registry = VibeSurfRegistry(exclude_actions)
10
11
  self._register_file_actions()
11
12
  self._register_done_action()
12
13
 
@@ -8,6 +8,8 @@ import json
8
8
  import enum
9
9
  import base64
10
10
  import mimetypes
11
+ import yfinance as yf
12
+ import pprint
11
13
  from json_repair import repair_json
12
14
  from datetime import datetime
13
15
  from typing import Optional, Type, Callable, Dict, Any, Union, Awaitable, TypeVar
@@ -29,7 +31,8 @@ from browser_use.tools.views import NoParamsAction
29
31
  from vibe_surf.browser.agent_browser_session import AgentBrowserSession
30
32
  from vibe_surf.tools.views import HoverAction, ExtractionAction, FileExtractionAction, BrowserUseAgentExecution, \
31
33
  ReportWriterTask, TodoGenerateAction, TodoModifyAction, VibeSurfDoneAction, SkillSearchAction, SkillCrawlAction, \
32
- SkillSummaryAction, SkillTakeScreenshotAction, SkillDeepResearchAction, SkillCodeAction
34
+ SkillSummaryAction, SkillTakeScreenshotAction, SkillDeepResearchAction, SkillCodeAction, SkillFinanceAction
35
+ from vibe_surf.tools.finance_tools import FinanceDataRetriever, FinanceMarkdownFormatter, FinanceMethod
33
36
  from vibe_surf.tools.mcp_client import CustomMCPClient
34
37
  from vibe_surf.tools.file_system import CustomFileSystem
35
38
  from vibe_surf.browser.browser_manager import BrowserManager
@@ -818,6 +821,92 @@ Please generate alternative JavaScript code that avoids this system error:"""
818
821
  include_extracted_content_only_once=True,
819
822
  )
820
823
 
824
+ @self.registry.action(
825
+ 'Skill: Get comprehensive financial data for stocks - retrieve company information, historical prices, news, earnings, dividends, analyst recommendations and other financial data using Yahoo Finance. Available methods include: get_info (company info), get_history (price history), get_news (latest news), get_dividends (dividend history), get_earnings (earnings data), get_recommendations (analyst recommendations), get_balance_sheet (balance sheet data), get_income_stmt (income statement), get_cashflow (cash flow statement), get_fast_info (quick stats), get_institutional_holders (institutional ownership), get_major_holders (major shareholders), get_sustainability (ESG data), get_upgrades_downgrades (analyst upgrades/downgrades), and more. If no methods specified, defaults to get_info.',
826
+ param_model=SkillFinanceAction,
827
+ )
828
+ async def skill_finance(
829
+ params: SkillFinanceAction,
830
+ ):
831
+ """
832
+ Skill: Get comprehensive financial data using Yahoo Finance
833
+
834
+ Available methods include:
835
+ - get_info: Company information including sector, industry, market cap, business summary
836
+ - get_history: Historical stock prices and volume data over time periods
837
+ - get_news: Latest news articles about the company
838
+ - get_dividends: Historical dividend payments and yield data
839
+ - get_earnings: Quarterly and annual earnings data and growth trends
840
+ - get_recommendations: Analyst recommendations, price targets, and ratings
841
+ - get_balance_sheet: Company balance sheet data (assets, liabilities, equity)
842
+ - get_income_stmt: Income statement data (revenue, expenses, profit)
843
+ - get_cashflow: Cash flow statement data (operating, investing, financing)
844
+ - get_fast_info: Quick statistics like current price, volume, market cap
845
+ - get_institutional_holders: Institutional ownership and holdings data
846
+ - get_major_holders: Major shareholders and insider ownership percentages
847
+ - get_sustainability: ESG (Environmental, Social, Governance) scores and data
848
+ - get_upgrades_downgrades: Recent analyst upgrades and downgrades
849
+ - get_splits: Historical stock splits and stock split dates
850
+ - get_actions: Corporate actions including dividends and splits
851
+ - get_sec_filings: Recent SEC filings and regulatory documents
852
+ - get_calendar: Upcoming earnings dates and events
853
+ - get_mutualfund_holders: Mutual fund ownership data
854
+ - get_insider_purchases: Recent insider buying activity
855
+ - get_insider_transactions: All insider trading transactions
856
+ - get_shares: Outstanding shares and float data
857
+ """
858
+ try:
859
+ # Default to get_info if no methods specified
860
+ methods = params.methods if params.methods else [FinanceMethod.GET_INFO]
861
+
862
+ # Convert string methods to FinanceMethod enum if needed
863
+ if methods and isinstance(methods[0], str):
864
+ try:
865
+ methods = [FinanceMethod(method) for method in methods]
866
+ except ValueError as e:
867
+ available_methods = [method.value for method in FinanceMethod]
868
+ return ActionResult(
869
+ error=f'Invalid method in {methods}. Available methods: {available_methods}'
870
+ )
871
+
872
+ # Create data retriever with symbol
873
+ retriever = FinanceDataRetriever(params.symbol)
874
+
875
+ # Convert FinanceMethod enum values to strings for the retriever
876
+ method_strings = [method.value for method in methods]
877
+
878
+ # Retrieve financial data
879
+ financial_data = retriever.get_finance_data(
880
+ methods=method_strings,
881
+ period=getattr(params, 'period', '1y'),
882
+ start_date=getattr(params, 'start_date', None),
883
+ end_date=getattr(params, 'end_date', None),
884
+ interval=getattr(params, 'interval', '1d'),
885
+ num_news=getattr(params, 'num_news', 5)
886
+ )
887
+
888
+ # Format as markdown using the static method
889
+ markdown_content = FinanceMarkdownFormatter.format_finance_data(
890
+ symbol=params.symbol,
891
+ results=financial_data,
892
+ methods=method_strings
893
+ )
894
+
895
+ method_names = [method.value for method in methods]
896
+ logger.info(f'💹 Comprehensive finance data retrieved for {params.symbol} with methods: {method_names}')
897
+
898
+ return ActionResult(
899
+ extracted_content=markdown_content,
900
+ include_extracted_content_only_once=True,
901
+ long_term_memory=f'Retrieved comprehensive financial data for {params.symbol} using methods: {", ".join(method_names)}',
902
+ )
903
+
904
+ except Exception as e:
905
+ error_msg = f'❌ Failed to retrieve financial data for {params.symbol}: {str(e)}'
906
+ logger.error(error_msg)
907
+ return ActionResult(error=error_msg)
908
+
909
+
821
910
  async def _perform_google_search(self, browser_session, query: str, llm: BaseChatModel):
822
911
  """Helper method to perform Google search and extract top 5 results"""
823
912
  try:
@@ -1379,7 +1468,7 @@ You will be given a query and the markdown of a webpage that has been filtered t
1379
1468
  async def write_file(
1380
1469
  file_path: str,
1381
1470
  content: str,
1382
- file_system: FileSystem,
1471
+ file_system: CustomFileSystem,
1383
1472
  append: bool = False,
1384
1473
  trailing_newline: bool = True,
1385
1474
  leading_newline: bool = False,
vibe_surf/tools/views.py CHANGED
@@ -178,3 +178,36 @@ class SkillCodeAction(BaseModel):
178
178
  max_length=4,
179
179
  description='Optional 4 character Tab ID to execute code on specific tab',
180
180
  )
181
+
182
+
183
+ class SkillFinanceAction(BaseModel):
184
+ """Parameters for skill_finance action"""
185
+ symbol: str = Field(
186
+ description='Stock symbol to retrieve financial data for (e.g., AAPL, GOOG, TSLA)',
187
+ )
188
+ methods: list[str] | None = Field(
189
+ default=None,
190
+ description='List of finance methods to retrieve. Common methods: get_info (basic company info), get_history (stock price history), get_news (latest news), get_dividends (dividend history), get_earnings (earnings data), get_fast_info (quick stats), get_recommendations (analyst recommendations), get_financials (income statement), get_balance_sheet (balance sheet), get_cashflow (cash flow). If empty, defaults to get_info. Full list available in FinanceMethod enum.',
191
+ )
192
+ period: str = Field(
193
+ default='1y',
194
+ description='Time period for historical data (1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max)',
195
+ )
196
+ start_date: str | None = Field(
197
+ default=None,
198
+ description='Start date for historical data (YYYY-MM-DD format). Use with end_date instead of period.',
199
+ )
200
+ end_date: str | None = Field(
201
+ default=None,
202
+ description='End date for historical data (YYYY-MM-DD format). Use with start_date instead of period.',
203
+ )
204
+ interval: str = Field(
205
+ default='1d',
206
+ description='Data interval for historical data (1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo)',
207
+ )
208
+ num_news: int = Field(
209
+ default=5,
210
+ description='Number of news articles to retrieve when get_news method is selected',
211
+ ge=1,
212
+ le=20,
213
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vibesurf
3
- Version: 0.1.23
3
+ Version: 0.1.24
4
4
  Summary: VibeSurf: A powerful browser assistant for vibe surfing
5
5
  Author: Shao Warm
6
6
  License: Apache-2.0
@@ -43,6 +43,7 @@ Requires-Dist: nanoid>=2.0.0
43
43
  Requires-Dist: markdownify>=1.2.0
44
44
  Requires-Dist: pathvalidate>=3.3.1
45
45
  Requires-Dist: dashscope>=1.24.5
46
+ Requires-Dist: yfinance>=0.2.66
46
47
  Dynamic: license-file
47
48
 
48
49
  # VibeSurf: A powerful browser assistant for vibe surfing
@@ -1,12 +1,12 @@
1
1
  vibe_surf/__init__.py,sha256=WtduuMFGauMD_9dpk4fnRnLTAP6ka9Lfu0feAFNzLfo,339
2
- vibe_surf/_version.py,sha256=eZj6tqY-zTtH5r_8y6a4Vovz6LQ_hDHSesTIiwuyahQ,706
2
+ vibe_surf/_version.py,sha256=IV4a2R7tlzuACf6FAyPEbprLKNroeE-n_UPSKi1QJSc,706
3
3
  vibe_surf/cli.py,sha256=pbep2dBeQqralZ8AggkH4h2nayBarbdN8lhZxo35gNU,16689
4
4
  vibe_surf/common.py,sha256=_WWMxen5wFwzUjEShn3yDVC1OBFUiJ6Vccadi6tuG6w,1215
5
5
  vibe_surf/logger.py,sha256=k53MFA96QX6t9OfcOf1Zws8PP0OOqjVJfhUD3Do9lKw,3043
6
6
  vibe_surf/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  vibe_surf/agents/browser_use_agent.py,sha256=jeUYV7yk6vyycw6liju_597GdjB3CW_B2wEhn2F0ekk,45957
8
8
  vibe_surf/agents/report_writer_agent.py,sha256=pCF2k6VLyO-sSviGBqqIyVD3SLqaZtSqiW3kvNfPY1I,20967
9
- vibe_surf/agents/vibe_surf_agent.py,sha256=V1H7BquR0zIderpBw5ArY8d_6oLphSAxoiE6_ABTJyk,74301
9
+ vibe_surf/agents/vibe_surf_agent.py,sha256=sTUO4xAiznr7RRzdrRYzXENos9XovicZw8ow2UuJsyI,74286
10
10
  vibe_surf/agents/views.py,sha256=yHjNJloa-aofVTGyuRy08tBYP_Y3XLqt1DUWOUmHRng,4825
11
11
  vibe_surf/agents/prompts/__init__.py,sha256=l4ieA0D8kLJthyNN85FKLNe4ExBa3stY3l-aImLDRD0,36
12
12
  vibe_surf/agents/prompts/report_writer_prompt.py,sha256=sZE8MUT1CDLmRzbnbEQzAvTwJjpITgh2Q8g1_eXmkzE,4454
@@ -20,7 +20,7 @@ vibe_surf/backend/api/__init__.py,sha256=XxF1jUOORpLYCfFuPrrnUGRnOrr6ClH0_MNPU-4
20
20
  vibe_surf/backend/api/activity.py,sha256=_cnHusqolt5Hf3KdAf6FK-3sBc-TSaadmb5dJxGI57A,9398
21
21
  vibe_surf/backend/api/agent.py,sha256=ISsG3FUIYoUCGcoQAfV3T6mtJSKHxC809p4bqjzjqlU,1199
22
22
  vibe_surf/backend/api/browser.py,sha256=NXedyZG3NIVRIx5O7d9mHwVWX-Q4_KsX5mSgfKt8UEA,2122
23
- vibe_surf/backend/api/config.py,sha256=EwzxYvC6HlaVo2OFWjtBmBMjX4eW2q8hp7l2LO2GZV0,27124
23
+ vibe_surf/backend/api/config.py,sha256=vKY6ZnKZeazQP9qqUEiQvP9HoPtJbAzETORuPWZomGw,27272
24
24
  vibe_surf/backend/api/files.py,sha256=kJMG9MWECKXwGh64Q6xvAzNjeZGcLhIEnn65HiMZHKE,11762
25
25
  vibe_surf/backend/api/models.py,sha256=n_bu8vavvO8bIKA1WUAbaGPFeZKeamMJelDWU3DlFJc,10533
26
26
  vibe_surf/backend/api/task.py,sha256=vpQMOn6YBuD_16jzfUajUvBYaydC0jj8Ny3WOJDVuck,14359
@@ -36,7 +36,7 @@ vibe_surf/backend/database/migrations/v003_fix_task_status_case.sql,sha256=npzRg
36
36
  vibe_surf/backend/database/migrations/v004_add_voice_profiles.sql,sha256=-9arjQBF-OxvFIOwkEl7JJJRDTS_nJ8GNX3T7bJgVq0,1321
37
37
  vibe_surf/backend/utils/__init__.py,sha256=V8leMFp7apAglUAoCHPZrNNcRHthSLYIudIJE5qwjb0,184
38
38
  vibe_surf/backend/utils/encryption.py,sha256=CjLNh_n0Luhfa-6BB-icfzkiiDqj5b4Gu6MADU3p2eM,3754
39
- vibe_surf/backend/utils/llm_factory.py,sha256=mNy8o3sw7vYJ8gwiTsrgXbG7Ri_B11ylE4KGcfHULp8,8972
39
+ vibe_surf/backend/utils/llm_factory.py,sha256=KF84YYgPaOF0_1P_IF0cAtY1kua0D-8gEP2NoSu2UZM,9033
40
40
  vibe_surf/browser/__init__.py,sha256=_UToO2fZfSCrfjOcxhn4Qq7ZLbYeyPuUUEmqIva-Yv8,325
41
41
  vibe_surf/browser/agen_browser_profile.py,sha256=J06hCBJSJ-zAFVM9yDFz8UpmiLuFyWke1EMekpU45eo,5871
42
42
  vibe_surf/browser/agent_browser_session.py,sha256=xV0nHo_TCb7b7QYhIee4cLzH-1rqJswYwH7GEwyQmqc,33980
@@ -85,19 +85,20 @@ vibe_surf/chrome_extension/styles/settings-responsive.css,sha256=jLE0yG15n2aI6_6
85
85
  vibe_surf/chrome_extension/styles/settings-utilities.css,sha256=3PuQS2857kg83d5erLbLdo_7J95-qV-qyNWS5M-w1oQ,505
86
86
  vibe_surf/chrome_extension/styles/variables.css,sha256=enjyhsa0PeU3b-3uiXa-VkV-1-h2-Ai3m4KpmC2k0rY,2984
87
87
  vibe_surf/llm/__init__.py,sha256=_vDVPo6STf343p1SgMQrF5023hicAx0g83pK2Gbk4Ek,601
88
- vibe_surf/llm/openai_compatible.py,sha256=8v0LW_-ZoKv4gcO--6_SmU_BLF8XCJaiPxZ6kXFgM4I,14998
88
+ vibe_surf/llm/openai_compatible.py,sha256=7e0XC-Mtz8MmgQZHH8tx8H_VXB6MLvMhDy1qKbESmVo,16149
89
89
  vibe_surf/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
90
90
  vibe_surf/tools/browser_use_tools.py,sha256=tacxKUJL6uOt04f52_iIw1cs-FT-mBgIPmAsIc4Hww0,23730
91
- vibe_surf/tools/file_system.py,sha256=PKRHe3EuOl78zzrA4SFiz1DDJotzx7OmszZqQMipQ-k,19317
91
+ vibe_surf/tools/file_system.py,sha256=Tw_6J5QjCahQ3fd26CXziF1zPvRxhYM0889oK4bDhlU,19304
92
+ vibe_surf/tools/finance_tools.py,sha256=pwPSBb0HwCDTdKZNAS5NPE8-rM1Nz57foj9XyKgQmI4,24803
92
93
  vibe_surf/tools/mcp_client.py,sha256=OeCoTgyx4MoY7JxXndK6pGHIoyFOhf5r7XCbx25y1Ec,2446
93
- vibe_surf/tools/report_writer_tools.py,sha256=sUqUFr-_Rs8RJ0Bs77Hrp07kNwRIvHv7ErzSPYSlbTg,705
94
+ vibe_surf/tools/report_writer_tools.py,sha256=2CyTTXOahTKZo7XwyWDDhJ--1mRA0uTtUWxu_DACAY0,776
94
95
  vibe_surf/tools/vibesurf_registry.py,sha256=Z-8d9BrJl3RFMEK0Tw1Q5xNHX2kZGsnIGCTBZ3RM-pw,2159
95
- vibe_surf/tools/vibesurf_tools.py,sha256=oeByep_Ud1V0n6pBayvLZJBUXch_iVVwOzUx8Rmsbqo,74350
96
- vibe_surf/tools/views.py,sha256=QyOjbEL4VwJvyFnYytP6r-Nju9ufvrAllSK2U8kwrGU,6183
96
+ vibe_surf/tools/vibesurf_tools.py,sha256=KMf9J_GDo9MbjBruv6-aHi5srR2pvlvW3uegihAMRIc,79994
97
+ vibe_surf/tools/views.py,sha256=AEAPzML-lqWJ7dBMjXTl7o-rk4hp5PGaPRqLyilJUl8,7789
97
98
  vibe_surf/tools/voice_asr.py,sha256=AJG0yq_Jq-j8ulDlbPhVFfK1jch9_ASesis73iki9II,4702
98
- vibesurf-0.1.23.dist-info/licenses/LICENSE,sha256=czn6QYya0-jhLnStD9JqnMS-hwP5wRByipkrGTvoXLI,11355
99
- vibesurf-0.1.23.dist-info/METADATA,sha256=De_G65fKEOpXevnlZu6IEyQ8W4F1KoxIif2yn8CJjqk,5158
100
- vibesurf-0.1.23.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
101
- vibesurf-0.1.23.dist-info/entry_points.txt,sha256=UxqpvMocL-PR33S6vLF2OmXn-kVzM-DneMeZeHcPMM8,48
102
- vibesurf-0.1.23.dist-info/top_level.txt,sha256=VPZGHqSb6EEqcJ4ZX6bHIuWfon5f6HXl3c7BYpbRqnY,10
103
- vibesurf-0.1.23.dist-info/RECORD,,
99
+ vibesurf-0.1.24.dist-info/licenses/LICENSE,sha256=czn6QYya0-jhLnStD9JqnMS-hwP5wRByipkrGTvoXLI,11355
100
+ vibesurf-0.1.24.dist-info/METADATA,sha256=Ck-enMQ77f9ekeLQG9xzNGX3mOuDhqIXiXdA3_Zcq4I,5190
101
+ vibesurf-0.1.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
102
+ vibesurf-0.1.24.dist-info/entry_points.txt,sha256=UxqpvMocL-PR33S6vLF2OmXn-kVzM-DneMeZeHcPMM8,48
103
+ vibesurf-0.1.24.dist-info/top_level.txt,sha256=VPZGHqSb6EEqcJ4ZX6bHIuWfon5f6HXl3c7BYpbRqnY,10
104
+ vibesurf-0.1.24.dist-info/RECORD,,