vibesurf 0.1.31__py3-none-any.whl → 0.1.33__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.
Files changed (35) hide show
  1. vibe_surf/_version.py +2 -2
  2. vibe_surf/agents/browser_use_agent.py +1 -1
  3. vibe_surf/agents/prompts/vibe_surf_prompt.py +6 -0
  4. vibe_surf/agents/report_writer_agent.py +50 -0
  5. vibe_surf/agents/vibe_surf_agent.py +56 -1
  6. vibe_surf/backend/api/composio.py +952 -0
  7. vibe_surf/backend/database/migrations/v005_add_composio_integration.sql +33 -0
  8. vibe_surf/backend/database/migrations/v006_add_credentials_table.sql +26 -0
  9. vibe_surf/backend/database/models.py +53 -1
  10. vibe_surf/backend/database/queries.py +312 -2
  11. vibe_surf/backend/main.py +28 -0
  12. vibe_surf/backend/shared_state.py +123 -9
  13. vibe_surf/chrome_extension/scripts/api-client.js +32 -0
  14. vibe_surf/chrome_extension/scripts/settings-manager.js +954 -1
  15. vibe_surf/chrome_extension/sidepanel.html +190 -0
  16. vibe_surf/chrome_extension/styles/settings-integrations.css +927 -0
  17. vibe_surf/chrome_extension/styles/settings-modal.css +7 -3
  18. vibe_surf/chrome_extension/styles/settings-responsive.css +37 -5
  19. vibe_surf/cli.py +98 -3
  20. vibe_surf/telemetry/__init__.py +60 -0
  21. vibe_surf/telemetry/service.py +112 -0
  22. vibe_surf/telemetry/views.py +156 -0
  23. vibe_surf/tools/browser_use_tools.py +90 -90
  24. vibe_surf/tools/composio_client.py +456 -0
  25. vibe_surf/tools/mcp_client.py +21 -2
  26. vibe_surf/tools/vibesurf_tools.py +290 -87
  27. vibe_surf/tools/views.py +16 -0
  28. vibe_surf/tools/website_api/youtube/client.py +35 -13
  29. vibe_surf/utils.py +13 -0
  30. {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/METADATA +11 -9
  31. {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/RECORD +35 -26
  32. {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/WHEEL +0 -0
  33. {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/entry_points.txt +0 -0
  34. {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/licenses/LICENSE +0 -0
  35. {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/top_level.txt +0 -0
@@ -594,93 +594,93 @@ class BrowserUseTools(Tools, VibeSurfTools):
594
594
  logger.error(error_msg)
595
595
  return ActionResult(error=error_msg)
596
596
 
597
- async def _detect_file_format(self, url: str, headers: dict, content: bytes) -> str:
598
- """Detect file format from URL, headers, and content"""
599
-
600
- # Try Content-Type header first
601
- content_type = headers.get('content-type', '').lower()
602
- if content_type:
603
- # Common image formats
604
- if 'image/jpeg' in content_type or 'image/jpg' in content_type:
605
- return '.jpg'
606
- elif 'image/png' in content_type:
607
- return '.png'
608
- elif 'image/gif' in content_type:
609
- return '.gif'
610
- elif 'image/webp' in content_type:
611
- return '.webp'
612
- elif 'image/svg' in content_type:
613
- return '.svg'
614
- elif 'image/bmp' in content_type:
615
- return '.bmp'
616
- elif 'image/tiff' in content_type:
617
- return '.tiff'
618
- # Video formats
619
- elif 'video/mp4' in content_type:
620
- return '.mp4'
621
- elif 'video/webm' in content_type:
622
- return '.webm'
623
- elif 'video/avi' in content_type:
624
- return '.avi'
625
- elif 'video/mov' in content_type or 'video/quicktime' in content_type:
626
- return '.mov'
627
- # Audio formats
628
- elif 'audio/mpeg' in content_type or 'audio/mp3' in content_type:
629
- return '.mp3'
630
- elif 'audio/wav' in content_type:
631
- return '.wav'
632
- elif 'audio/ogg' in content_type:
633
- return '.ogg'
634
- elif 'audio/webm' in content_type:
635
- return '.webm'
636
-
637
- # Try magic number detection
638
- if len(content) >= 8:
639
- # JPEG
640
- if content.startswith(b'\xff\xd8\xff'):
641
- return '.jpg'
642
- # PNG
643
- elif content.startswith(b'\x89PNG\r\n\x1a\n'):
644
- return '.png'
645
- # GIF
646
- elif content.startswith(b'GIF87a') or content.startswith(b'GIF89a'):
647
- return '.gif'
648
- # WebP
649
- elif content[8:12] == b'WEBP':
650
- return '.webp'
651
- # BMP
652
- elif content.startswith(b'BM'):
653
- return '.bmp'
654
- # TIFF
655
- elif content.startswith(b'II*\x00') or content.startswith(b'MM\x00*'):
656
- return '.tiff'
657
- # MP4
658
- elif b'ftyp' in content[4:12]:
659
- return '.mp4'
660
- # PDF
661
- elif content.startswith(b'%PDF'):
662
- return '.pdf'
663
-
664
- # Try URL path extension
665
- url_path = urllib.parse.urlparse(url).path
666
- if url_path:
667
- ext = os.path.splitext(url_path)[1].lower()
668
- if ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.tiff',
669
- '.mp4', '.webm', '.avi', '.mov', '.wmv', '.flv',
670
- '.mp3', '.wav', '.ogg', '.aac', '.flac',
671
- '.pdf', '.doc', '.docx', '.txt']:
672
- return ext
673
-
674
- # Default fallback
675
- return '.bin'
676
-
677
- def _format_file_size(self, size_bytes: int) -> str:
678
- """Format file size in human readable format"""
679
- if size_bytes == 0:
680
- return "0 B"
681
- size_names = ["B", "KB", "MB", "GB", "TB"]
682
- i = 0
683
- while size_bytes >= 1024.0 and i < len(size_names) - 1:
684
- size_bytes /= 1024.0
685
- i += 1
686
- return f"{size_bytes:.1f} {size_names[i]}"
597
+ async def _detect_file_format(self, url: str, headers: dict, content: bytes) -> str:
598
+ """Detect file format from URL, headers, and content"""
599
+
600
+ # Try Content-Type header first
601
+ content_type = headers.get('content-type', '').lower()
602
+ if content_type:
603
+ # Common image formats
604
+ if 'image/jpeg' in content_type or 'image/jpg' in content_type:
605
+ return '.jpg'
606
+ elif 'image/png' in content_type:
607
+ return '.png'
608
+ elif 'image/gif' in content_type:
609
+ return '.gif'
610
+ elif 'image/webp' in content_type:
611
+ return '.webp'
612
+ elif 'image/svg' in content_type:
613
+ return '.svg'
614
+ elif 'image/bmp' in content_type:
615
+ return '.bmp'
616
+ elif 'image/tiff' in content_type:
617
+ return '.tiff'
618
+ # Video formats
619
+ elif 'video/mp4' in content_type:
620
+ return '.mp4'
621
+ elif 'video/webm' in content_type:
622
+ return '.webm'
623
+ elif 'video/avi' in content_type:
624
+ return '.avi'
625
+ elif 'video/mov' in content_type or 'video/quicktime' in content_type:
626
+ return '.mov'
627
+ # Audio formats
628
+ elif 'audio/mpeg' in content_type or 'audio/mp3' in content_type:
629
+ return '.mp3'
630
+ elif 'audio/wav' in content_type:
631
+ return '.wav'
632
+ elif 'audio/ogg' in content_type:
633
+ return '.ogg'
634
+ elif 'audio/webm' in content_type:
635
+ return '.webm'
636
+
637
+ # Try magic number detection
638
+ if len(content) >= 8:
639
+ # JPEG
640
+ if content.startswith(b'\xff\xd8\xff'):
641
+ return '.jpg'
642
+ # PNG
643
+ elif content.startswith(b'\x89PNG\r\n\x1a\n'):
644
+ return '.png'
645
+ # GIF
646
+ elif content.startswith(b'GIF87a') or content.startswith(b'GIF89a'):
647
+ return '.gif'
648
+ # WebP
649
+ elif content[8:12] == b'WEBP':
650
+ return '.webp'
651
+ # BMP
652
+ elif content.startswith(b'BM'):
653
+ return '.bmp'
654
+ # TIFF
655
+ elif content.startswith(b'II*\x00') or content.startswith(b'MM\x00*'):
656
+ return '.tiff'
657
+ # MP4
658
+ elif b'ftyp' in content[4:12]:
659
+ return '.mp4'
660
+ # PDF
661
+ elif content.startswith(b'%PDF'):
662
+ return '.pdf'
663
+
664
+ # Try URL path extension
665
+ url_path = urllib.parse.urlparse(url).path
666
+ if url_path:
667
+ ext = os.path.splitext(url_path)[1].lower()
668
+ if ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.tiff',
669
+ '.mp4', '.webm', '.avi', '.mov', '.wmv', '.flv',
670
+ '.mp3', '.wav', '.ogg', '.aac', '.flac',
671
+ '.pdf', '.doc', '.docx', '.txt']:
672
+ return ext
673
+
674
+ # Default fallback
675
+ return '.bin'
676
+
677
+ def _format_file_size(self, size_bytes: int) -> str:
678
+ """Format file size in human readable format"""
679
+ if size_bytes == 0:
680
+ return "0 B"
681
+ size_names = ["B", "KB", "MB", "GB", "TB"]
682
+ i = 0
683
+ while size_bytes >= 1024.0 and i < len(size_names) - 1:
684
+ size_bytes /= 1024.0
685
+ i += 1
686
+ return f"{size_bytes:.1f} {size_names[i]}"
@@ -0,0 +1,456 @@
1
+ """Composio client integration for VibeSurf tools.
2
+
3
+ This module provides integration between Composio toolkits and VibeSurf's action registry.
4
+ Composio tools are dynamically discovered and registered as VibeSurf actions.
5
+
6
+ Example usage:
7
+ from vibe_surf.tools.composio_client import ComposioClient
8
+ from vibe_surf.tools.vibesurf_tools import VibeSurfTools
9
+
10
+ tools = VibeSurfTools()
11
+
12
+ # Connect to Composio
13
+ composio_client = ComposioClient(
14
+ composio_instance=composio_instance
15
+ )
16
+
17
+ # Register all Composio tools as VibeSurf actions
18
+ await composio_client.register_to_tools(tools, toolkit_tools_dict)
19
+ """
20
+
21
+ import asyncio
22
+ import logging
23
+ import time
24
+ import json
25
+ from typing import Any, Dict, Optional, List
26
+
27
+ from pydantic import BaseModel, ConfigDict, Field, create_model
28
+
29
+ from browser_use.agent.views import ActionResult
30
+ from vibe_surf.logger import get_logger
31
+ from vibe_surf.telemetry.service import ProductTelemetry
32
+ from vibe_surf.telemetry.views import ComposioTelemetryEvent
33
+ from vibe_surf.utils import get_vibesurf_version
34
+
35
+ logger = get_logger(__name__)
36
+
37
+
38
+ class ComposioClient:
39
+ """Client for connecting to Composio and exposing toolkit tools as VibeSurf actions."""
40
+
41
+ def __init__(
42
+ self,
43
+ composio_instance: Optional[Any] = None,
44
+ ):
45
+ """Initialize Composio client.
46
+
47
+ Args:
48
+ composio_instance: Composio instance (optional, can be set later)
49
+ """
50
+ self.composio_instance = composio_instance
51
+ self._registered_actions: set[str] = set()
52
+ self._toolkit_tools: Dict[str, List[Dict]] = {}
53
+ self._telemetry = ProductTelemetry()
54
+
55
+ def update_composio_instance(self, composio_instance: Any):
56
+ """Update the Composio instance"""
57
+ self.composio_instance = composio_instance
58
+
59
+ async def register_to_tools(
60
+ self,
61
+ tools, # VibeSurfTools instance
62
+ toolkit_tools_dict: Dict[str, List[Dict]],
63
+ prefix: str = "cpo.",
64
+ ) -> None:
65
+ """Register Composio tools as actions in the VibeSurf tools.
66
+
67
+ Args:
68
+ tools: VibeSurf tools instance to register actions to
69
+ toolkit_tools_dict: Dict of toolkit_slug -> tools list
70
+ prefix: Prefix to add to action names (e.g., "cpo.")
71
+ """
72
+ if not self.composio_instance:
73
+ logger.warning("Composio instance not available, skipping registration")
74
+ return
75
+
76
+ self._toolkit_tools = toolkit_tools_dict
77
+ registry = tools.registry
78
+
79
+ for toolkit_slug, tools_list in toolkit_tools_dict.items():
80
+ # Parse tools if it's a JSON string
81
+ if isinstance(tools_list, str):
82
+ try:
83
+ tools_list = json.loads(tools_list)
84
+ except (json.JSONDecodeError, TypeError) as e:
85
+ logger.warning(f"Failed to parse tools for toolkit {toolkit_slug}: {e}")
86
+ continue
87
+
88
+ if not isinstance(tools_list, list):
89
+ logger.warning(f"Tools for toolkit {toolkit_slug} is not a list: {type(tools_list)}")
90
+ continue
91
+
92
+ for tool_info in tools_list:
93
+ if not isinstance(tool_info, dict):
94
+ continue
95
+
96
+ tool_name = tool_info.get('name')
97
+ if not tool_name:
98
+ continue
99
+
100
+ # Skip if tool is disabled
101
+ if not tool_info.get('enabled', True):
102
+ continue
103
+
104
+ # Apply prefix
105
+ action_name = f'{prefix}{toolkit_slug}.{tool_name}'
106
+
107
+ # Skip if already registered
108
+ if action_name in self._registered_actions:
109
+ continue
110
+
111
+ # Register the tool as an action
112
+ self._register_tool_as_action(registry, action_name, toolkit_slug, tool_info)
113
+ self._registered_actions.add(action_name)
114
+
115
+ logger.info(f"✅ Registered {len(self._registered_actions)} Composio tools as VibeSurf actions")
116
+
117
+ # Capture telemetry for registration
118
+ self._telemetry.capture(
119
+ ComposioTelemetryEvent(
120
+ toolkit_slugs=list(toolkit_tools_dict.keys()),
121
+ tools_registered=len(self._registered_actions),
122
+ version=get_vibesurf_version(),
123
+ action='register'
124
+ )
125
+ )
126
+
127
+ def _register_tool_as_action(self, registry, action_name: str, toolkit_slug: str, tool_info: Dict) -> None:
128
+ """Register a single Composio tool as a VibeSurf action.
129
+
130
+ Args:
131
+ registry: VibeSurf registry to register action to
132
+ action_name: Name for the registered action
133
+ toolkit_slug: Toolkit slug
134
+ tool_info: Tool information dictionary
135
+ """
136
+ # Parse tool parameters to create Pydantic model
137
+ param_fields = {}
138
+ tool_name = tool_info.get('name', '')
139
+ description = tool_info.get('description', f'Composio tool: {tool_name}')
140
+ parameters = tool_info.get('parameters', {})
141
+
142
+ if parameters and isinstance(parameters, dict):
143
+ # Handle JSON Schema parameters
144
+ properties = parameters.get('properties', {})
145
+ required = set(parameters.get('required', []))
146
+
147
+ for param_name, param_schema in properties.items():
148
+ # Convert JSON Schema type to Python type
149
+ param_type = self._json_schema_to_python_type(param_schema, f'{action_name}_{param_name}')
150
+
151
+ # Determine if field is required and handle defaults
152
+ if param_name in required:
153
+ default = ... # Required field
154
+ else:
155
+ # Optional field - make type optional and handle default
156
+ param_type = param_type | None
157
+ if 'default' in param_schema:
158
+ default = param_schema['default']
159
+ else:
160
+ default = None
161
+
162
+ # Add field with description if available
163
+ field_kwargs = {}
164
+ if 'description' in param_schema:
165
+ field_kwargs['description'] = param_schema['description']
166
+
167
+ param_fields[param_name] = (param_type, Field(default, **field_kwargs))
168
+
169
+ # Create Pydantic model for the tool parameters
170
+ if param_fields:
171
+ # Create a BaseModel class with proper configuration
172
+ class ConfiguredBaseModel(BaseModel):
173
+ model_config = ConfigDict(extra='forbid', validate_by_name=True, validate_by_alias=True)
174
+
175
+ param_model = create_model(f'{action_name}_Params', __base__=ConfiguredBaseModel, **param_fields)
176
+ else:
177
+ # No parameters - create empty model
178
+ param_model = None
179
+
180
+ # Create async wrapper function for the Composio tool
181
+ if param_model:
182
+ # Function takes param model as first parameter
183
+ async def composio_action_wrapper(params: param_model) -> ActionResult: # type: ignore[no-redef]
184
+ """Wrapper function that calls the Composio tool."""
185
+ if not self.composio_instance:
186
+ return ActionResult(error=f"Composio instance not available", success=False)
187
+
188
+ # Convert pydantic model to dict for Composio call
189
+ tool_params = params.model_dump(exclude_none=True)
190
+
191
+ logger.debug(f"🔧 Calling Composio tool '{tool_name}' with params: {tool_params}")
192
+
193
+ start_time = time.time()
194
+ error_msg = None
195
+
196
+ try:
197
+ # Call the Composio tool using the tools.execute method
198
+ entity_id = "default" # Use default entity ID
199
+ if 'include_payload' in tool_params:
200
+ tool_params['include_payload'] = False
201
+ result = self.composio_instance.tools.execute(
202
+ slug=tool_name,
203
+ arguments=tool_params,
204
+ user_id=entity_id,
205
+ )
206
+
207
+ # Convert Composio result to ActionResult
208
+ extracted_content = self._format_composio_result(result)
209
+
210
+ return ActionResult(
211
+ extracted_content=extracted_content,
212
+ long_term_memory=f"Used Composio tool '{tool_name}' from {toolkit_slug}",
213
+ )
214
+
215
+ except Exception as e:
216
+ error_msg = f"Composio tool '{tool_name}' failed: {str(e)}"
217
+ logger.error(error_msg)
218
+ return ActionResult(error=error_msg, success=False)
219
+ finally:
220
+ # Log execution time and capture telemetry
221
+ duration = time.time() - start_time
222
+ logger.debug(f"Composio tool '{tool_name}' executed in {duration:.2f}s")
223
+
224
+ # Capture telemetry for tool call
225
+ self._telemetry.capture(
226
+ ComposioTelemetryEvent(
227
+ toolkit_slugs=[toolkit_slug],
228
+ tools_registered=len(self._registered_actions),
229
+ version=get_vibesurf_version(),
230
+ action='tool_call',
231
+ toolkit_slug=toolkit_slug,
232
+ tool_name=tool_name,
233
+ duration_seconds=duration,
234
+ error_message=error_msg
235
+ )
236
+ )
237
+ else:
238
+ # No parameters - empty function signature
239
+ async def composio_action_wrapper() -> ActionResult: # type: ignore[no-redef]
240
+ """Wrapper function that calls the Composio tool."""
241
+ if not self.composio_instance:
242
+ return ActionResult(error=f"Composio instance not available", success=False)
243
+
244
+ logger.debug(f"🔧 Calling Composio tool '{tool_name}' with no params")
245
+
246
+ start_time = time.time()
247
+ error_msg = None
248
+
249
+ try:
250
+ # Call the Composio tool with empty params
251
+ entity_id = "default" # Use default entity ID
252
+ result = self.composio_instance.tools.execute(
253
+ slug=tool_name,
254
+ arguments={},
255
+ user_id=entity_id,
256
+ )
257
+
258
+ # Convert Composio result to ActionResult
259
+ extracted_content = self._format_composio_result(result)
260
+
261
+ return ActionResult(
262
+ extracted_content=extracted_content,
263
+ long_term_memory=f"Used Composio tool '{tool_name}' from {toolkit_slug}",
264
+ )
265
+
266
+ except Exception as e:
267
+ error_msg = f"Composio tool '{tool_name}' failed: {str(e)}"
268
+ logger.error(error_msg)
269
+ return ActionResult(error=error_msg, success=False)
270
+ finally:
271
+ # Log execution time and capture telemetry
272
+ duration = time.time() - start_time
273
+ logger.debug(f"Composio tool '{tool_name}' executed in {duration:.2f}s")
274
+
275
+ # Capture telemetry for tool call
276
+ self._telemetry.capture(
277
+ ComposioTelemetryEvent(
278
+ toolkit_slugs=[toolkit_slug],
279
+ tools_registered=len(self._registered_actions),
280
+ version=get_vibesurf_version(),
281
+ action='tool_call',
282
+ toolkit_slug=toolkit_slug,
283
+ tool_name=tool_name,
284
+ duration_seconds=duration,
285
+ error_message=error_msg
286
+ )
287
+ )
288
+
289
+ # Set function metadata for better debugging
290
+ composio_action_wrapper.__name__ = action_name
291
+ composio_action_wrapper.__qualname__ = f'composio.{toolkit_slug}.{action_name}'
292
+
293
+ # Register the action with VibeSurf
294
+ registry.action(description=description, param_model=param_model)(composio_action_wrapper)
295
+
296
+ logger.debug(f"✅ Registered Composio tool '{tool_name}' as action '{action_name}'")
297
+
298
+ def _format_composio_result(self, result: Any) -> str:
299
+ """Format Composio tool result into a string for ActionResult.
300
+
301
+ Args:
302
+ result: Raw result from Composio tool call
303
+
304
+ Returns:
305
+ Formatted string representation of the result
306
+ """
307
+ # Handle different Composio result formats
308
+ if isinstance(result, dict) or isinstance(result, list):
309
+ # Dictionary result
310
+ try:
311
+ return f"```json\n{json.dumps(result, indent=2, ensure_ascii=False)}\n```"
312
+ except (TypeError, ValueError):
313
+ return str(result)
314
+ else:
315
+ # Direct result or unknown format
316
+ return str(result)
317
+
318
+ def _json_schema_to_python_type(self, schema: dict, model_name: str = 'NestedModel') -> Any:
319
+ """Convert JSON Schema type to Python type.
320
+
321
+ Args:
322
+ schema: JSON Schema definition
323
+ model_name: Name for nested models
324
+
325
+ Returns:
326
+ Python type corresponding to the schema
327
+ """
328
+ json_type = schema.get('type', 'string')
329
+
330
+ # Basic type mapping
331
+ type_mapping = {
332
+ 'string': str,
333
+ 'number': float,
334
+ 'integer': int,
335
+ 'boolean': bool,
336
+ 'array': list,
337
+ 'null': type(None),
338
+ }
339
+
340
+ # Handle enums (they're still strings)
341
+ if 'enum' in schema:
342
+ return str
343
+
344
+ # Handle objects with nested properties
345
+ if json_type == 'object':
346
+ properties = schema.get('properties', {})
347
+ if properties:
348
+ # Create nested pydantic model for objects with properties
349
+ nested_fields = {}
350
+ required_fields = set(schema.get('required', []))
351
+
352
+ for prop_name, prop_schema in properties.items():
353
+ # Recursively process nested properties
354
+ prop_type = self._json_schema_to_python_type(prop_schema, f'{model_name}_{prop_name}')
355
+
356
+ # Determine if field is required and handle defaults
357
+ if prop_name in required_fields:
358
+ default = ... # Required field
359
+ else:
360
+ # Optional field - make type optional and handle default
361
+ prop_type = prop_type | None
362
+ if 'default' in prop_schema:
363
+ default = prop_schema['default']
364
+ else:
365
+ default = None
366
+
367
+ # Add field with description if available
368
+ field_kwargs = {}
369
+ if 'description' in prop_schema:
370
+ field_kwargs['description'] = prop_schema['description']
371
+
372
+ nested_fields[prop_name] = (prop_type, Field(default, **field_kwargs))
373
+
374
+ # Create a BaseModel class with proper configuration
375
+ class ConfiguredBaseModel(BaseModel):
376
+ model_config = ConfigDict(extra='forbid', validate_by_name=True, validate_by_alias=True)
377
+
378
+ try:
379
+ # Create and return nested pydantic model
380
+ return create_model(model_name, __base__=ConfiguredBaseModel, **nested_fields)
381
+ except Exception as e:
382
+ logger.error(f'Failed to create nested model {model_name}: {e}')
383
+ logger.debug(f'Fields: {nested_fields}')
384
+ # Fallback to basic dict if model creation fails
385
+ return dict
386
+ else:
387
+ # Object without properties - just return dict
388
+ return dict
389
+
390
+ # Handle arrays with specific item types
391
+ if json_type == 'array':
392
+ if 'items' in schema:
393
+ # Get the item type recursively
394
+ item_type = self._json_schema_to_python_type(schema['items'], f'{model_name}_item')
395
+ # Return properly typed list
396
+ return list[item_type]
397
+ else:
398
+ # Array without item type specification
399
+ return list
400
+
401
+ # Get base type for non-object types
402
+ base_type = type_mapping.get(json_type, str)
403
+
404
+ # Handle nullable/optional types
405
+ if schema.get('nullable', False) or json_type == 'null':
406
+ return base_type | None
407
+
408
+ return base_type
409
+
410
+ def unregister_all_tools(self, tools):
411
+ """Unregister all Composio tools from the registry"""
412
+ try:
413
+ # Get all registered actions
414
+ actions_to_remove = []
415
+ for action_name in list(tools.registry.registry.actions.keys()):
416
+ if action_name.startswith('cpo.'):
417
+ actions_to_remove.append(action_name)
418
+
419
+ # Remove Composio actions from registry
420
+ for action_name in actions_to_remove:
421
+ if action_name in tools.registry.registry.actions:
422
+ del tools.registry.registry.actions[action_name]
423
+ logger.debug(f'Removed Composio action: {action_name}')
424
+
425
+ # Clear the registered actions set
426
+ self._registered_actions.clear()
427
+ self._toolkit_tools.clear()
428
+
429
+ logger.info(f"Unregistered {len(actions_to_remove)} Composio actions")
430
+
431
+ # Capture telemetry for unregistration
432
+ self._telemetry.capture(
433
+ ComposioTelemetryEvent(
434
+ toolkit_slugs=list(self._toolkit_tools.keys()),
435
+ tools_registered=0, # All tools unregistered
436
+ version=get_vibesurf_version(),
437
+ action='unregister'
438
+ )
439
+ )
440
+ self._telemetry.flush()
441
+
442
+ except Exception as e:
443
+ error_msg = str(e)
444
+ logger.error(f'Failed to unregister Composio actions: {error_msg}')
445
+
446
+ # Capture telemetry for unregistration error
447
+ self._telemetry.capture(
448
+ ComposioTelemetryEvent(
449
+ toolkit_slugs=list(self._toolkit_tools.keys()),
450
+ tools_registered=len(self._registered_actions),
451
+ version=get_vibesurf_version(),
452
+ action='unregister',
453
+ error_message=error_msg
454
+ )
455
+ )
456
+ self._telemetry.flush()