camel-ai 0.2.69a4__py3-none-any.whl → 0.2.69a7__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.

@@ -13,9 +13,8 @@
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
 
15
15
  # Enables postponed evaluation of annotations (for string-based type hints)
16
- from __future__ import annotations
17
-
18
- from typing import TYPE_CHECKING, List, Optional
16
+ import os
17
+ from typing import TYPE_CHECKING, List, Optional, Union
19
18
 
20
19
  from camel.logger import get_logger
21
20
  from camel.toolkits.base import BaseToolkit
@@ -24,7 +23,7 @@ from camel.utils import MCPServer
24
23
 
25
24
  # Import only for type hints (not executed at runtime)
26
25
  if TYPE_CHECKING:
27
- import pandas as pd
26
+ from pandas import DataFrame
28
27
 
29
28
  logger = get_logger(__name__)
30
29
 
@@ -42,6 +41,7 @@ class ExcelToolkit(BaseToolkit):
42
41
  def __init__(
43
42
  self,
44
43
  timeout: Optional[float] = None,
44
+ file_path: Optional[str] = None,
45
45
  ):
46
46
  r"""Initializes a new instance of the ExcelToolkit class.
47
47
 
@@ -49,14 +49,38 @@ class ExcelToolkit(BaseToolkit):
49
49
  timeout (Optional[float]): The timeout value for API requests
50
50
  in seconds. If None, no timeout is applied.
51
51
  (default: :obj:`None`)
52
+ file_path (Optional[str]): Path to an existing Excel file to load.
53
+ (default: :obj:`None`)
52
54
  """
53
55
  super().__init__(timeout=timeout)
56
+ self.file_path = file_path
57
+ self.wb = None
58
+ if file_path and os.path.exists(file_path):
59
+ from openpyxl import load_workbook
60
+
61
+ self.wb = load_workbook(file_path)
62
+
63
+ def _validate_file_path(self, file_path: str) -> bool:
64
+ r"""Validate file path for security.
65
+
66
+ Args:
67
+ file_path (str): The file path to validate.
68
+
69
+ Returns:
70
+ bool: True if path is safe, False otherwise.
71
+ """
72
+ normalized_path = os.path.normpath(file_path)
73
+
74
+ if '..' in normalized_path.split(os.path.sep):
75
+ return False
76
+
77
+ return True
54
78
 
55
- def _convert_to_markdown(self, df: pd.DataFrame) -> str:
79
+ def _convert_to_markdown(self, df: "DataFrame") -> str:
56
80
  r"""Convert DataFrame to Markdown format table.
57
81
 
58
82
  Args:
59
- df (pd.DataFrame): DataFrame containing the Excel data.
83
+ df (DataFrame): DataFrame containing the Excel data.
60
84
 
61
85
  Returns:
62
86
  str: Markdown formatted table.
@@ -67,14 +91,22 @@ class ExcelToolkit(BaseToolkit):
67
91
  return str(md_table)
68
92
 
69
93
  def extract_excel_content(self, document_path: str) -> str:
70
- r"""Extract detailed cell information from an Excel file, including
71
- multiple sheets.
94
+ r"""Extract and analyze the full content of an Excel file (.xlsx/.xls/.
95
+ csv).
96
+
97
+ Use this tool to read and understand the structure and content of
98
+ Excel files. This is typically the first step when working with
99
+ existing Excel files.
72
100
 
73
101
  Args:
74
- document_path (str): The path of the Excel file.
102
+ document_path (str): The file path to the Excel file.
75
103
 
76
104
  Returns:
77
- str: Extracted excel information, including details of each sheet.
105
+ str: A comprehensive report containing:
106
+ - Sheet names and their content in markdown table format
107
+ - Detailed cell information including values, colors, and
108
+ positions
109
+ - Formatted data that's easy to understand and analyze
78
110
  """
79
111
  import pandas as pd
80
112
  from openpyxl import load_workbook
@@ -85,6 +117,9 @@ class ExcelToolkit(BaseToolkit):
85
117
  f": {document_path}"
86
118
  )
87
119
 
120
+ if not self._validate_file_path(document_path):
121
+ return "Error: Invalid file path."
122
+
88
123
  if not (
89
124
  document_path.endswith("xls")
90
125
  or document_path.endswith("xlsx")
@@ -96,6 +131,9 @@ class ExcelToolkit(BaseToolkit):
96
131
  f"It is not excel format. Please try other ways."
97
132
  )
