camel-ai 0.2.71a3__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.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +1482 -134
- camel/agents/repo_agent.py +2 -1
- camel/benchmarks/browsecomp.py +6 -6
- camel/interpreters/docker_interpreter.py +3 -2
- camel/loaders/base_loader.py +85 -0
- camel/logger.py +1 -1
- camel/messages/base.py +12 -1
- camel/models/azure_openai_model.py +96 -7
- camel/models/base_model.py +68 -10
- camel/models/deepseek_model.py +5 -0
- camel/models/gemini_model.py +5 -0
- camel/models/litellm_model.py +48 -16
- camel/models/model_manager.py +24 -6
- camel/models/openai_compatible_model.py +109 -5
- camel/models/openai_model.py +117 -8
- camel/societies/workforce/prompts.py +68 -5
- camel/societies/workforce/role_playing_worker.py +1 -0
- camel/societies/workforce/single_agent_worker.py +1 -0
- camel/societies/workforce/utils.py +67 -2
- camel/societies/workforce/workforce.py +412 -67
- camel/societies/workforce/workforce_logger.py +0 -8
- camel/tasks/task.py +2 -0
- camel/toolkits/__init__.py +7 -2
- camel/toolkits/craw4ai_toolkit.py +2 -2
- camel/toolkits/file_write_toolkit.py +526 -121
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +9 -3
- camel/toolkits/hybrid_browser_toolkit/unified_analyzer.js +31 -8
- camel/toolkits/message_agent_toolkit.py +608 -0
- camel/toolkits/note_taking_toolkit.py +90 -0
- camel/toolkits/openai_image_toolkit.py +292 -0
- camel/toolkits/slack_toolkit.py +4 -4
- camel/toolkits/terminal_toolkit.py +223 -73
- camel/utils/mcp_client.py +37 -1
- {camel_ai-0.2.71a3.dist-info → camel_ai-0.2.71a5.dist-info}/METADATA +48 -7
- {camel_ai-0.2.71a3.dist-info → camel_ai-0.2.71a5.dist-info}/RECORD +38 -35
- camel/toolkits/dalle_toolkit.py +0 -175
- {camel_ai-0.2.71a3.dist-info → camel_ai-0.2.71a5.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.71a3.dist-info → camel_ai-0.2.71a5.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
37
|
-
and enhanced formatting options for
|
|
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
|
|
106
|
-
r"""
|
|
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):
|
|
110
|
-
"""
|
|
111
|
-
import shutil
|
|
113
|
+
file_path (Path): The original file path.
|
|
112
114
|
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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('
|
|
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
|
|
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
|
|
162
|
-
content (str): The
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
290
|
-
|
|
337
|
+
if content:
|
|
338
|
+
table = self._create_pdf_table(content)
|
|
339
|
+
story.append(table)
|
|
291
340
|
else:
|
|
292
|
-
#
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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):
|
|
443
|
-
(
|
|
444
|
-
`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
|
-
#
|
|
453
|
-
self.
|
|
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
|