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

@@ -14,7 +14,10 @@
14
14
  import re
15
15
  from datetime import datetime
16
16
  from pathlib import Path
17
- from typing import List, Optional, Union
17
+ from typing import TYPE_CHECKING, List, Optional, Tuple, Union
18
+
19
+ if TYPE_CHECKING:
20
+ from reportlab.platypus import Table
18
21
 
19
22
  from camel.logger import get_logger
20
23
  from camel.toolkits.base import BaseToolkit
@@ -33,8 +36,9 @@ class FileWriteToolkit(BaseToolkit):
33
36
 
34
37
  This class provides cross-platform (macOS, Linux, Windows) support for
35
38
  writing to various file formats (Markdown, DOCX, PDF, and plaintext),
36
- replacing text in existing files, automatic backups, custom encoding,
37
- and enhanced formatting options for specialized formats.
39
+ replacing text in existing files, automatic filename uniquification to
40
+ prevent overwrites, custom encoding and enhanced formatting options for
41
+ specialized formats.
38
42
  """
39
43
 
40
44
  def __init__(
@@ -102,21 +106,36 @@ class FileWriteToolkit(BaseToolkit):
102
106
  f.write(content)
103
107
  logger.debug(f"Wrote text to {file_path} with {encoding} encoding")
104
108
 
105
- def _create_backup(self, file_path: Path) -> None:
106
- r"""Create a backup of the file if it exists and backup is enabled.
109
+ def _generate_unique_filename(self, file_path: Path) -> Path:
110
+ r"""Generate a unique filename if the target file already exists.
107
111
 
108
112
  Args:
109
- file_path (Path): Path to the file to backup.
110
- """
111
- import shutil
113
+ file_path (Path): The original file path.
112
114
 
113
- if not self.backup_enabled or not file_path.exists():
114
- return
115
+ Returns:
116
+ Path: A unique file path that doesn't exist yet.
117
+ """
118
+ if not file_path.exists():
119
+ return file_path
115
120
 
121
+ # Generate unique filename with timestamp and counter
116
122
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
117
- backup_path = file_path.parent / f"{file_path.name}.{timestamp}.bak"
118
- shutil.copy2(file_path, backup_path)
119
- logger.info(f"Created backup at {backup_path}")
123
+ stem = file_path.stem
124
+ suffix = file_path.suffix
125
+ parent = file_path.parent
126
+
127
+ # First try with timestamp
128
+ new_path = parent / f"{stem}_{timestamp}{suffix}"
129
+ if not new_path.exists():
130
+ return new_path
131
+
132
+ # If timestamp version exists, add counter
133
+ counter = 1
134
+ while True:
135
+ new_path = parent / f"{stem}_{timestamp}_{counter}{suffix}"
136
+ if not new_path.exists():
137
+ return new_path
138
+ counter += 1
120
139
 
121
140
  def _write_docx_file(self, file_path: Path, content: str) -> None:
122
141
  r"""Write text content to a DOCX file with default formatting.
@@ -146,25 +165,28 @@ class FileWriteToolkit(BaseToolkit):
146
165
  document.save(str(file_path))
147
166
  logger.debug(f"Wrote DOCX to {file_path} with default formatting")
148
167
 
149
- @dependencies_required('pylatex', 'pymupdf')
168
+ @dependencies_required('reportlab')
150
169
  def _write_pdf_file(
151
170
  self,
152
171
  file_path: Path,
153
172
  title: str,
154
- content: str,
173
+ content: Union[str, List[List[str]]],
155
174
  use_latex: bool = False,
156
175
  ) -> None:
157
- r"""Write text content to a PDF file with default formatting.
176
+ r"""Write text content to a PDF file with LaTeX and table support.
158
177
 
159
178
  Args:
160
179
  file_path (Path): The target file path.