98
133
 
134
+ if not os.path.exists(document_path):
135
+ return f"Error: File {document_path} does not exist."
136
+
99
137
  if document_path.endswith("csv"):
100
138
  try:
101
139
  df = pd.read_csv(document_path)
@@ -111,64 +149,74 @@ class ExcelToolkit(BaseToolkit):
111
149
  x2x.to_xlsx(output_path)
112
150
  document_path = output_path
113
151
 
114
- # Load the Excel workbook
115
- wb = load_workbook(document_path, data_only=True)
116
- sheet_info_list = []
117
-
118
- # Iterate through all sheets
119
- for sheet in wb.sheetnames:
120
- ws = wb[sheet]
121
- cell_info_list = []
122
-
123
- for row in ws.iter_rows():
124
- for cell in row:
125
- row_num = cell.row
126
- col_letter = cell.column_letter
127
-
128
- cell_value = cell.value
129
-
130
- font_color = None
131
- if (
132
- cell.font
133
- and cell.font.color
134
- and "rgb=None" not in str(cell.font.color)
135
- ): # Handle font color
136
- font_color = cell.font.color.rgb
137
-
138
- fill_color = None
139
- if (
140
- cell.fill
141
- and cell.fill.fgColor
142
- and "rgb=None" not in str(cell.fill.fgColor)
143
- ): # Handle fill color
144
- fill_color = cell.fill.fgColor.rgb
145
-
146
- cell_info_list.append(
147
- {
148
- "index": f"{row_num}{col_letter}",
149
- "value": cell_value,
150
- "font_color": font_color,
151
- "fill_color": fill_color,
152
- }
153
- )
154
-
155
- # Convert the sheet to a DataFrame and then to markdown
156
- sheet_df = pd.read_excel(
157
- document_path, sheet_name=sheet, engine='openpyxl'
158
- )
159
- markdown_content = self._convert_to_markdown(sheet_df)
160
-
161
- # Collect all information for the sheet
162
- sheet_info = {
163
- "sheet_name": sheet,
164
- "cell_info_list": cell_info_list,
165
- "markdown_content": markdown_content,
166
- }
167
- sheet_info_list.append(sheet_info)
168
-
169
- result_str = ""
170
- for sheet_info in sheet_info_list:
171
- result_str += f"""
152
+ try:
153
+ # Load the Excel workbook
154
+ wb = load_workbook(document_path, data_only=True)
155
+ sheet_info_list = []
156
+
157
+ # Iterate through all sheets
158
+ for sheet in wb.sheetnames:
159
+ ws = wb[sheet]
160
+ cell_info_list = []
161
+
162
+ for row in ws.iter_rows():
163
+ for cell in row:
164
+ # Skip cells that don't have proper coordinates (like
165
+ # merged cells)
166
+ if (
167
+ not hasattr(cell, 'column_letter')
168
+ or cell.value is None
169
+ ):
170
+ continue
171
+
172
+ row_num = cell.row
173
+ # Use getattr with fallback for column_letter
174
+ col_letter = getattr(cell, 'column_letter', 'A')
175
+
176
+ cell_value = cell.value
177
+
178
+ font_color = None
179
+ if (
180
+ cell.font
181
+ and cell.font.color
182
+ and "rgb=None" not in str(cell.font.color)
183
+ ): # Handle font color
184
+ font_color = cell.font.color.rgb
185
+
186
+ fill_color = None
187
+ if (
188
+ cell.fill
189
+ and cell.fill.fgColor
190
+ and "rgb=None" not in str(cell.fill.fgColor)
191
+ ): # Handle fill color
192
+ fill_color = cell.fill.fgColor.rgb
193
+
194
+ cell_info_list.append(
195
+ {
196
+ "index": f"{row_num}{col_letter}",
197
+ "value": cell_value,
198
+ "font_color": font_color,
199
+ "fill_color": fill_color,
200
+ }
201
+ )
202
+
203
+ # Convert the sheet to a DataFrame and then to markdown
204
+ sheet_df = pd.read_excel(
205
+ document_path, sheet_name=sheet, engine='openpyxl'
206
+ )
207
+ markdown_content = self._convert_to_markdown(sheet_df)
208
+
209
+ # Collect all information for the sheet
210
+ sheet_info = {
211
+ "sheet_name": sheet,
212
+ "cell_info_list": cell_info_list,
213
+ "markdown_content": markdown_content,
214
+ }
215
+ sheet_info_list.append(sheet_info)
216
+
217
+ result_str = ""
218
+ for sheet_info in sheet_info_list:
219
+ result_str += f"""
172
220
  Sheet Name: {sheet_info['sheet_name']}
