camel-ai 0.2.73a2__py3-none-any.whl → 0.2.73a4__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.
@@ -36,7 +36,7 @@ class ToolkitMessageIntegration:
36
36
  >>> # Using default message handler with toolkit
37
37
  >>> message_integration = ToolkitMessageIntegration()
38
38
  >>> search_with_messaging = message_integration.
39
- add_messaging_to_toolkit(
39
+ register_toolkits(
40
40
  ... SearchToolkit()
41
41
  ... )
42
42
 
@@ -44,7 +44,7 @@ class ToolkitMessageIntegration:
44
44
  >>> def search_web(query: str) -> list:
45
45
  ... return ["result1", "result2"]
46
46
  ...
47
- >>> enhanced_tools = message_integration.add_messaging_to_functions
47
+ >>> enhanced_tools = message_integration.register_functions
48
48
  ([search_web])
49
49
 
50
50
  >>> # Using custom message handler with different parameters
@@ -148,7 +148,7 @@ class ToolkitMessageIntegration:
148
148
  """
149
149
  return FunctionTool(self.send_message_to_user)
150
150
 
151
- def add_messaging_to_toolkit(
151
+ def register_toolkits(
152
152
  self, toolkit: BaseToolkit, tool_names: Optional[List[str]] = None
153
153
  ) -> BaseToolkit:
154
154
  r"""Add messaging capabilities to toolkit methods.
@@ -168,26 +168,72 @@ class ToolkitMessageIntegration:
168
168
  Returns:
169
169
  The toolkit with messaging capabilities added
170
170
  """
171
- original_get_tools = toolkit.get_tools
171
+ original_tools = toolkit.get_tools()
172
+ enhanced_methods = {}
173
+ for tool in original_tools:
174
+ method_name = tool.func.__name__
175
+ if tool_names is None or method_name in tool_names:
176
+ enhanced_func = self._add_messaging_to_tool(tool.func)
177
+ enhanced_methods[method_name] = enhanced_func
178
+ setattr(toolkit, method_name, enhanced_func)
179
+ original_get_tools_method = toolkit.get_tools
172
180
 
173
181
  def enhanced_get_tools() -> List[FunctionTool]:
174
- tools = original_get_tools()
175
- enhanced_tools = []
182
+ tools = []
183
+ for _, enhanced_method in enhanced_methods.items():
184
+ tools.append(FunctionTool(enhanced_method))
185
+ original_tools_list = original_get_tools_method()
186
+ for tool in original_tools_list:
187
+ if tool.func.__name__ not in enhanced_methods:
188
+ tools.append(tool)
176
189
 
177
- for tool in tools:
178
- if tool_names is None or tool.func.__name__ in tool_names:
179
- enhanced_func = self._add_messaging_to_tool(tool.func)
180
- enhanced_tools.append(FunctionTool(enhanced_func))
181
- else:
182
- enhanced_tools.append(tool)
183
-
184
- return enhanced_tools
190
+ return tools
185
191
 
186
- # Replace the get_tools method
187
192
  toolkit.get_tools = enhanced_get_tools # type: ignore[method-assign]
193
+
194
+ # Also handle clone_for_new_session
195
+ # if it exists to ensure cloned toolkits
196
+ # also have message integration
197
+ if hasattr(toolkit, 'clone_for_new_session'):
198
+ original_clone_method = toolkit.clone_for_new_session
199
+ message_integration_instance = self
200
+
201
+ def enhanced_clone_for_new_session(new_session_id=None):
202
+ cloned_toolkit = original_clone_method(new_session_id)
203
+ return message_integration_instance.register_toolkits(
204
+ cloned_toolkit, tool_names
205
+ )
206
+
207
+ toolkit.clone_for_new_session = enhanced_clone_for_new_session
208
+
188
209
  return toolkit
189
210
 
190
- def add_messaging_to_functions(
211
+ def _create_bound_method_wrapper(
212
+ self, enhanced_func: Callable, toolkit_instance
213
+ ) -> Callable:
214
+ r"""Create a wrapper that mimics a bound method for _clone_tools.
215
+
216
+ This wrapper preserves the toolkit instance reference while maintaining
217
+ the enhanced messaging functionality.
218
+ """
219
+
220
+ # Create a wrapper that appears as a bound method to _clone_tools
221
+ @wraps(enhanced_func)
222
+ def bound_method_wrapper(*args, **kwargs):
223
+ return enhanced_func(*args, **kwargs)
224
+
225
+ # Make it appear as a bound method by setting __self__
226
+ bound_method_wrapper.__self__ = toolkit_instance # type: ignore[attr-defined]
227
+
228
+ # Preserve other important attributes
229
+ if hasattr(enhanced_func, '__signature__'):
230
+ bound_method_wrapper.__signature__ = enhanced_func.__signature__ # type: ignore[attr-defined]
231
+ if hasattr(enhanced_func, '__doc__'):
232
+ bound_method_wrapper.__doc__ = enhanced_func.__doc__
233
+
234
+ return bound_method_wrapper
235
+
236
+ def register_functions(
191
237
  self,
192
238
  functions: Union[List[FunctionTool], List[Callable]],
193
239
  function_names: Optional[List[str]] = None,
@@ -210,12 +256,12 @@ class ToolkitMessageIntegration:
210
256
  Example:
211
257
  >>> # With FunctionTools
212
258
  >>> tools = [FunctionTool(search_func), FunctionTool(analyze_func)]
213
- >>> enhanced_tools = message_integration.add_messaging_to_functions
259
+ >>> enhanced_tools = message_integration.register_functions
214
260
  (tools)
215
261
 
216
262
  >>> # With callable functions
217
263
  >>> funcs = [search_web, analyze_data, generate_report]
218
- >>> enhanced_tools = message_integration.add_messaging_to_functions
264
+ >>> enhanced_tools = message_integration.register_functions
219
265
  (
220
266
  ... funcs,
221
267
  ... function_names=['search_web', 'analyze_data']
@@ -257,6 +303,9 @@ class ToolkitMessageIntegration:
257
303
  # Get the original signature
258
304
  original_sig = inspect.signature(func)
259
305
 
306
+ # Check if the function is async
307
+ is_async = inspect.iscoroutinefunction(func)
308
+
260
309
  # Create new parameters for the enhanced function
261
310
  new_params = list(original_sig.parameters.values())
262
311
 
@@ -321,45 +370,123 @@ class ToolkitMessageIntegration:
321
370
  # Create the new signature
322
371
  new_sig = original_sig.replace(parameters=new_params)
323
372
 
324
- @wraps(func)
325
- def wrapper(*args, **kwargs):
326
- # Extract parameters using the callback
327
- try:
328
- params = self.extract_params_callback(kwargs)
329
- except KeyError:
330
- # If parameters are missing, just execute the original function
331
- return func(*args, **kwargs)
332
-
333
- # Check if we should send a message
334
- should_send = False
335
- if self.use_custom_handler:
336
- should_send = any(p is not None and p != '' for p in params)
337
- else:
338
- # For default handler, params = (title, description,
339
- # attachment)
340
- should_send = bool(params[0]) or bool(params[1])
373
+ if is_async:
374
+
375
+ @wraps(func)
376
+ async def wrapper(*args, **kwargs):
377
+ try:
378
+ params = self.extract_params_callback(kwargs)
379
+ except KeyError:
380
+ return await func(*args, **kwargs)
341
381
 
342
- # Send message if needed
343
- if should_send:
382
+ # Check if we should send a message
383
+ should_send = False
344
384
  if self.use_custom_handler:
345
- self.message_handler(*params)
385
+ should_send = any(
386
+ p is not None and p != '' for p in params
387
+ )
346
388
  else:
347
- # For built-in handler, provide defaults
348
- title, desc, attach = params
349
- self.message_handler(
350
- title or "Executing Tool",
351
- desc or f"Running {func.__name__}",
352
- attach or '',
389
+ # For default handler, params
390
+ # (title, description, attachment)
391
+ should_send = bool(params[0]) or bool(params[1])
392
+
393
+ # Send message if needed (handle async properly)
394
+ if should_send:
395
+ try:
396
+ if self.use_custom_handler:
397
+ # Check if message handler is async
398
+ if inspect.iscoroutinefunction(
399
+ self.message_handler
400
+ ):
401
+ await self.message_handler(*params)
402
+ else:
403
+ self.message_handler(*params)
404
+ else:
405
+ # For built-in handler, provide defaults
406
+ title, desc, attach = params
407
+ self.message_handler(
408
+ title or "Executing Tool",
409
+ desc or f"Running {func.__name__}",
410
+ attach or '',
411
+ )
412
+ except Exception as msg_error:
413
+ # Don't let message handler
414
+ # errors break the main function
415
+ logger.warning(f"Message handler error: {msg_error}")
416
+
417
+ # Execute the original function
418
+ # (kwargs have been modified to remove message params)
419
+ result = await func(*args, **kwargs)
420
+
421
+ return result
422
+ else:
423
+
424
+ @wraps(func)
425
+ def wrapper(*args, **kwargs):
426
+ # Extract parameters using the callback
427
+ # (this will modify kwargs by removing message params)
428
+ try:
429
+ params = self.extract_params_callback(kwargs)
430
+ except KeyError:
431
+ # If parameters are missing,
432
+ # just execute the original function
433
+ return func(*args, **kwargs)
434
+
435
+ # Check if we should send a message
436
+ should_send = False
437
+ if self.use_custom_handler:
438
+ should_send = any(
439
+ p is not None and p != '' for p in params
353
440
  )
441
+ else:
442
+ should_send = bool(params[0]) or bool(params[1])
354
443
 
355
- # Execute the original function
356
- result = func(*args, **kwargs)
444
+ # Send message if needed
445
+ if should_send:
446
+ try:
447
+ if self.use_custom_handler:
448
+ self.message_handler(*params)
449
+ else:
450
+ # For built-in handler, provide defaults
451
+ title, desc, attach = params
452
+ self.message_handler(
453
+ title or "Executing Tool",
454
+ desc or f"Running {func.__name__}",
455
+ attach or '',
456
+ )
457
+ except Exception as msg_error:
458
+ logger.warning(f"Message handler error: {msg_error}")
459
+
460
+ result = func(*args, **kwargs)
357
461
 
358
- return result
462
+ return result
359
463
 
360
464
  # Apply the new signature to the wrapper
361
465
  wrapper.__signature__ = new_sig # type: ignore[attr-defined]
362
466
 
467
+ # Create a hybrid approach:
468
+ # store toolkit instance info but preserve calling behavior
469
+ # We'll use a property-like
470
+ # approach to make __self__ available when needed
471
+ if hasattr(func, '__self__'):
472
+ toolkit_instance = func.__self__
473
+
474
+ # Store the toolkit instance as an attribute
475
+ # Use setattr to avoid MyPy type checking issues
476
+ wrapper.__toolkit_instance__ = toolkit_instance # type: ignore[attr-defined]
477
+
478
+ # Create a dynamic __self__ property
479
+ # that only appears during introspection
480
+ # but doesn't interfere with normal function calls
481
+ def get_self():
482
+ return toolkit_instance
483
+
484
+ # Only set __self__
485
+ # if we're being called in an introspection context
486
+ # (like from _clone_tools)
487
+ # Use setattr to avoid MyPy type checking issues
488
+ wrapper.__self__ = toolkit_instance # type: ignore[attr-defined]
489
+
363
490
  # Enhance the docstring
364
491
  if func.__doc__:
365
492
  enhanced_doc = func.__doc__.rstrip()
@@ -79,7 +79,9 @@ class WebDeployToolkit(BaseToolkit):
79
79
  self.tag_text = self._sanitize_text(tag_text)
80
80
  self.tag_url = self._validate_url(tag_url)
81
81
  self.remote_server_ip = (
82
- self._validate_ip(remote_server_ip) if remote_server_ip else None
82
+ self._validate_ip_or_domain(remote_server_ip)
83
+ if remote_server_ip
84
+ else None
83
85
  )
84
86
  self.remote_server_port = self._validate_port(remote_server_port)
85
87
  self.server_registry_file = os.path.join(
@@ -87,24 +89,36 @@ class WebDeployToolkit(BaseToolkit):
87
89
  )
88
90
  self._load_server_registry()
89
91
 
90
- def _validate_ip(self, ip: str) -> str:
91
- """Validate IP address format."""
92
+ def _validate_ip_or_domain(self, address: str) -> str:
93
+ r"""Validate IP address or domain name format."""
92
94
  import ipaddress
95
+ import re
93
96
 
94
97
  try:
95
- ipaddress.ip_address(ip)
96
- return ip
98
+ # Try to validate as IP address first
99
+ ipaddress.ip_address(address)
100
+ return address
97
101
  except ValueError:
98
- raise ValueError(f"Invalid IP address: {ip}")
102
+ # If not a valid IP, check if it's a valid domain name
103
+ domain_pattern = re.compile(
104
+ r'^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?'
105
+ r'(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$'
106
+ )
107
+ if domain_pattern.match(address) and len(address) <= 253:
108
+ return address
109
+ else:
110
+ raise ValueError(
111
+ f"Invalid IP address or domain name: {address}"
112
+ )
99
113
 
100
114
  def _validate_port(self, port: int) -> int:
101
- """Validate port number."""
115
+ r"""Validate port number."""
102
116
  if not isinstance(port, int) or port < 1 or port > 65535:
103
117
  raise ValueError(f"Invalid port number: {port}")
104
118
  return port
105
119
 
106
120
  def _sanitize_text(self, text: str) -> str:
107
- """Sanitize text to prevent XSS."""
121
+ r"""Sanitize text to prevent XSS."""
108
122
  if not isinstance(text, str):
109
123
  return ""
110
124
  # Remove any HTML/script tags
@@ -119,7 +133,7 @@ class WebDeployToolkit(BaseToolkit):
119
133
  return text[:100] # Limit length
120
134
 
121
135
  def _validate_url(self, url: str) -> str:
122
- """Validate URL format."""
136
+ r"""Validate URL format."""
123
137
  if not isinstance(url, str):
124
138
  raise ValueError("URL must be a string")
125
139
  # Basic URL validation
@@ -139,7 +153,7 @@ class WebDeployToolkit(BaseToolkit):
139
153
  def _validate_subdirectory(
140
154
  self, subdirectory: Optional[str]
141
155
  ) -> Optional[str]:
142
- """Validate subdirectory to prevent path traversal."""
156
+ r"""Validate subdirectory to prevent path traversal."""
143
157
  if subdirectory is None:
144
158
  return None
145
159
 
@@ -157,7 +171,7 @@ class WebDeployToolkit(BaseToolkit):
157
171
  return subdirectory
158
172
 
159
173
  def _is_port_available(self, port: int) -> bool:
160
- """Check if a port is available for binding."""
174
+ r"""Check if a port is available for binding."""
161
175
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
162
176
  try:
163
177
  sock.bind(('127.0.0.1', port))
@@ -604,7 +618,7 @@ class WebDeployToolkit(BaseToolkit):
604
618
  return {
605
619
  'success': False,
606
620
  'error': (
607
- f'Port {port} is already in use by ' f'another process'
621
+ f'Port {port} is already in use by another process'
608
622
  ),
609
623
  }
610
624
 
@@ -702,6 +716,44 @@ class WebDeployToolkit(BaseToolkit):
702
716
  # Validate subdirectory
703
717
  subdirectory = self._validate_subdirectory(subdirectory)
704
718
 
719
+ # Check if remote deployment is configured
720
+ if self.remote_server_ip:
721
+ return self._deploy_folder_to_remote_server(
722
+ folder_path,
723
+ subdirectory,
724
+ domain,
725
+ )
726
+ else:
727
+ return self._deploy_folder_to_local_server(
728
+ folder_path,
729
+ port,
730
+ domain,
731
+ subdirectory,
732
+ )
733
+
734
+ except Exception as e:
735
+ logger.error(f"Error deploying folder: {e}")
736
+ return {'success': False, 'error': str(e)}
737
+
738
+ def _deploy_folder_to_local_server(
739
+ self,
740
+ folder_path: str,
741
+ port: int,
742
+ domain: Optional[str],
743
+ subdirectory: Optional[str],
744
+ ) -> Dict[str, Any]:
745
+ r"""Deploy folder to local server (original functionality).
746
+
747
+ Args:
748
+ folder_path (str): Path to the folder to deploy
749
+ port (int): Port to serve on
750
+ domain (Optional[str]): Custom domain
751
+ subdirectory (Optional[str]): Subdirectory path
752
+
753
+ Returns:
754
+ Dict[str, Any]: Deployment result
755
+ """
756
+ try:
705
757
  temp_dir = None
706
758
  if self.add_branding_tag:
707
759
  # Create temporary directory and copy all files
@@ -877,6 +929,149 @@ class WebDeployToolkit(BaseToolkit):
877
929
  logger.error(f"Error deploying folder: {e}")
878
930
  return {'success': False, 'error': str(e)}
879
931
 
932
+ def _deploy_folder_to_remote_server(
933
+ self,
934
+ folder_path: str,
935
+ subdirectory: Optional[str] = None,
936
+ domain: Optional[str] = None,
937
+ ) -> Dict[str, Any]:
938
+ r"""Deploy folder to remote server via API.
939
+
940
+ Args:
941
+ folder_path (str): Path to the folder to deploy
942
+ subdirectory (Optional[str]): Subdirectory path for deployment
943
+ domain (Optional[str]): Custom domain
944
+
945
+ Returns:
946
+ Dict[str, Any]: Deployment result
947
+ """
948
+ try:
949
+ import tempfile
950
+ import zipfile
951
+
952
+ import requests
953
+
954
+ # Validate subdirectory
955
+ subdirectory = self._validate_subdirectory(subdirectory)
956
+
957
+ # Create a temporary zip file of the folder
958
+ with tempfile.NamedTemporaryFile(
959
+ suffix='.zip', delete=False
960
+ ) as temp_zip:
961
+ zip_path = temp_zip.name
962
+
963
+ try:
964
+ # Create zip archive
965
+ with zipfile.ZipFile(
966
+ zip_path, 'w', zipfile.ZIP_DEFLATED
967
+ ) as zipf:
968
+ for root, _, files in os.walk(folder_path):
969
+ for file in files:
970
+ file_path = os.path.join(root, file)
971
+ # Calculate relative path within the archive
972
+ arcname = os.path.relpath(file_path, folder_path)
973
+ zipf.write(file_path, arcname)
974
+
975
+ # Read zip file as base64
976
+ with open(zip_path, 'rb') as f:
977
+ zip_data = base64.b64encode(f.read()).decode('utf-8')
978
+
979
+ # Prepare deployment data
980
+ deploy_data = {
981
+ "deployment_type": "folder",
982
+ "folder_data": zip_data,
983
+ "subdirectory": subdirectory,
984
+ "domain": domain,
985
+ "timestamp": time.time(),
986
+ }
987
+
988
+ # Add logo data if custom logo is specified
989
+ if self.logo_path and os.path.exists(self.logo_path):
990
+ try:
991
+ logo_ext = os.path.splitext(self.logo_path)[1]
992
+ logo_filename = f"custom_logo{logo_ext}"
993
+
994
+ with open(self.logo_path, 'rb') as logo_file:
995
+ logo_data = base64.b64encode(
996
+ logo_file.read()
997
+ ).decode('utf-8')
998
+
999
+ deploy_data.update(
1000
+ {
1001
+ "logo_data": logo_data,
1002
+ "logo_ext": logo_ext,
1003
+ "logo_filename": logo_filename,
1004
+ }
1005
+ )
1006
+ except Exception as logo_error:
1007
+ logger.warning(
1008
+ f"Failed to process custom logo: {logo_error}"
1009
+ )
1010
+
1011
+ # Send to remote server API
1012
+ api_url = f"http://{self.remote_server_ip}:{self.remote_server_port}/api/deploy"
1013
+
1014
+ response = requests.post(
1015
+ api_url,
1016
+ json=deploy_data,
1017
+ timeout=self.timeout
1018
+ or 60, # Extended timeout for folder uploads
1019
+ allow_redirects=False,
1020
+ headers={'Content-Type': 'application/json'},
1021
+ )
1022
+
1023
+ if response.status_code == 200:
1024
+ result = response.json()
1025
+
1026
+ # Build URLs
1027
+ base_url = f"http://{self.remote_server_ip}:{self.remote_server_port}"
1028
+ deployed_url = (
1029
+ f"{base_url}/{subdirectory}/"
1030
+ if subdirectory
1031
+ else base_url
1032
+ )
1033
+
1034
+ return {
1035
+ 'success': True,
1036
+ 'remote_url': deployed_url,
1037
+ 'server_ip': self.remote_server_ip,
1038
+ 'subdirectory': subdirectory,
1039
+ 'domain': domain,
1040
+ 'message': (
1041
+ f'Successfully deployed folder to remote server!\n'
1042
+ f' • Access URL: {deployed_url}\n'
1043
+ f' • Server: '
1044
+ f'{self.remote_server_ip}:{self.remote_server_port}'
1045
+ ),
1046
+ 'branding_tag_added': self.add_branding_tag,
1047
+ 'logo_processed': result.get('logo_processed', False),
1048
+ }
1049
+ else:
1050
+ return {
1051
+ 'success': False,
1052
+ 'error': (
1053
+ f'Remote folder deployment failed: '
1054
+ f'HTTP {response.status_code}'
1055
+ ),
1056
+ }
1057
+
1058
+ finally:
1059
+ # Clean up temporary zip file
1060
+ if os.path.exists(zip_path):
1061
+ os.unlink(zip_path)
1062
+
1063
+ except ImportError:
1064
+ return {
1065
+ 'success': False,
1066
+ 'error': 'Remote deployment requires requests library. '
1067
+ 'Install with: pip install requests',
1068
+ }
1069
+ except Exception as e:
1070
+ return {
1071
+ 'success': False,
1072
+ 'error': f'Remote folder deployment error: {e!s}',
1073
+ }
1074
+
880
1075
  def stop_server(self, port: int) -> Dict[str, Any]:
881
1076
  r"""Stop a running server on the specified port.
882
1077
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: camel-ai
3
- Version: 0.2.73a2
3
+ Version: 0.2.73a4
4
4
  Summary: Communicative Agents for AI Society Study
5
5
  Project-URL: Homepage, https://www.camel-ai.org/
6
6
  Project-URL: Repository, https://github.com/camel-ai/camel
@@ -124,6 +124,7 @@ Requires-Dist: slack-bolt<2,>=1.20.1; extra == 'all'
124
124
  Requires-Dist: slack-sdk<4,>=3.27.2; extra == 'all'
125
125
  Requires-Dist: soundfile<0.14,>=0.13; extra == 'all'
126
126
  Requires-Dist: stripe<12,>=11.3.0; extra == 'all'
127
+ Requires-Dist: surrealdb>=1.0.6; extra == 'all'
127
128
  Requires-Dist: sympy<2,>=1.13.3; extra == 'all'
128
129
  Requires-Dist: tabulate>=0.9.0; extra == 'all'
129
130
  Requires-Dist: tavily-python<0.6,>=0.5.0; extra == 'all'
@@ -336,6 +337,7 @@ Requires-Dist: pyobvector>=0.1.18; extra == 'storage'
336
337
  Requires-Dist: pytidb-experimental==0.0.1.dev4; extra == 'storage'
337
338
  Requires-Dist: qdrant-client<2,>=1.9.0; extra == 'storage'
338
339
  Requires-Dist: redis<6,>=5.0.6; extra == 'storage'
340
+ Requires-Dist: surrealdb>=1.0.6; extra == 'storage'
339
341
  Requires-Dist: weaviate-client>=4.15.0; extra == 'storage'
340
342
  Provides-Extra: web-tools
341
343
  Requires-Dist: apify-client<2,>=1.8.1; extra == 'web-tools'