161
- title (str): The title of the document.
162
- content (str): The text content to write.
163
- use_latex (bool): Whether to use LaTeX for rendering. Only
164
- Recommended for documents with mathematical formulas or
165
- complex typesetting needs. (default: :obj:`False`)
180
+ title (str): The document title.
181
+ content (Union[str, List[List[str]]]): The content to write. Can
182
+ be:
183
+ - String: Supports Markdown-style tables and LaTeX math
184
+ expressions
185
+ - List[List[str]]: Table data as list of rows for direct table
186
+ rendering
187
+ use_latex (bool): Whether to use LaTeX for math rendering.
188
+ (default: :obj:`False`)
166
189
  """
167
- # TODO: table generation need to be improved
168
190
  if use_latex:
169
191
  from pylatex import (
170
192
  Command,
@@ -179,7 +201,28 @@ class FileWriteToolkit(BaseToolkit):
179
201
  doc = Document(documentclass="article")
180
202
  doc.packages.append(Command('usepackage', 'amsmath'))
181
203
  with doc.create(Section('Generated Content')):
182
- for line in content.split('\n'):
204
+ # Handle different content types
205
+ if isinstance(content, str):
206
+ content_lines = content.split('\n')
207
+ else:
208
+ # Convert table data to LaTeX table format
209
+ content_lines = []
210
+ if content:
211
+ # Add table header
212
+ table_header = (
213
+ r'\begin{tabular}{' + 'l' * len(content[0]) + '}'
214
+ )
215
+ content_lines.append(table_header)
216
+ content_lines.append(r'\hline')
217
+ for row in content:
218
+ row_content = (
219
+ ' & '.join(str(cell) for cell in row) + r' \\'
220
+ )
221
+ content_lines.append(row_content)
222
+ content_lines.append(r'\hline')
223
+ content_lines.append(r'\end{tabular}')
224
+
225
+ for line in content_lines:
183
226
  stripped_line = line.strip()
184
227
 
185
228
  # Skip empty lines
@@ -214,106 +257,471 @@ class FileWriteToolkit(BaseToolkit):
214
257
  doc.generate_pdf(str(file_path), clean_tex=True)
215
258
 
216
259
  logger.info(f"Wrote PDF (with LaTeX) to {file_path}")
260
+
217
261
  else:
218
- import pymupdf
219
-
220
- # Create a new PDF document
221
- doc = pymupdf.open()
222
-
223
- # Add a page
224
- page = doc.new_page()
225
-
226
- # Process the content
227
- lines = content.strip().split('\n')
228
- document_title = title
229
-
230
- # Create a TextWriter for writing text to the page
231
- text_writer = pymupdf.TextWriter(page.rect)
232
-
233
- # Define fonts
234
- normal_font = pymupdf.Font(
235
- "helv"
236
- ) # Standard font with multilingual support
237
- bold_font = pymupdf.Font("helv")
238
-
239
- # Start position for text
240
- y_pos = 50
241
- x_pos = 50
242
-
243
- # Add title
244
- text_writer.fill_textbox(
245
- pymupdf.Rect(
246
- x_pos, y_pos, page.rect.width - x_pos, y_pos + 30
247
- ),
248
- document_title,
249
- fontsize=16,
250
- )
251
- y_pos += 40
262
+ try:
263
+ from reportlab.lib import colors
264
+ from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY
265
+ from reportlab.lib.pagesizes import A4
266
+ from reportlab.lib.styles import (
267
+ ParagraphStyle,
268
+ getSampleStyleSheet,
269
+ )
270
+ from reportlab.platypus import (
271
+ Paragraph,
272
+ SimpleDocTemplate,
273
+ Spacer,
274
+ )
275
+
276
+ # Register Chinese fonts
277
+ chinese_font = self._register_chinese_font()
278
+
279
+ # Create PDF document
280
+ doc = SimpleDocTemplate(
281
+ str(file_path),
282
+ pagesize=A4,
283
+ rightMargin=72,
284
+ leftMargin=72,
285
+ topMargin=72,
286
+ bottomMargin=18,
287
+ )
252
288
 
253
- # Process content
254
- for line in lines:
255
- stripped_line = line.strip()
289
+ # Get styles with Chinese font support
290
+ styles = getSampleStyleSheet()
291
+ title_style = ParagraphStyle(
292
+ 'CustomTitle',
293
+ parent=styles['Heading1'],
294
+ fontSize=18,
295
+ spaceAfter=30,
296
+ alignment=TA_CENTER,
297
+ textColor=colors.black,
298
+ fontName=chinese_font,
299
+ )
256
300
 
257
- # Skip empty lines but add some space
258
- if not stripped_line:
259
- y_pos += 10
260
- continue
301
+ heading_style = ParagraphStyle(
302
+ 'CustomHeading',
303
+ parent=styles['Heading2'],
304
+ fontSize=14,
305
+ spaceAfter=12,
306
+ spaceBefore=20,
307
+ textColor=colors.black,
308
+ fontName=chinese_font,
309
+ )
261
310
 
262
- # Handle headers
263
- if stripped_line.startswith('## '):
264
- text_writer.fill_textbox(
265
- pymupdf.Rect(
266
- x_pos, y_pos, page.rect.width - x_pos, y_pos + 20
267
- ),
268
- stripped_line[3:].strip(),
269
- font=bold_font,
270
- fontsize=14,
271
- )
272
- y_pos += 25
273
- elif stripped_line.startswith('# '):
274
- text_writer.fill_textbox(
275
- pymupdf.Rect(
276
- x_pos, y_pos, page.rect.width - x_pos, y_pos + 25
277
- ),
278
- stripped_line[2:].strip(),
279
- font=bold_font,
280
- fontsize=16,
281
- )
282
- y_pos += 30
283
- # Handle horizontal rule
284
- elif stripped_line == '---':
285
- page.draw_line(
286
- pymupdf.Point(x_pos, y_pos + 5),
287
- pymupdf.Point(page.rect.width - x_pos, y_pos + 5),
311
+ body_style = ParagraphStyle(
312
+ 'CustomBody',
313
+ parent=styles['Normal'],
314
+ fontSize=11,
315
+ spaceAfter=12,
316
+ alignment=TA_JUSTIFY,
317
+ textColor=colors.black,
318
+ fontName=chinese_font,
319
+ )
320
+
321
+ # Build story (content elements)
322
+ story = []
323
+
324
+ # Add title
325
+ if title:
326
+ story.append(Paragraph(title, title_style))
327
+ story.append(Spacer(1, 12))
328
+
329
+ # Handle different content types
330
+ if isinstance(content, list) and all(
331
+ isinstance(row, list) for row in content
332
+ ):
333
+ # Content is a table (List[List[str]])
334
+ logger.debug(
335
+ f"Processing content as table with {len(content)} rows"
288
336
  )
289
- y_pos += 15
290
- # Regular text
337
+ if content:
338
+ table = self._create_pdf_table(content)
339
+ story.append(table)
291
340
  else:
292
- # Check if we need a new page
293
- if y_pos > page.rect.height - 50:
294
- text_writer.write_text(page)
295
- page = doc.new_page()
296
- text_writer = pymupdf.TextWriter(page.rect)
297
- y_pos = 50
298
-
299
- # Add text to the current page
300
- text_writer.fill_textbox(
301
- pymupdf.Rect(
302
- x_pos, y_pos, page.rect.width - x_pos, y_pos + 15
303
- ),
304
- stripped_line,
305
- font=normal_font,
341
+ # Content is a string, process normally
342
+ content_str = str(content)
343
+ self._process_text_content(
344
+ story, content_str, heading_style, body_style
306
345
  )
307
- y_pos += 15
308
346
 
309
- # Write the accumulated text to the last page
310
- text_writer.write_text(page)
347
+ # Build PDF
348
+ doc.build(story)
349
+ except Exception as e:
350
+ logger.error(f"Error creating PDF: {e}")
351
+
352
+ def _process_text_content(
353
+ self, story, content: str, heading_style, body_style
354
+ ):
355
+ r"""Process text content and add to story.
356
+
357
+ Args:
358
+ story: The reportlab story list to append to
359
+ content (str): The text content to process
360
+ heading_style: Style for headings
361
+ body_style: Style for body text
362
+ """
363
+ from reportlab.platypus import Paragraph, Spacer
364
+
365
+ # Process content
366
+ lines = content.split('\n')
367
+ logger.debug(f"Processing {len(lines)} lines of content")
368
+
369
+ # Parse all tables from the content first
370
+ tables = self._parse_markdown_table(lines)
371
+ table_line_ranges = []
372
+
373
+ # Find line ranges that contain tables
374
+ if tables:
375
+ table_line_ranges = self._find_table_line_ranges(lines)
376
+
377
+ # Process lines, skipping table lines and adding tables at
378
+ # appropriate positions
379
+ i = 0
380
+ current_table_idx = 0
381
+
382
+ while i < len(lines):
383
+ line = lines[i].strip()
384
+
385
+ # Check if this line is part of a table
386
+ is_table_line = any(
387
+ start <= i <= end for start, end in table_line_ranges
388
+ )
389
+
390
+ if is_table_line:
391
+ # Skip all lines in this table and add the table to story
392
+ table_start, table_end = next(
393
+ (start, end)
394
+ for start, end in table_line_ranges
395
+ if start <= i <= end
396
+ )
397
+
398
+ if current_table_idx < len(tables):
399
+ table_row_count = len(tables[current_table_idx])
400
+ logger.debug(f"Adding table with {table_row_count} rows")
401
+ try:
402
+ table = self._create_pdf_table(
403
+ tables[current_table_idx]
404
+ )
405
+ story.append(table)
406
+ story.append(Spacer(1, 12))
407
+ except Exception as e:
408
+ logger.error(f"Failed to create table: {e}")
409
+ # Fallback: render as text
410
+ table_error_msg = (
411
+ f"Table data (error): "
412
+ f"{tables[current_table_idx]}"
413
+ )
414
+ story.append(
415
+ Paragraph(
416
+ table_error_msg,
417
+ body_style,
418
+ )
419
+ )
420
+ current_table_idx += 1
421
+
422
+ # Skip to end of table
423
+ i = table_end + 1
424
+ continue
425
+
426
+ # Skip empty lines
427
+ if not line:
428
+ story.append(Spacer(1, 6))
429
+ i += 1
430
+ continue
431
+
432
+ # Handle headings
433
+ if line.startswith('# '):
434
+ story.append(Paragraph(line[2:], heading_style))
435
+ elif line.startswith('## '):
436
+ story.append(Paragraph(line[3:], heading_style))
437
+ elif line.startswith('### '):
438
+ story.append(Paragraph(line[4:], heading_style))
439
+ else:
440
+ # Regular paragraph
441
+ # Convert basic markdown formatting
442
+ line = self._convert_markdown_to_html(line)
443
+ story.append(Paragraph(line, body_style))
444
+
445
+ i += 1
446
+
447
+ def _find_table_line_ranges(
448
+ self, lines: List[str]
449
+ ) -> List[Tuple[int, int]]:
450
+ r"""Find line ranges that contain markdown tables.
451
+
452
+ Args:
453
+ lines (List[str]): List of lines to analyze.
454
+
455
+ Returns:
456
+ List[Tuple[int, int]]: List of (start_line, end_line) tuples
457
+ for table ranges.
458
+ """
459
+ ranges = []
460
+ in_table = False
461
+ table_start = 0
462
+
463
+ for i, line in enumerate(lines):
464
+ line = line.strip()
465
+
466
+ if self._is_table_row(line):
467
+ if not in_table:
468
+ in_table = True
469
+ table_start = i
470
+ else:
471
+ if in_table:
472
+ # End of table
473
+ ranges.append((table_start, i - 1))
474
+ in_table = False
475
+
476
+ # Handle table at end of content
477
+ if in_table:
478
+ ranges.append((table_start, len(lines) - 1))
479
+
480
+ return ranges
481
+
482
+ def _register_chinese_font(self) -> str:
483
+ r"""Register Chinese font for PDF generation.
484
+
485
+ Returns:
486
+ str: The font name to use for Chinese text.
487
+ """
488
+ import os
489
+ import platform
490
+
491
+ from reportlab.lib.fonts import addMapping
492
+ from reportlab.pdfbase import pdfmetrics
493
+ from reportlab.pdfbase.ttfonts import TTFont
494
+
495
+ # Try to find and register Chinese fonts on the system
496
+ font_paths = []
497
+ system = platform.system()
498
+
499
+ if system == "Darwin": # macOS
500
+ font_paths = [
501
+ "/System/Library/Fonts/PingFang.ttc",
502
+ "/System/Library/Fonts/Hiragino Sans GB.ttc",
503
+ "/System/Library/Fonts/STHeiti Light.ttc",
504
+ "/System/Library/Fonts/STHeiti Medium.ttc",
505
+ "/Library/Fonts/Arial Unicode MS.ttf",
506
+ ]
507
+ elif system == "Windows":
508
+ font_paths = [
509
+ r"C:\Windows\Fonts\msyh.ttc", # Microsoft YaHei
510
+ r"C:\Windows\Fonts\simsun.ttc", # SimSun
511
+ r"C:\Windows\Fonts\arial.ttf", # Arial (fallback)
512
+ ]
513
+ elif system == "Linux":
514
+ font_paths = [
515
+ "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
516
+ "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
517
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
518
+ ]
519
+
520
+ # Try to register the first available font
521
+ for font_path in font_paths:
522
+ if os.path.exists(font_path):
523
+ try:
524
+ font_name = "ChineseFont"
525
+ # Only register if not already registered
526
+ if font_name not in pdfmetrics.getRegisteredFontNames():
527
+ pdfmetrics.registerFont(TTFont(font_name, font_path))
528
+ # Add font mapping for bold/italic variants
529
+ addMapping(font_name, 0, 0, font_name) # normal
530
+ addMapping(font_name, 0, 1, font_name) # italic
531
+ addMapping(font_name, 1, 0, font_name) # bold
532
+ addMapping(font_name, 1, 1, font_name) # bold italic
533
+ logger.debug(f"Registered Chinese font: {font_path}")
534
+ return font_name
535
+ except Exception as e:
536
+ logger.debug(f"Failed to register font {font_path}: {e}")
537
+ continue
538
+
539
+ # Fallback to Helvetica if no Chinese font found
540
+ logger.warning("No Chinese font found, falling back to Helvetica")
541
+ return "Helvetica"
311
542
 
312
- # Save the PDF
313
- doc.save(str(file_path))
314
- doc.close()
543
+ def _parse_markdown_table(self, lines: List[str]) -> List[List[List[str]]]:
544
+ r"""Parse markdown-style tables from a list of lines.
315
545
 