173
221
  Cell information list:
174
222
  {sheet_info['cell_info_list']}
@@ -179,7 +227,682 @@ class ExcelToolkit(BaseToolkit):
179
227
  {'-'*40}
180
228
  """
181
229
 
182
- return result_str
230
+ return result_str
231
+ except Exception as e:
232
+ logger.error(f"Failed to process Excel file {document_path}: {e}")
233
+ return f"Failed to process Excel file {document_path}: {e}"
234
+
235
+ def _save_workbook(self, file_path: Optional[str] = None) -> str:
236
+ r"""Save the current workbook to file.
237
+
238
+ Args:
239
+ file_path (Optional[str]): The path to save the workbook.
240
+ If None, uses self.file_path.
241
+
242
+ Returns:
243
+ str: Success or error message.
244
+ """
245
+ if not self.wb:
246
+ return "Error: No workbook loaded to save."
247
+
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):
253
+ return "Error: Invalid file path for saving."
254
+
255
+ try:
256
+ self.wb.save(save_path)
257
+ return f"Workbook saved successfully to {save_path}"
258
+ except Exception as e:
259
+ logger.error(f"Failed to save workbook: {e}")
260
+ return f"Error: Failed to save workbook: {e}"
261
+
262
+ def create_workbook(
263
+ self,
264
+ file_path: str,
265
+ sheet_name: Optional[str] = None,
266
+ data: Optional[List[List[Union[str, int, float, None]]]] = None,
267
+ ) -> str:
268
+ r"""Create a new Excel workbook from scratch.
269
+
270
+ Use this when you need to create a new Excel file. This sets up the
271
+ toolkit to work with the new file and optionally adds initial data.
272
+
273
+ Args:
274
+ file_path (str): Where to save the new Excel file. Must end with .
275
+ xlsx.
276
+ sheet_name (Optional[str]): Name for the first sheet. If None,
277
+ creates "Sheet1". (default: :obj:`None`)
278
+ data (Optional[List[List[Union[str, int, float, None]]]]): Initial
279
+ data as rows. Each inner list is one row. (default:
280
+ :obj:`None`)
281
+
282
+ Returns:
283
+ str: Success confirmation message or error details
284
+ """
285
+ from openpyxl import Workbook
286
+
287
+ if not self._validate_file_path(file_path):
288
+ return "Error: Invalid file path."
289
+
290
+ try:
291
+ # Create a new workbook
292
+ wb = Workbook()
293
+ self.wb = wb
294
+ self.file_path = file_path
295
+
296
+ # Handle sheet creation safely
297
+ if sheet_name:
298
+ # Remove the default sheet safely
299
+ default_sheet = wb.active
300
+ if default_sheet is not None:
301
+ wb.remove(default_sheet)
302
+ ws = wb.create_sheet(sheet_name)
303
+ else:
304
+ ws = wb.active
305
+ if ws is not None and sheet_name is None:
306
+ sheet_name = "Sheet1"
307
+ ws.title = sheet_name
308
+
309
+ # Add data if provided
310
+ if data and ws is not None:
311
+ for row in data:
312
+ ws.append(row)
313
+
314
+ # Save the workbook to the specified file path
315
+ wb.save(file_path)
316
+
317
+ return f"Workbook created successfully at {file_path}"
318
+ except Exception as e:
319
+ logger.error(f"Failed to create workbook: {e}")
320
+ return f"Error: Failed to create workbook: {e}"
321
+
322
+ def delete_workbook(self, file_path: Optional[str] = None) -> str:
323
+ r"""Delete a spreadsheet file.
324
+
325
+ Args:
326
+ file_path (Optional[str]): The path of the file to delete.
327
+ If None, uses self.file_path.
328
+
329
+ Returns:
330
+ str: Success message.
331
+ """
332
+ target_path = file_path or self.file_path
333
+ if not target_path:
334
+ return "Error: No file path specified for deletion."
335
+
336
+ if not self._validate_file_path(target_path):
337
+ return "Error: Invalid file path."
338
+
339
+ if not os.path.exists(target_path):
340
+ return f"File {target_path} does not exist."
341
+
342
+ try:
343
+ 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."
348
+ except Exception as e:
349
+ logger.error(f"Failed to delete workbook: {e}")
350
+ return f"Failed to delete workbook {target_path}: {e}"
351
+
352
+ def create_sheet(
353
+ self,
354
+ sheet_name: str,
355
+ data: Optional[List[List[Union[str, int, float, None]]]] = None,
356
+ ) -> str:
357
+ r"""Create a new sheet with the given sheet name and data.
358
+
359
+ Args:
360
+ sheet_name (str): The name of the sheet to create.
361
+ data (Optional[List[List[Union[str, int, float, None]]]]):
362
+ The data to write to the sheet.
363
+
364
+ Returns:
365
+ str: Success message.
366
+ """
367
+ if not self.wb:
368
+ return (
369
+ "Error: Workbook not initialized. "
370
+ "Please create a workbook first."
371
+ )
372
+
373
+ if sheet_name in self.wb.sheetnames:
374
+ return f"Error: Sheet {sheet_name} already exists."
375
+
376
+ try:
377
+ ws = self.wb.create_sheet(sheet_name)
378
+ if data:
379
+ for row in data:
380
+ ws.append(row)
381
+
382
+ save_result = self._save_workbook()
383
+ if save_result.startswith("Error"):
384
+ return save_result
385
+ return f"Sheet {sheet_name} created successfully."
386
+ except Exception as e:
387
+ logger.error(f"Failed to create sheet: {e}")
388
+ return f"Error: Failed to create sheet {sheet_name}: {e}"
389
+
390
+ def delete_sheet(self, sheet_name: str) -> str:
391
+ r"""Delete a sheet from the workbook.
392
+
393
+ Args:
394
+ sheet_name (str): The name of the sheet to delete.
395
+
396
+ Returns:
397
+ str: Success message.
398
+ """
399
+ if not self.wb:
400
+ return "Error: Workbook not initialized."
401
+
402
+ if sheet_name not in self.wb.sheetnames:
403
+ return f"Sheet {sheet_name} does not exist."
404
+
405
+ if len(self.wb.sheetnames) == 1:
406
+ return "Cannot delete the last remaining sheet in the workbook."
407
+
408
+ try:
409
+ ws = self.wb[sheet_name]
410
+ self.wb.remove(ws)
411
+ save_result = self._save_workbook()
412
+ if save_result.startswith("Error"):
413
+ return save_result
414
+ return f"Sheet {sheet_name} deleted successfully."
415
+ except Exception as e:
416
+ logger.error(f"Failed to delete sheet: {e}")
417
+ return f"Error: Failed to delete sheet {sheet_name}: {e}"
418
+
419
+ def clear_sheet(self, sheet_name: str) -> str:
420
+ r"""Clear all data from a sheet.
421
+
422
+ Args:
423
+ sheet_name (str): The name of the sheet to clear.
424
+
425
+ Returns:
426
+ str: Success message.
427
+ """
428
+ if not self.wb:
429
+ return "Error: Workbook not initialized."
430
+
431
+ if sheet_name not in self.wb.sheetnames:
432
+ return f"Sheet {sheet_name} does not exist."
433
+
434
+ try:
435
+ ws = self.wb[sheet_name]
436
+
437
+ # Clear all cells
438
+ for row in ws.iter_rows():
439
+ for cell in row:
440
+ cell.value = None
441
+
442
+ save_result = self._save_workbook()
443
+ if save_result.startswith("Error"):
444
+ return save_result
445
+ return f"Sheet {sheet_name} cleared successfully."
446
+ except Exception as e:
447
+ logger.error(f"Failed to clear sheet: {e}")
448
+ return f"Error: Failed to clear sheet {sheet_name}: {e}"
449
+
450
+ def delete_rows(
451
+ self, sheet_name: str, start_row: int, end_row: Optional[int] = None
452
+ ) -> str:
453
+ r"""Delete rows from a sheet.
454
+
455
+ Use this to remove unwanted rows. You can delete single rows or ranges.
456
+
457
+ Args:
458
+ sheet_name (str): Name of the sheet to modify.
459
+ start_row (int): Starting row number to delete (1-based, where 1
460
+ is first row).
461
+ end_row (Optional[int]): Ending row number to delete (1-based).
462
+ If None, deletes only start_row. (default: :obj:`None`)
463
+
464
+ Returns:
465
+ str: Success confirmation message or error details
466
+ """
467
+ if not self.wb:
468
+ return "Error: Workbook not initialized."
469
+
470
+ if sheet_name not in self.wb.sheetnames:
471
+ return f"Sheet {sheet_name} does not exist."
472
+
473
+ ws = self.wb[sheet_name]
474
+
475
+ if end_row is None:
476
+ end_row = start_row
477
+
478
+ # Delete rows (openpyxl uses 1-based indexing)
479
+ num_rows = end_row - start_row + 1
480
+ ws.delete_rows(start_row, num_rows)
481
+
482
+ self._save_workbook()
483
+ return (
484
+ f"Deleted rows {start_row} to {end_row} from sheet "
485
+ f"{sheet_name} successfully."
486
+ )
487
+
488
+ def delete_columns(
489
+ self, sheet_name: str, start_col: int, end_col: Optional[int] = None
490
+ ) -> str:
491
+ r"""Delete columns from a sheet.
492
+
493
+ Use this to remove unwanted columns. You can delete single columns or
494
+ ranges.
495
+
496
+ Args:
497
+ sheet_name (str): Name of the sheet to modify.
498
+ start_col (int): Starting column number to delete (1-based, where
499
+ 1 is column A).
500
+ end_col (Optional[int]): Ending column number to delete (1-based).
501
+ If None, deletes only start_col. (default: :obj:`None`)
502
+
503
+ Returns:
504
+ str: Success confirmation message or error details
505
+ """
506
+ if not self.wb:
507
+ return "Error: Workbook not initialized."
508
+
509
+ if sheet_name not in self.wb.sheetnames:
510
+ return f"Sheet {sheet_name} does not exist."
511
+
512
+ ws = self.wb[sheet_name]
513
+
514
+ if end_col is None:
515
+ end_col = start_col
516
+
517
+ # Delete columns (openpyxl uses 1-based indexing)
518
+ num_cols = end_col - start_col + 1
519
+ ws.delete_cols(start_col, num_cols)
520
+
521
+ self._save_workbook()
522
+ return (
523
+ f"Deleted columns {start_col} to {end_col} from sheet "
524
+ f"{sheet_name} successfully."
525
+ )
526
+
527
+ def get_cell_value(
528
+ self, sheet_name: str, cell_reference: str
529
+ ) -> Union[str, int, float, None]:
530
+ r"""Get the value from a specific cell.
531
+
532
+ Use this to read a single cell's value. Useful for checking specific
533
+ data points or getting values for calculations.
534
+
535
+ Args:
536
+ sheet_name (str): Name of the sheet containing the cell.
537
+ cell_reference (str): Excel-style cell reference (column letter +
538
+ row number).
539
+
540
+ Returns:
541
+ Union[str, int, float, None]: The cell's value or error message
542
+ Returns None for empty cells.
543
+ """
544
+ if not self.wb:
545
+ return "Error: Workbook not initialized."
546
+
547
+ if sheet_name not in self.wb.sheetnames:
548
+ return f"Error: Sheet {sheet_name} does not exist."
549
+
550
+ ws = self.wb[sheet_name]
551
+ return ws[cell_reference].value
552
+
553
+ def set_cell_value(
554
+ self,
555
+ sheet_name: str,
556
+ cell_reference: str,
557
+ value: Union[str, int, float, None],
558
+ ) -> str:
559
+ r"""Set the value of a specific cell.
560
+
561
+ Use this to update individual cells with new values. Useful for
562
+ corrections, calculations, or updating specific data points.
563
+
564
+ Args:
565
+ sheet_name (str): Name of the sheet containing the cell.
566
+ cell_reference (str): Excel-style cell reference (column letter +
567
+ row number).
568
+ value (Union[str, int, float, None]): New value for the cell.
569
+ (default: :obj:`None`)
570
+
571
+ Returns:
572
+ str: Success confirmation message or error details.
573
+ """
574
+ if not self.wb:
575
+ return "Error: Workbook not initialized."
576
+
577
+ if sheet_name not in self.wb.sheetnames:
578
+ return f"Sheet {sheet_name} does not exist."
579
+
580
+ try:
581
+ ws = self.wb[sheet_name]
582
+ # Handle None values properly - openpyxl doesn't accept None
583
+ # directly
584
+ if value is None:
585
+ ws[cell_reference].value = None
586
+ else:
587
+ ws[cell_reference] = value
588
+ save_result = self._save_workbook()
589
+ if save_result.startswith("Error"):
590
+ return save_result
591
+ return (
592
+ f"Cell {cell_reference} updated successfully in sheet "
593
+ f"{sheet_name}."
594
+ )
595
+ except Exception as e:
596
+ logger.error(f"Failed to set cell value: {e}")
597
+ return f"Error: Failed to set cell value: {e}"
598
+
599
+ def get_column_data(
600
+ self, sheet_name: str, column: Union[int, str]
601
+ ) -> Union[List[Union[str, int, float, None]], str]:
602
+ r"""Get all data from a specific column.
603
+
604
+ Use this to extract all values from a column for analysis or
605
+ processing.
606
+
607
+ Args:
608
+ sheet_name (str): Name of the sheet to read from.
609
+ column (Union[int, str]): Column identifier - either number
610
+ (1-based) or letter.
611
+
612
+ Returns:
613
+ Union[List[Union[str, int, float, None]], str]:
614
+ List of all non-empty values in the column or error message
615
+ """
616
+ if not self.wb:
617
+ return "Error: Workbook not initialized."
618
+
619
+ if sheet_name not in self.wb.sheetnames:
620
+ return f"Error: Sheet {sheet_name} does not exist."
621
+
622
+ ws = self.wb[sheet_name]
623
+
624
+ if isinstance(column, str):
625
+ col_letter = column.upper()
626
+ else:
627
+ from openpyxl.utils import ( # type: ignore[import]
628
+ get_column_letter,
629
+ )
630
+
631
+ col_letter = get_column_letter(column)
632
+
633
+ column_data = []
634
+ for cell in ws[col_letter]:
635
+ if cell.value is not None:
636
+ column_data.append(cell.value)
637
+
638
+ return column_data
639
+
640
+ def find_cells(
641
+ self,
642
+ sheet_name: str,
643
+ search_value: Union[str, int, float],
644
+ search_column: Optional[Union[int, str]] = None,
645
+ ) -> Union[List[str], str]:
646
+ r"""Find cells containing a specific value.
647
+
648
+ Use this to locate where specific data appears in the sheet.
649
+
650
+ Args:
651
+ sheet_name (str): Name of the sheet to search in.
652
+ search_value (Union[str, int, float]): Value to search for.
653
+ search_column (Optional[Union[int, str]]): Limit search to
654
+ specific column. If None, searches entire sheet. (default:
655
+ :obj:`None`)
656
+
657
+ Returns:
658
+ Union[List[str], str]: List of cell references (like "A5", "B12")
659
+ where the value was found, or error message.
660
+ """
661
+ if not self.wb:
662
+ return "Error: Workbook not initialized."
663
+
664
+ if sheet_name not in self.wb.sheetnames:
665
+ return f"Error: Sheet {sheet_name} does not exist."
666
+
667
+ ws = self.wb[sheet_name]
668
+ found_cells = []
669
+
670
+ if search_column:
671
+ # Search in specific column
672
+ if isinstance(search_column, str):
673
+ col_letter = search_column.upper()
674
+ for cell in ws[col_letter]:
675
+ if cell.value == search_value:
676
+ found_cells.append(cell.coordinate)
677
+ else:
678
+ from openpyxl.utils import ( # type: ignore[import]
679
+ get_column_letter,
680
+ )
681
+
682
+ col_letter = get_column_letter(search_column)
683
+ for cell in ws[col_letter]:
684
+ if cell.value == search_value:
685
+ found_cells.append(cell.coordinate)
686
+ else:
687
+ # Search entire sheet
688
+ for row in ws.iter_rows():
689
+ for cell in row:
690
+ if cell.value == search_value:
691
+ found_cells.append(cell.coordinate)
692
+
693
+ return found_cells
694
+
695
+ def get_range_values(
696
+ self, sheet_name: str, cell_range: str
697
+ ) -> Union[List[List[Union[str, int, float, None]]], str]:
698
+ r"""Get values from a specific range of cells.
699
+
700
+ Use this to read a rectangular block of cells at once.
701
+
702
+ Args:
703
+ sheet_name (str): Name of the sheet to read from.
704
+ cell_range (str): Range in Excel format (start:end).
705
+
706
+ Returns:
707
+ Union[List[List[Union[str, int, float, None]]], str]:
708
+ 2D list where each inner list is a row of cell values, or
709
+ error message.
710
+ """
711
+ if not self.wb:
712
+ return "Error: Workbook not initialized."
713
+
714
+ if sheet_name not in self.wb.sheetnames:
715
+ return f"Error: Sheet {sheet_name} does not exist."
716
+
717
+ ws = self.wb[sheet_name]
718
+ range_values = []
719
+
720
+ for row in ws[cell_range]:
721
+ row_values = []
722
+ for cell in row:
723
+ row_values.append(cell.value)
724
+ range_values.append(row_values)
725
+
726
+ return range_values
727
+
728
+ def set_range_values(
729
+ self,
730
+ sheet_name: str,
731
+ cell_range: str,
732
+ values: List[List[Union[str, int, float, None]]],
733
+ ) -> str:
734
+ r"""Set values for a specific range of cells.
735
+
736
+ Use this to update multiple cells at once with a 2D array of data.
737
+
738
+ Args:
739
+ sheet_name (str): Name of the sheet to modify.
740
+ cell_range (str): Range in Excel format to update.
741
+ values (List[List[Union[str, int, float, None]]]): 2D array of
742
+ values. Each inner list represents a row.
743
+
744
+ Returns:
745
+ str: Success confirmation message or error details.
746
+ """
747
+ if not self.wb:
748
+ return "Error: Workbook not initialized."
749
+
750
+ if sheet_name not in self.wb.sheetnames:
751
+ return f"Error: Sheet {sheet_name} does not exist."
752
+
753
+ ws = self.wb[sheet_name]
754
+
755
+ # Get the range
756
+ cell_range_obj = ws[cell_range]
757
+
758
+ # If it's a single row or column, convert to 2D format
759
+ if not isinstance(cell_range_obj[0], tuple):
760
+ cell_range_obj = [cell_range_obj]
761
+
762
+ for row_idx, row in enumerate(cell_range_obj):
763
+ if row_idx < len(values):
764
+ for col_idx, cell in enumerate(row):
765
+ if col_idx < len(values[row_idx]):
766
+ cell.value = values[row_idx][col_idx]
767
+
768
+ self._save_workbook()
769
+ return f"Values set for range {cell_range} in sheet {sheet_name}."
770
+
771
+ def export_sheet_to_csv(self, sheet_name: str, csv_path: str) -> str:
772
+ r"""Export a specific sheet to CSV format.
773
+
774
+ Use this to convert Excel sheets to CSV files for compatibility or
775
+ data exchange.
776
+
777
+ Args:
778
+ sheet_name (str): Name of the sheet to export.
779
+ csv_path (str): File path where CSV will be saved.
780
+
781
+ Returns:
782
+ str: Success confirmation message or error details.
783
+ """
784
+ if not self.wb:
785
+ return "Error: Workbook not initialized."
786
+
787
+ if sheet_name not in self.wb.sheetnames:
788
+ return f"Error: Sheet {sheet_name} does not exist."
789
+
790
+ import pandas as pd
791
+
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)
795
+
796
+ return f"Sheet {sheet_name} exported to CSV: {csv_path}"
797
+
798
+ def get_rows(
799
+ self,
800
+ sheet_name: str,
801
+ start_row: Optional[int] = None,
802
+ end_row: Optional[int] = None,
803
+ ) -> Union[List[List[Union[str, int, float, None]]], str]:
804
+ r"""Retrieve rows of data from a sheet.
805
+
806
+ Use this to read data from a sheet. You can get all rows or specify a
807
+ range. Returns actual data as lists, making it easy to process
808
+ programmatically.
809
+
810
+ Args:
811
+ sheet_name (str): Name of the sheet to read from.
812
+ start_row (Optional[int]): First row to read (1-based). If None,
813
+ starts from row 1. (default: :obj:`None`)
814
+ end_row (Optional[int]): Last row to read (1-based). If None,
815
+ reads to the end. (default: :obj:`None`)
816
+
817
+ Returns:
818
+ Union[List[List[Union[str, int, float, None]]], str]:
819
+ List of rows (each row is a list of cell values) or error
820
+ message.
821
+ """
822
+ if not self.wb:
823
+ return "Error: Workbook not initialized."
824
+
825
+ if sheet_name not in self.wb.sheetnames:
826
+ return f"Error: Sheet {sheet_name} does not exist."
827
+
828
+ ws = self.wb[sheet_name]
829
+ rows = []
830
+
831
+ # Get all rows with data
832
+ for row in ws.iter_rows(
833
+ min_row=start_row, max_row=end_row, values_only=True
834
+ ):
835
+ # Skip completely empty rows
836
+ if any(cell is not None for cell in row):
837
+ rows.append(list(row))
838
+
839
+ return rows
840
+
841
+ def append_row(
842
+ self,
843
+ sheet_name: str,
844
+ row_data: List[Union[str, int, float, None]],
845
+ ) -> str:
846
+ r"""Add a single row to the end of a sheet.
847
+
848
+ Use this to add one row of data to the end of existing content.
849
+ For multiple rows, use multiple calls to this function.
850
+
851
+ Args:
852
+ sheet_name (str): Name of the target sheet.
853
+ row_data (List[Union[str, int, float, None]]): Single row of data
854
+ to add.
855
+
856
+ Returns:
857
+ str: Success confirmation message or error details.
858
+ """
859
+ if not self.wb:
860
+ return "Error: Workbook not initialized."
861
+ if sheet_name not in self.wb.sheetnames:
862
+ return f"Error: Sheet {sheet_name} does not exist."
863
+ ws = self.wb[sheet_name]
864
+ ws.append(row_data)
865
+ self._save_workbook()
866
+ return f"Row appended to sheet {sheet_name} successfully."
867
+
868
+ def update_row(
869
+ self,
870
+ sheet_name: str,
871
+ row_number: int,
872
+ row_data: List[Union[str, int, float, None]],
873
+ ) -> str:
874
+ r"""Update a specific row in the sheet.
875
+
876
+ Use this to replace all data in a specific row with new values.
877
+
878
+ Args:
879
+ sheet_name (str): Name of the sheet to modify.
880
+ row_number (int): The row number to update (1-based, where 1 is
881
+ first row).
882
+ row_data (List[Union[str, int, float, None]]): New data for the
883
+ entire row.
884
+
885
+ Returns:
886
+ str: Success confirmation message or error details.
887
+ """
888
+ if not self.wb:
889
+ return "Error: Workbook not initialized."
890
+
891
+ if sheet_name not in self.wb.sheetnames:
892
+ return f"Sheet {sheet_name} does not exist."
893
+
894
+ ws = self.wb[sheet_name]
895
+
896
+ # Clear the existing row first
897
+ for col_idx in range(1, ws.max_column + 1):
898
+ ws.cell(row=row_number, column=col_idx).value = None
899
+
900
+ # Set new values
901
+ for col_idx, value in enumerate(row_data, 1):
902
+ ws.cell(row=row_number, column=col_idx).value = value
903
+
904
+ self._save_workbook()
905
+ return f"Row {row_number} updated in sheet {sheet_name} successfully."
183
906
 
184
907
  def get_tools(self) -> List[FunctionTool]:
185
908
  r"""Returns a list of FunctionTool objects representing the functions
@@ -190,5 +913,27 @@ class ExcelToolkit(BaseToolkit):
190
913
  the functions in the toolkit.
191
914
  """
192
915
  return [
916
+ # File operations
193
917
  FunctionTool(self.extract_excel_content),
918
+ FunctionTool(self.create_workbook),
919
+ FunctionTool(self.delete_workbook),
920
+ FunctionTool(self.export_sheet_to_csv),
921
+ # Sheet operations
922
+ FunctionTool(self.create_sheet),
923
+ FunctionTool(self.delete_sheet),
924
+ FunctionTool(self.clear_sheet),
925
+ # Data reading
926
+ FunctionTool(self.get_rows),
927
+ FunctionTool(self.get_cell_value),
928
+ FunctionTool(self.get_column_data),
929
+ FunctionTool(self.get_range_values),
930
+ FunctionTool(self.find_cells),
931
+ # Data writing
932
+ FunctionTool(self.append_row),
933
+ FunctionTool(self.update_row),
934
+ FunctionTool(self.set_cell_value),
935
+ FunctionTool(self.set_range_values),
936
+ # Structure modification
937
+ FunctionTool(self.delete_rows),
938
+ FunctionTool(self.delete_columns),
194
939
  ]