camel-ai 0.2.73a2__py3-none-any.whl → 0.2.73a3__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 camel-ai might be problematic. Click here for more details.

camel/__init__.py CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  from camel.logger import disable_logging, enable_logging, set_log_level
16
16
 
17
- __version__ = '0.2.73a2'
17
+ __version__ = '0.2.73a3'
18
18
 
19
19
  __all__ = [
20
20
  '__version__',
@@ -14,6 +14,7 @@
14
14
 
15
15
  # Enables postponed evaluation of annotations (for string-based type hints)
16
16
  import os
17
+ from pathlib import Path
17
18
  from typing import TYPE_CHECKING, List, Optional, Union
18
19
 
19
20
  from camel.logger import get_logger
@@ -41,7 +42,7 @@ class ExcelToolkit(BaseToolkit):
41
42
  def __init__(
42
43
  self,
43
44
  timeout: Optional[float] = None,
44
- file_path: Optional[str] = None,
45
+ working_directory: Optional[str] = None,
45
46
  ):
46
47
  r"""Initializes a new instance of the ExcelToolkit class.
47
48
 
@@ -49,16 +50,28 @@ class ExcelToolkit(BaseToolkit):
49
50
  timeout (Optional[float]): The timeout value for API requests
50
51
  in seconds. If None, no timeout is applied.
51
52
  (default: :obj:`None`)
52
- file_path (Optional[str]): Path to an existing Excel file to load.
53
- (default: :obj:`None`)
53
+ working_directory (str, optional): The default directory for
54
+ output files. If not provided, it will be determined by the
55
+ `CAMEL_WORKDIR` environment variable (if set). If the
56
+ environment variable is not set, it defaults to
57
+ `camel_working_dir`.
54
58
  """
55
59
  super().__init__(timeout=timeout)
56
- self.file_path = file_path
57
60
  self.wb = None
58
- if file_path and os.path.exists(file_path):
59
- from openpyxl import load_workbook
61
+ if working_directory:
62
+ self.working_directory = Path(working_directory).resolve()
63
+ else:
64
+ camel_workdir = os.environ.get("CAMEL_WORKDIR")
65
+ if camel_workdir:
66
+ self.working_directory = Path(camel_workdir).resolve()
67
+ else:
68
+ self.working_directory = Path("./camel_working_dir").resolve()
60
69
 
61
- self.wb = load_workbook(file_path)
70
+ self.working_directory.mkdir(parents=True, exist_ok=True)
71
+ logger.info(
72
+ f"ExcelToolkit initialized with output directory: "
73
+ f"{self.working_directory}"
74
+ )
62
75
 
63
76
  def _validate_file_path(self, file_path: str) -> bool:
64
77
  r"""Validate file path for security.
@@ -232,12 +245,11 @@ class ExcelToolkit(BaseToolkit):
232
245
  logger.error(f"Failed to process Excel file {document_path}: {e}")
233
246
  return f"Failed to process Excel file {document_path}: {e}"
234
247
 
235
- def _save_workbook(self, file_path: Optional[str] = None) -> str:
248
+ def _save_workbook(self, file_path: str) -> str:
236
249
  r"""Save the current workbook to file.
237
250
 
238
251
  Args:
239
- file_path (Optional[str]): The path to save the workbook.
240
- If None, uses self.file_path.
252
+ file_path (str): The path to save the workbook.
241
253
 
242
254
  Returns:
243
255
  str: Success or error message.
@@ -245,23 +257,46 @@ class ExcelToolkit(BaseToolkit):
245
257
  if not self.wb:
246
258
  return "Error: No workbook loaded to save."
247
259
 
248
- save_path = file_path or self.file_path
249
- if not save_path:
250
- return "Error: No file path specified for saving."
251
-
252
- if not self._validate_file_path(save_path):
260
+ if not self._validate_file_path(file_path):
253
261
  return "Error: Invalid file path for saving."
254
262
 
255
263
  try:
256
- self.wb.save(save_path)
257
- return f"Workbook saved successfully to {save_path}"
264
+ self.wb.save(file_path)
265
+ return f"Workbook saved successfully to {file_path}"
258
266
  except Exception as e:
259
267
  logger.error(f"Failed to save workbook: {e}")
260
268
  return f"Error: Failed to save workbook: {e}"
261
269
 
270
+ def save_workbook(self, filename: str) -> str:
271
+ r"""Save the current in-memory workbook to a file.
272
+
273
+ Args:
274
+ filename (str): The filename to save the workbook. Must end with
275
+ .xlsx extension. The file will be saved in self.
276
+ working_directory.
277
+
278
+ Returns:
279
+ str: Success message or error details.
280
+ """
281
+ if not self.wb:
282
+ return "Error: No workbook is currently loaded in memory."
283
+
284
+ # Validate filename
285
+ if not filename:
286
+ return "Error: Filename is required."
287
+
288
+ if not filename.endswith('.xlsx'):
289
+ return "Error: Filename must end with .xlsx extension."
290
+
291
+ # Create full path in working directory
292
+ file_path = self.working_directory / filename
293
+ resolved_file_path = str(file_path.resolve())
294
+
295
+ return self._save_workbook(resolved_file_path)
296
+
262
297
  def create_workbook(
263
298
  self,
264
- file_path: str,
299
+ filename: Optional[str] = None,
265
300
  sheet_name: Optional[str] = None,
266
301
  data: Optional[List[List[Union[str, int, float, None]]]] = None,
267
302
  ) -> str:
@@ -271,8 +306,9 @@ class ExcelToolkit(BaseToolkit):
271
306
  toolkit to work with the new file and optionally adds initial data.
272
307
 
273
308
  Args:
274
- file_path (str): Where to save the new Excel file. Must end with .
275
- xlsx.
309
+ filename (Optional[str]): The filename for the workbook. Must end
310
+ with .xlsx extension. The file will be saved in
311
+ self.working_directory. (default: :obj:`None`)
276
312
  sheet_name (Optional[str]): Name for the first sheet. If None,
277
313
  creates "Sheet1". (default: :obj:`None`)
278
314
  data (Optional[List[List[Union[str, int, float, None]]]]): Initial
@@ -284,14 +320,31 @@ class ExcelToolkit(BaseToolkit):
284
320
  """
285
321
  from openpyxl import Workbook
286
322
 
287
- if not self._validate_file_path(file_path):
323
+ # Validate filename
324
+ if filename is None:
325
+ return "Error: Filename is required."
326
+
327
+ if not filename.endswith('.xlsx'):
328
+ return "Error: Filename must end with .xlsx extension."
329
+
330
+ # Create full path in working directory
331
+ file_path = self.working_directory / filename
332
+ resolved_file_path = str(file_path.resolve())
333
+
334
+ if not self._validate_file_path(resolved_file_path):
288
335
  return "Error: Invalid file path."
289
336
 
337
+ # Check if file already exists
338
+ if os.path.exists(resolved_file_path):
339
+ return (
340
+ f"Error: File {filename} already exists in "
341
+ f"{self.working_directory}."
342
+ )
343
+
290
344
  try:
291
345
  # Create a new workbook
292
346
  wb = Workbook()
293
347
  self.wb = wb
294
- self.file_path = file_path
295
348
 
296
349
  # Handle sheet creation safely
297
350
  if sheet_name:
@@ -312,42 +365,55 @@ class ExcelToolkit(BaseToolkit):
312
365
  ws.append(row)
313
366
 
314
367
  # Save the workbook to the specified file path
315
- wb.save(file_path)
368
+ wb.save(resolved_file_path)
316
369
 
317
- return f"Workbook created successfully at {file_path}"
370
+ return f"Workbook created successfully at {resolved_file_path}"
318
371
  except Exception as e:
319
372
  logger.error(f"Failed to create workbook: {e}")
320
373
  return f"Error: Failed to create workbook: {e}"
321
374
 
322
- def delete_workbook(self, file_path: Optional[str] = None) -> str:
323
- r"""Delete a spreadsheet file.
375
+ def delete_workbook(self, filename: str) -> str:
376
+ r"""Delete a spreadsheet file from the working directory.
324
377
 
325
378
  Args:
326
- file_path (Optional[str]): The path of the file to delete.
327
- If None, uses self.file_path.
379
+ filename (str): The filename to delete. Must end with .xlsx
380
+ extension. The file will be deleted from self.
381
+ working_directory.
328
382
 
329
383
  Returns:
330
- str: Success message.
384
+ str: Success message or error details.
331
385
  """
332
- target_path = file_path or self.file_path
333
- if not target_path:
334
- return "Error: No file path specified for deletion."
386
+ # Validate filename
387
+ if not filename:
388
+ return "Error: Filename is required."
389
+
390
+ if not filename.endswith('.xlsx'):
391
+ return "Error: Filename must end with .xlsx extension."
392
+
393
+ # Create full path in working directory
394
+ file_path = self.working_directory / filename
395
+ target_path = str(file_path.resolve())
335
396
 
336
397
  if not self._validate_file_path(target_path):
337
398
  return "Error: Invalid file path."
338
399
 
339
400
  if not os.path.exists(target_path):
340
- return f"File {target_path} does not exist."
401
+ return (
402
+ f"Error: File {filename} does not exist in "
403
+ f"{self.working_directory}."
404
+ )
341
405
 
342
406
  try:
343
407
  os.remove(target_path)
344
- if target_path == self.file_path:
345
- self.wb = None
346
- self.file_path = None
347
- return f"Workbook {target_path} deleted successfully."
408
+ # Clean up workbook if one is loaded
409
+ self.wb = None
410
+ return (
411
+ f"Workbook {filename} deleted successfully from "
412
+ f"{self.working_directory}."
413
+ )
348
414
  except Exception as e:
349
415
  logger.error(f"Failed to delete workbook: {e}")
350
- return f"Failed to delete workbook {target_path}: {e}"
416
+ return f"Error: Failed to delete workbook {filename}: {e}"
351
417
 
352
418
  def create_sheet(
353
419
  self,
@@ -379,9 +445,6 @@ class ExcelToolkit(BaseToolkit):
379
445
  for row in data:
380
446
  ws.append(row)
381
447
 
382
- save_result = self._save_workbook()
383
- if save_result.startswith("Error"):
384
- return save_result
385
448
  return f"Sheet {sheet_name} created successfully."
386
449
  except Exception as e:
387
450
  logger.error(f"Failed to create sheet: {e}")
@@ -408,9 +471,6 @@ class ExcelToolkit(BaseToolkit):
408
471
  try:
409
472
  ws = self.wb[sheet_name]
410
473
  self.wb.remove(ws)
411
- save_result = self._save_workbook()
412
- if save_result.startswith("Error"):
413
- return save_result
414
474
  return f"Sheet {sheet_name} deleted successfully."
415
475
  except Exception as e:
416
476
  logger.error(f"Failed to delete sheet: {e}")
@@ -439,9 +499,6 @@ class ExcelToolkit(BaseToolkit):
439
499
  for cell in row:
440
500
  cell.value = None
441
501
 
442
- save_result = self._save_workbook()
443
- if save_result.startswith("Error"):
444
- return save_result
445
502
  return f"Sheet {sheet_name} cleared successfully."
446
503
  except Exception as e:
447
504
  logger.error(f"Failed to clear sheet: {e}")
@@ -479,7 +536,6 @@ class ExcelToolkit(BaseToolkit):
479
536
  num_rows = end_row - start_row + 1
480
537
  ws.delete_rows(start_row, num_rows)
481
538
 
482
- self._save_workbook()
483
539
  return (
484
540
  f"Deleted rows {start_row} to {end_row} from sheet "
485
541
  f"{sheet_name} successfully."
@@ -518,7 +574,6 @@ class ExcelToolkit(BaseToolkit):
518
574
  num_cols = end_col - start_col + 1
519
575
  ws.delete_cols(start_col, num_cols)
520
576
 
521
- self._save_workbook()
522
577
  return (
523
578
  f"Deleted columns {start_col} to {end_col} from sheet "
524
579
  f"{sheet_name} successfully."
@@ -585,9 +640,6 @@ class ExcelToolkit(BaseToolkit):
585
640
  ws[cell_reference].value = None
586
641
  else:
587
642
  ws[cell_reference] = value
588
- save_result = self._save_workbook()
589
- if save_result.startswith("Error"):
590
- return save_result
591
643
  return (
592
644
  f"Cell {cell_reference} updated successfully in sheet "
593
645
  f"{sheet_name}."
@@ -765,10 +817,9 @@ class ExcelToolkit(BaseToolkit):
765
817
  if col_idx < len(values[row_idx]):
766
818
  cell.value = values[row_idx][col_idx]
767
819
 
768
- self._save_workbook()
769
820
  return f"Values set for range {cell_range} in sheet {sheet_name}."
770
821
 
771
- def export_sheet_to_csv(self, sheet_name: str, csv_path: str) -> str:
822
+ def export_sheet_to_csv(self, sheet_name: str, csv_filename: str) -> str:
772
823
  r"""Export a specific sheet to CSV format.
773
824
 
774
825
  Use this to convert Excel sheets to CSV files for compatibility or
@@ -776,24 +827,63 @@ class ExcelToolkit(BaseToolkit):
776
827
 
777
828
  Args:
778
829
  sheet_name (str): Name of the sheet to export.
779
- csv_path (str): File path where CSV will be saved.
830
+ csv_filename (str): Filename for the CSV file. Must end with .csv
831
+ extension. The file will be saved in self.working_directory.
780
832
 
781
833
  Returns:
782
834
  str: Success confirmation message or error details.
783
835
  """
784
836
  if not self.wb:
785
- return "Error: Workbook not initialized."
837
+ return (
838
+ "Error: No workbook is currently loaded. Use "
839
+ "extract_excel_content to load a workbook first."
840
+ )
786
841
 
787
842
  if sheet_name not in self.wb.sheetnames:
788
- return f"Error: Sheet {sheet_name} does not exist."
843
+ return (
844
+ f"Error: Sheet {sheet_name} does not exist in the current "
845
+ "workbook."
846
+ )
789
847
 
790
- import pandas as pd
848
+ # Validate filename
849
+ if not csv_filename:
850
+ return "Error: CSV filename is required."
791
851
 
792
- # Read the specific sheet
793
- df = pd.read_excel(self.file_path, sheet_name=sheet_name)
794
- df.to_csv(csv_path, index=False)
852
+ if not csv_filename.endswith('.csv'):
853
+ return "Error: CSV filename must end with .csv extension."
795
854
 
796
- return f"Sheet {sheet_name} exported to CSV: {csv_path}"
855
+ # Create full path in working directory
856
+ csv_path = self.working_directory / csv_filename
857
+ resolved_csv_path = str(csv_path.resolve())
858
+
859
+ if not self._validate_file_path(resolved_csv_path):
860
+ return "Error: Invalid file path."
861
+
862
+ try:
863
+ # Get the worksheet
864
+ ws = self.wb[sheet_name]
865
+
866
+ # Convert worksheet to list of lists
867
+ data = []
868
+ for row in ws.iter_rows(values_only=True):
869
+ data.append(list(row))
870
+
871
+ # Write to CSV
872
+ import csv
873
+
874
+ with open(
875
+ resolved_csv_path, 'w', newline='', encoding='utf-8'
876
+ ) as csvfile:
877
+ writer = csv.writer(csvfile)
878
+ writer.writerows(data)
879
+
880
+ return (
881
+ f"Sheet {sheet_name} exported to {csv_filename} "
882
+ f"in {self.working_directory}."
883
+ )
884
+ except Exception as e:
885
+ logger.error(f"Failed to export sheet to CSV: {e}")
886
+ return f"Error: Failed to export sheet {sheet_name} to CSV: {e}"
797
887
 
798
888
  def get_rows(
799
889
  self,
@@ -862,7 +952,6 @@ class ExcelToolkit(BaseToolkit):
862
952
  return f"Error: Sheet {sheet_name} does not exist."
863
953
  ws = self.wb[sheet_name]
864
954
  ws.append(row_data)
865
- self._save_workbook()
866
955
  return f"Row appended to sheet {sheet_name} successfully."
867
956
 
868
957
  def update_row(
@@ -901,7 +990,6 @@ class ExcelToolkit(BaseToolkit):
901
990
  for col_idx, value in enumerate(row_data, 1):
902
991
  ws.cell(row=row_number, column=col_idx).value = value
903
992
 
904
- self._save_workbook()
905
993
  return f"Row {row_number} updated in sheet {sheet_name} successfully."
906
994
 
907
995
  def get_tools(self) -> List[FunctionTool]:
@@ -916,6 +1004,7 @@ class ExcelToolkit(BaseToolkit):
916
1004
  # File operations
917
1005
  FunctionTool(self.extract_excel_content),
918
1006
  FunctionTool(self.create_workbook),
1007
+ FunctionTool(self.save_workbook),
919
1008
  FunctionTool(self.delete_workbook),
920
1009
  FunctionTool(self.export_sheet_to_csv),
921
1010
  # Sheet operations
@@ -110,6 +110,11 @@ class WebSocketBrowserWrapper:
110
110
  self.process: Optional[subprocess.Popen] = None
111
111
  self.websocket = None
112
112
  self.server_port = None
113
+ self._send_lock = asyncio.Lock() # Lock for sending messages
114
+ self._receive_task = None # Background task for receiving messages
115
+ self._pending_responses: Dict[
116
+ str, asyncio.Future[Dict[str, Any]]
117
+ ] = {} # Message ID -> Future
113
118
 
114
119
  # Logging configuration
115
120
  self.browser_log_to_file = (config or {}).get(
@@ -251,11 +256,22 @@ class WebSocketBrowserWrapper:
251
256
  f"Failed to connect to WebSocket server: {e}"
252
257
  ) from e
253
258
 
259
+ # Start the background receiver task
260
+ self._receive_task = asyncio.create_task(self._receive_loop())
261
+
254
262
  # Initialize the browser toolkit
255
263
  await self._send_command('init', self.config)
256
264
 
257
265
  async def stop(self):
258
266
  """Stop the WebSocket connection and server."""
267
+ # Cancel the receiver task
268
+ if self._receive_task and not self._receive_task.done():
269
+ self._receive_task.cancel()
270
+ try:
271
+ await self._receive_task
272
+ except asyncio.CancelledError:
273
+ pass
274
+
259
275
  if self.websocket:
260
276
  try:
261
277
  await self._send_command('shutdown', {})
@@ -327,6 +343,39 @@ class WebSocketBrowserWrapper:
327
343
  except Exception as e:
328
344
  logger.error(f"Failed to write to log file: {e}")
329
345
 
346
+ async def _receive_loop(self):
347
+ r"""Background task to receive messages from WebSocket."""
348
+ try:
349
+ while self.websocket:
350
+ try:
351
+ response_data = await self.websocket.recv()
352
+ response = json.loads(response_data)
353
+
354
+ message_id = response.get('id')
355
+ if message_id and message_id in self._pending_responses:
356
+ # Set the result for the waiting coroutine
357
+ future = self._pending_responses.pop(message_id)
358
+ if not future.done():
359
+ future.set_result(response)
360
+ else:
361
+ # Log unexpected messages
362
+ logger.warning(
363
+ f"Received unexpected message: {response}"
364
+ )
365
+
366
+ except asyncio.CancelledError:
367
+ break
368
+ except Exception as e:
369
+ logger.error(f"Error in receive loop: {e}")
370
+ # Notify all pending futures of the error
371
+ for future in self._pending_responses.values():
372
+ if not future.done():
373
+ future.set_exception(e)
374
+ self._pending_responses.clear()
375
+ break
376
+ finally:
377
+ logger.debug("Receive loop terminated")
378
+
330
379
  async def _ensure_connection(self) -> None:
331
380
  """Ensure WebSocket connection is alive."""
332
381
  if not self.websocket:
@@ -350,39 +399,39 @@ class WebSocketBrowserWrapper:
350
399
  message_id = str(uuid.uuid4())
351
400
  message = {'id': message_id, 'command': command, 'params': params}
352
401
 
353
- try:
354
- # Send command
355
- if self.websocket is None:
356
- raise RuntimeError("WebSocket connection not established")
357
- await self.websocket.send(json.dumps(message))
402
+ # Create a future for this message
403
+ future: asyncio.Future[Dict[str, Any]] = asyncio.Future()
404
+ self._pending_responses[message_id] = future
358
405
 
359
- # Wait for response with matching ID
360
- while True:
361
- try:
362
- if self.websocket is None:
363
- raise RuntimeError("WebSocket connection lost")
364
- response_data = await asyncio.wait_for(
365
- self.websocket.recv(), timeout=60.0
366
- )
367
- response = json.loads(response_data)
368
-
369
- # Check if this is the response we're waiting for
370
- if response.get('id') == message_id:
371
- if not response.get('success'):
372
- raise RuntimeError(
373
- f"Command failed: {response.get('error')}"
374
- )
375
- return response['result']
406
+ try:
407
+ # Use lock only for sending to prevent interleaved messages
408
+ async with self._send_lock:
409
+ if self.websocket is None:
410
+ raise RuntimeError("WebSocket connection not established")
411
+ await self.websocket.send(json.dumps(message))
412
+
413
+ # Wait for response (no lock needed, handled by background
414
+ # receiver)
415
+ try:
416
+ response = await asyncio.wait_for(future, timeout=60.0)
376
417
 
377
- except asyncio.TimeoutError:
418
+ if not response.get('success'):
378
419
  raise RuntimeError(
379
- f"Timeout waiting for response to command: {command}"
420
+ f"Command failed: {response.get('error')}"
380
421
  )
381
- except json.JSONDecodeError as e:
382
- logger.warning(f"Failed to decode WebSocket response: {e}")
383
- continue
422
+ return response['result']
423
+
424
+ except asyncio.TimeoutError:
425
+ # Remove from pending if timeout
426
+ self._pending_responses.pop(message_id, None)
427
+ raise RuntimeError(
428
+ f"Timeout waiting for response to command: {command}"
429
+ )
384
430
 
385
431
  except Exception as e:
432
+ # Clean up the pending response
433
+ self._pending_responses.pop(message_id, None)
434
+
386
435
  # Check if it's a connection closed error
387
436
  if (
388
437
  "close frame" in str(e)
@@ -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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: camel-ai
3
- Version: 0.2.73a2
3
+ Version: 0.2.73a3
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
@@ -1,4 +1,4 @@
1
- camel/__init__.py,sha256=pn3-Lfew1wFDh_51dBOk0tY-qRlN-W_XoCGsZ9kOZ7o,901
1
+ camel/__init__.py,sha256=LY1yvi28uSSDg8ifjgLeFiCqzATMOyd5fXqDmkOmEAo,901
2
2
  camel/generators.py,sha256=JRqj9_m1PF4qT6UtybzTQ-KBT9MJQt18OAAYvQ_fr2o,13844
3
3
  camel/human.py,sha256=Xg8x1cS5KK4bQ1SDByiHZnzsRpvRP-KZViNvmu38xo4,5475
4
4
  camel/logger.py,sha256=WgEwael_eT6D-lVAKHpKIpwXSTjvLbny5jbV1Ab8lnA,5760
@@ -332,7 +332,7 @@ camel/toolkits/craw4ai_toolkit.py,sha256=av8mqY68QgMSm27htnSdq0aqE6z3yWMVDSrNafQ
332
332
  camel/toolkits/dappier_toolkit.py,sha256=OEHOYXX_oXhgbVtWYAy13nO9uXf9i5qEXSwY4PexNFg,8194
333
333
  camel/toolkits/data_commons_toolkit.py,sha256=aHZUSL1ACpnYGaf1rE2csVKTmXTmN8lMGRUBYhZ_YEk,14168
334
334
  camel/toolkits/edgeone_pages_mcp_toolkit.py,sha256=1TFpAGHUNLggFQeN1OEw7P5laijwnlrCkfxBtgxFuUY,2331
335
- camel/toolkits/excel_toolkit.py,sha256=9Uk5GLWl719c4W-NcGPJTNMtodAbEE5gUgLsFkIInbk,32564
335
+ camel/toolkits/excel_toolkit.py,sha256=tQaonygk0yDTPZHWWQKG5osTN-R_EawR0bJIKLsLg08,35768
336
336
  camel/toolkits/file_write_toolkit.py,sha256=d8N8FfmK1fS13sY58PPhJh6M0vq6yh-s1-ltCZQJObg,37044
337
337
  camel/toolkits/function_tool.py,sha256=3_hE-Khqf556CeebchsPpjIDCynC6vKmUJLdh1EO_js,34295
338
338
  camel/toolkits/github_toolkit.py,sha256=iUyRrjWGAW_iljZVfNyfkm1Vi55wJxK6PsDAQs9pOag,13099
@@ -351,7 +351,7 @@ camel/toolkits/mcp_toolkit.py,sha256=da7QLwGKIKnKvMx5mOOiC56w0hKV1bvD1Z9PgrSHOtA
351
351
  camel/toolkits/memory_toolkit.py,sha256=TeKYd5UMwgjVpuS2orb-ocFL13eUNKujvrFOruDCpm8,4436
352
352
  camel/toolkits/meshy_toolkit.py,sha256=NbgdOBD3FYLtZf-AfonIv6-Q8-8DW129jsaP1PqI2rs,7126
353
353
  camel/toolkits/message_agent_toolkit.py,sha256=yWvAaxoxAvDEtD7NH7IkkHIyfWIYK47WZhn5E_RaxKo,22661
354
- camel/toolkits/message_integration.py,sha256=WdcoVoDAPwlvfXK26wHBf2q_IQCH4PQ_gJtyqUViVvE,23213
354
+ camel/toolkits/message_integration.py,sha256=-dcf91DJzj8asXP2cc7mMy1BH2xzifhH-MMGr_nvJGw,28730
355
355
  camel/toolkits/mineru_toolkit.py,sha256=vRX9LholLNkpbJ6axfEN4pTG85aWb0PDmlVy3rAAXhg,6868
356
356
  camel/toolkits/networkx_toolkit.py,sha256=C7pUCZTzzGkFyqdkrmhRKpAHmHWfLKeuzYHC_BHPtbk,8826
357
357
  camel/toolkits/note_taking_toolkit.py,sha256=cp7uoSBMjiGy331Tdk2Bl6yqKSMGwws7rQJkq8tfTQs,10687
@@ -392,7 +392,7 @@ camel/toolkits/hybrid_browser_toolkit/__init__.py,sha256=vxjWhq7GjUKE5I9RGQU_Goi
392
392
  camel/toolkits/hybrid_browser_toolkit/config_loader.py,sha256=UwBh7wG4-owoI2VfiNMum0O7dPWMYiEDxQKtq_GazU4,6903
393
393
  camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py,sha256=gotOOlXJjfjv9Qnn89PLNhJ4_Rw_aMMU6gTJcG-uCf8,7938
394
394
  camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py,sha256=8ArSGFCx1bIrZR8cdRVUX6axy5Fxgk5ADEgiSrPQ_Bo,45269
395
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py,sha256=5wssGj2LvREtyl91lC5pTIb0G7DZlFvLT_Pn-7XPESY,18460
395
+ camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py,sha256=gyJsqHDvvpXa86zxqn8nsitS0QYqwmIHGgvPmcxVsYc,20445
396
396
  camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json,sha256=_-YE9S_C1XT59A6upQp9lLuZcC67cV9QlbwAsEKkfyw,156337
397
397
  camel/toolkits/hybrid_browser_toolkit/ts/package.json,sha256=pUQm0xwXR7ZyWNv6O2QtHW00agnfAoX9F_XGXZlAxl4,745
398
398
  camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json,sha256=SwpQnq4Q-rwRobF2iWrP96mgmgwaVPZEv-nii5QIYEU,523
@@ -467,7 +467,7 @@ camel/verifiers/math_verifier.py,sha256=tA1D4S0sm8nsWISevxSN0hvSVtIUpqmJhzqfbuMo
467
467
  camel/verifiers/models.py,sha256=GdxYPr7UxNrR1577yW4kyroRcLGfd-H1GXgv8potDWU,2471
468
468
  camel/verifiers/physics_verifier.py,sha256=c1grrRddcrVN7szkxhv2QirwY9viIRSITWeWFF5HmLs,30187
469
469
  camel/verifiers/python_verifier.py,sha256=ogTz77wODfEcDN4tMVtiSkRQyoiZbHPY2fKybn59lHw,20558
470
- camel_ai-0.2.73a2.dist-info/METADATA,sha256=cZbuO7_djNwJRLDIsjoopizSes24a2pjAewf7XM-u0A,50334
471
- camel_ai-0.2.73a2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
472
- camel_ai-0.2.73a2.dist-info/licenses/LICENSE,sha256=id0nB2my5kG0xXeimIu5zZrbHLS6EQvxvkKkzIHaT2k,11343
473
- camel_ai-0.2.73a2.dist-info/RECORD,,
470
+ camel_ai-0.2.73a3.dist-info/METADATA,sha256=vy-EgY3IPDzVVc5UNfT7MQvrWqvpBZ1ZUPGQb-bXOrA,50334
471
+ camel_ai-0.2.73a3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
472
+ camel_ai-0.2.73a3.dist-info/licenses/LICENSE,sha256=id0nB2my5kG0xXeimIu5zZrbHLS6EQvxvkKkzIHaT2k,11343
473
+ camel_ai-0.2.73a3.dist-info/RECORD,,