316
- logger.debug(f"Wrote PDF to {file_path} with PyMuPDF formatting")
546
+ Args:
547
+ lines (List[str]): List of text lines that may contain tables.
548
+
549
+ Returns:
550
+ List[List[List[str]]]: List of tables, where each table is a list
551
+ of rows, and each row is a list of cells.
552
+ """
553
+ tables = []
554
+ current_table_data: List[List[str]] = []
555
+ in_table = False
556
+
557
+ for line in lines:
558
+ line = line.strip()
559
+
560
+ # Check for table (Markdown-style)
561
+ if self._is_table_row(line):
562
+ logger.debug(f"Found table line: {line}")
563
+
564
+ if not in_table:
565
+ in_table = True
566
+ current_table_data = []
567
+ logger.debug("Starting new table")
568
+
569
+ # Skip separator lines (e.g., |---|---|)
570
+ if self._is_table_separator(line):
571
+ logger.debug("Skipping separator line")
572
+ continue
573
+
574
+ # Parse table row
575
+ cells = self._parse_table_row(line)
576
+ if cells:
577
+ current_table_data.append(cells)
578
+ logger.debug(f"Added table row: {cells}")
579
+ continue
580
+
581
+ # If we were in a table and now we're not, finalize the table
582
+ if in_table:
583
+ if current_table_data:
584
+ row_count = len(current_table_data)
585
+ logger.debug(f"Finalizing table with {row_count} rows")
586
+ tables.append(current_table_data)
587
+ current_table_data = []
588
+ in_table = False
589
+
590
+ # Add any remaining table
591
+ if in_table and current_table_data:
592
+ row_count = len(current_table_data)
593
+ logger.debug(f"Adding final table with {row_count} rows")
594
+ tables.append(current_table_data)
595
+
596
+ return tables
597
+
598
+ def _is_table_row(self, line: str) -> bool:
599
+ r"""Check if a line appears to be a table row.
600
+
601
+ Args:
602
+ line (str): The line to check.
603
+
604
+ Returns:
605
+ bool: True if the line looks like a table row.
606
+ """
607
+ return '|' in line and line.count('|') >= 2
608
+
609
+ def _is_table_separator(self, line: str) -> bool:
610
+ r"""Check if a line is a table separator (e.g., |---|---|).
611
+
612
+ Args:
613
+ line (str): The line to check.
614
+
615
+ Returns:
616
+ bool: True if the line is a table separator.
617
+ """
618
+ import re
619
+
620
+ # More precise check for separator lines
621
+ # Must contain only spaces, pipes, dashes, and colons
622
+ # and have at least one dash to be a separator
623
+ if not re.match(r'^[\s\|\-\:]+$', line):
624
+ return False
625
+
626
+ # Must contain at least one dash to be a valid separator
627
+ return '-' in line
628
+
629
+ def _parse_table_row(self, line: str) -> List[str]:
630
+ r"""Parse a single table row into cells.
631
+
632
+ Args:
633
+ line (str): The table row line.
634
+
635
+ Returns:
636
+ List[str]: List of cell contents.
637
+ """
638
+ # Parse table row
639
+ cells = [cell.strip() for cell in line.split('|')]
640
+
641
+ # Remove empty cells at start/end (common in markdown tables)
642
+ if cells and not cells[0]:
643
+ cells = cells[1:]
644
+ if cells and not cells[-1]:
645
+ cells = cells[:-1]
646
+
647
+ return cells
648
+
649
+ def _create_pdf_table(self, table_data: List[List[str]]) -> "Table":
650
+ r"""Create a formatted table for PDF.
651
+
652
+ Args:
653
+ table_data (List[List[str]]): Table data as list of rows.
654
+
655
+ Returns:
656
+ Table: A formatted reportlab Table object.
657
+ """
658
+ from reportlab.lib import colors
659
+ from reportlab.platypus import Table, TableStyle
660
+
661
+ try:
662
+ # Get Chinese font for table
663
+ chinese_font = self._register_chinese_font()
664
+
665
+ # Debug: Log table data
666
+ logger.debug(f"Creating table with {len(table_data)} rows")
667
+ for i, row in enumerate(table_data):
668
+ logger.debug(f"Row {i}: {row}")
669
+
670
+ # Create table
671
+ table = Table(table_data)
672
+
673
+ # Style the table with Chinese font support
674
+ table.setStyle(
675
+ TableStyle(
676
+ [
677
+ ('BACKGROUND', (0, 0), (-1, 0), colors.grey),
678
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
679
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
680
+ ('FONTNAME', (0, 0), (-1, 0), chinese_font),
681
+ ('FONTSIZE', (0, 0), (-1, 0), 10),
682
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
683
+ ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
684
+ ('FONTNAME', (0, 1), (-1, -1), chinese_font),
685
+ ('FONTSIZE', (0, 1), (-1, -1), 9),
686
+ ('GRID', (0, 0), (-1, -1), 1, colors.black),
687
+ ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
688
+ ]
689
+ )
690
+ )
691
+
692
+ logger.debug("Table created successfully")
693
+ return table
694
+
695
+ except Exception as e:
696
+ logger.error(f"Error creating table: {e}")
697
+ # Return simple unstyled table as fallback
698
+ from reportlab.platypus import Table
699
+
700
+ return Table(table_data)
701
+
702
+ def _convert_markdown_to_html(self, text: str) -> str:
703
+ r"""Convert basic markdown formatting to HTML for PDF rendering.
704
+
705
+ Args:
706
+ text (str): Text with markdown formatting.
707
+
708
+ Returns:
709
+ str: Text with HTML formatting.
710
+ """
711
+ # Bold text (check double markers first)
712
+ text = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', text)
713
+ text = re.sub(r'__(.*?)__', r'<b>\1</b>', text)
714
+
715
+ # Italic text (single markers after double markers)
716
+ text = re.sub(
717
+ r'(?<!\*)\*(?!\*)(.*?)(?<!\*)\*(?!\*)', r'<i>\1</i>', text
718
+ )
719
+ text = re.sub(r'(?<!_)_(?!_)(.*?)(?<!_)_(?!_)', r'<i>\1</i>', text)
720
+
721
+ # Code (inline)
722
+ text = re.sub(r'`(.*?)`', r'<font name="Courier">\1</font>', text)
723
+
724
+ return text
317
725
 
318
726
  def _write_csv_file(
319
727
  self,
@@ -439,9 +847,8 @@ class FileWriteToolkit(BaseToolkit):
439
847
  supplied, it is resolved to self.output_dir.
440
848
  encoding (Optional[str]): The character encoding to use. (default:
441
849
  :obj: `None`)
442
- use_latex (bool): For PDF files, whether to use LaTeX rendering.
443
- Only recommended for documents with mathematical formulas or
444
- complex typesetting needs. (default: :obj:`False`)
850
+ use_latex (bool): Whether to use LaTeX for math rendering.
851
+ (default: :obj:`False`)
445
852
 
446
853
  Returns:
447
854
  str: A message indicating success or error details.
@@ -449,8 +856,8 @@ class FileWriteToolkit(BaseToolkit):
449
856
  file_path = self._resolve_filepath(filename)
450
857
  file_path.parent.mkdir(parents=True, exist_ok=True)
451
858
 
452
- # Create backup if file exists
453
- self._create_backup(file_path)
859
+ # Generate unique filename if file exists
860
+ file_path = self._generate_unique_filename(file_path)
454
861
 
455
862
  extension = file_path.suffix.lower()
456
863
 
@@ -466,9 +873,7 @@ class FileWriteToolkit(BaseToolkit):
466
873
  if extension in [".doc", ".docx"]:
467
874
  self._write_docx_file(file_path, str(content))
468
875
  elif extension == ".pdf":
469
- self._write_pdf_file(
470
- file_path, title, str(content), use_latex=use_latex
471
- )
876
+ self._write_pdf_file(file_path, title, content, use_latex)
472
877
  elif extension == ".csv":
473
878
  self._write_csv_file(
474
879
  file_path, content, encoding=file_encoding