feat: add rfc_files functions and fix pdf handling for image pdfs
Rafael Uzarowski committed
May 23, 2025 at 18:45 UTC
7d72712fff91b2e00b16b0098fa3afea240c57d4
2 files changed
+713
-25
python/helpers/document_query.py
+91
-25
@@ -32,7 +32,7 @@ from langchain_community.vectorstores.utils import (
32
from langchain_core.embeddings import Embeddings
33
34
from python.helpers.print_style import PrintStyle
35
-from python.helpers import files
35
+from python.helpers import files, rfc_files
36
from agent import Agent
37
import models
38
@@ -641,43 +641,93 @@ class DocumentQueryHelper:
641
def handle_html_document(self, document: str, scheme: str) -> str:
642
if scheme in ["http", "https"]:
643
loader = AsyncHtmlLoader(web_path=document)
644
+ parts: list[Document] = loader.load()
645
elif scheme == "file":
645
- loader = TextLoader(file_path=document)
646
+ # Use RFC file operations instead of TextLoader
647
+ file_content_bytes = rfc_files.read_file_binary(document)
648
+ file_content = file_content_bytes.decode('utf-8')
649
+ # Create Document manually since we're not using TextLoader
650
+ parts = [Document(page_content=file_content, metadata={"source": document})]
651
else:
652
raise ValueError(f"Unsupported scheme: {scheme}")
653
649
- parts: list[Document] = loader.load()
654
return "\n".join([element.page_content for element in MarkdownifyTransformer().transform_documents(parts)])
655
656
def handle_text_document(self, document: str, scheme: str) -> str:
657
if scheme in ["http", "https"]:
658
loader = AsyncHtmlLoader(web_path=document)
659
+ elements: list[Document] = loader.load()
660
elif scheme == "file":
656
- loader = TextLoader(file_path=document)
661
+ # Use RFC file operations instead of TextLoader
662
+ file_content_bytes = rfc_files.read_file_binary(document)
663
+ file_content = file_content_bytes.decode('utf-8')
664
+ # Create Document manually since we're not using TextLoader
665
+ elements = [Document(page_content=file_content, metadata={"source": document})]
666
else:
667
raise ValueError(f"Unsupported scheme: {scheme}")
668
660
- elements: list[Document] = loader.load()
669
return "\n".join([element.page_content for element in elements])
670
671
def handle_pdf_document(self, document: str, scheme: str) -> str:
664
- if scheme not in ["file", "http", "https"]:
672
+ temp_file_path = ""
673
+ if scheme == "file":
674
+ # Use RFC file operations to read the PDF file as binary
675
+ file_content_bytes = rfc_files.read_file_binary(document)
676
+ # Create a temporary file for PyMuPDFLoader since it needs a file path
677
+ import tempfile
678
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file:
679
+ temp_file.write(file_content_bytes)
680
+ temp_file_path = temp_file.name
681
+ elif scheme in ["http", "https"]:
682
+ # download the file from the web url to a temporary file using python libraries for downloading
683
+ import requests
684
+ import tempfile
685
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file:
686
+ response = requests.get(document, timeout=10.0)
687
+ if response.status_code != 200:
688
+ raise ValueError(f"DocumentQueryHelper::handle_pdf_document: Failed to download PDF from {document}: {response.status_code}")
689
+ temp_file.write(response.content)
690
+ temp_file_path = temp_file.name
691
+ else:
692
raise ValueError(f"Unsupported scheme: {scheme}")
693
667
- loader = PyMuPDFLoader(
668
- document,
669
- mode="single",
670
- extract_tables="markdown",
671
- extract_images=True,
672
- images_inner_format="text",
673
- images_parser=TesseractBlobParser(),
674
- pages_delimiter="\n",
675
- )
694
+ if not os.path.exists(temp_file_path):
695
+ raise ValueError(f"DocumentQueryHelper::handle_pdf_document: Temporary file not found: {temp_file_path}")
696
677
- elements: list[Document] = loader.load()
678
- return "\n".join([element.page_content for element in elements])
697
+ try:
698
+ try:
699
+ loader = PyMuPDFLoader(
700
+ temp_file_path,
701
+ mode="single",
702
+ extract_tables="markdown",
703
+ extract_images=True,
704
+ images_inner_format="text",
705
+ images_parser=TesseractBlobParser(),
706
+ pages_delimiter="\n",
707
+ )
708
+ elements: list[Document] = loader.load()
709
+ contents = "\n".join([element.page_content for element in elements])
710
+ except Exception as e:
711
+ PrintStyle.error(f"DocumentQueryHelper::handle_pdf_document: Error loading with PyMuPDF: {e}")
712
+ contents = ""
713
+
714
+ if not contents:
715
+ import pdf2image
716
+ import pytesseract
717
+
718
+ PrintStyle.debug(f"DocumentQueryHelper::handle_pdf_document: FALLBACK Converting PDF to images: {temp_file_path}")
719
+
720
+ # Convert PDF to images
721
+ pages = pdf2image.convert_from_path(temp_file_path)
722
+ for page in pages:
723
+ contents += pytesseract.image_to_string(page) + "\n\n"
724
+
725
+ return contents
726
+ finally:
727
+ os.unlink(temp_file_path)
728
729
def handle_unstructured_document(self, document: str, scheme: str) -> str:
730
+ elements: list[Document] = []
731
if scheme in ["http", "https"]:
732
# loader = UnstructuredURLLoader(urls=[document], mode="single")
733
loader = UnstructuredLoader(
@@ -687,16 +737,32 @@ class DocumentQueryHelper:
737
# chunking_strategy="by_page",
738
strategy="hi_res",
739
)
740
+ elements = loader.load()
741
elif scheme == "file":
691
- loader = UnstructuredLoader(
692
- file_path=document,
693
- mode="single",
694
- partition_via_api=False,
695
- # chunking_strategy="by_page",
696
- strategy="hi_res",
697
- )
742
+ # Use RFC file operations to read the file as binary
743
+ file_content_bytes = rfc_files.read_file_binary(document)
744
+ # Create a temporary file for UnstructuredLoader since it needs a file path
745
+ import tempfile
746
+ import os
747
+ # Get file extension to preserve it for proper processing
748
+ _, ext = os.path.splitext(document)
749
+ with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as temp_file:
750
+ temp_file.write(file_content_bytes)
751
+ temp_file_path = temp_file.name
752
+
753
+ try:
754
+ loader = UnstructuredLoader(
755
+ file_path=temp_file_path,
756
+ mode="single",
757
+ partition_via_api=False,
758
+ # chunking_strategy="by_page",
759
+ strategy="hi_res",
760
+ )
761
+ elements = loader.load()
762
+ finally:
763
+ # Clean up temporary file
764
+ os.unlink(temp_file_path)
765
else:
766
raise ValueError(f"Unsupported scheme: {scheme}")
767
701
- elements: list[Document] = loader.load()
768
return "\n".join([element.page_content for element in elements])
python/helpers/rfc_files.py
new
+622
@@ -0,0 +1,622 @@
1
+import os
2
+import shutil
3
+import fnmatch
4
+import base64
5
+import tempfile
6
+import zipfile
7
+from python.helpers import runtime
8
+
9
+
10
+def get_abs_path(*relative_paths):
11
+ """Convert relative paths to absolute paths based on the base directory."""
12
+ if not relative_paths:
13
+ return os.path.abspath(os.path.dirname(__file__) + "/../..")
14
+
15
+ base_dir = os.path.abspath(os.path.dirname(__file__) + "/../..")
16
+ return os.path.join(base_dir, *relative_paths)
17
+
18
+
19
+# =====================================================
20
+# RFC-ENABLED FILESYSTEM OPERATIONS
21
+# =====================================================
22
+
23
+def read_file_binary(relative_path: str, backup_dirs=None) -> bytes:
24
+ """
25
+ Read binary file content.
26
+
27
+ Args:
28
+ relative_path: Path to the file relative to base directory
29
+ backup_dirs: List of backup directories to search in
30
+
31
+ Returns:
32
+ File content as bytes
33
+ """
34
+ if backup_dirs is None:
35
+ backup_dirs = []
36
+
37
+ # Find the file in directories
38
+ absolute_path = find_file_in_dirs(relative_path, backup_dirs)
39
+
40
+ # Use RFC routing for development mode
41
+ b64_content = runtime.call_development_function_sync(
42
+ _read_file_binary_impl, absolute_path
43
+ )
44
+ return base64.b64decode(b64_content)
45
+
46
+
47
+def read_file_base64(relative_path: str, backup_dirs=None) -> str:
48
+ """
49
+ Read file content and return as base64 string.
50
+
51
+ Args:
52
+ relative_path: Path to the file relative to base directory
53
+ backup_dirs: List of backup directories to search in
54
+
55
+ Returns:
56
+ File content as base64 encoded string
57
+ """
58
+ if backup_dirs is None:
59
+ backup_dirs = []
60
+
61
+ # Find the file in directories
62
+ absolute_path = find_file_in_dirs(relative_path, backup_dirs)
63
+
64
+ # Use RFC routing for development mode
65
+ return runtime.call_development_function_sync(
66
+ _read_file_as_base64_impl, absolute_path
67
+ )
68
+
69
+
70
+def write_file_binary(relative_path: str, content: bytes) -> bool:
71
+ """
72
+ Write binary content to a file.
73
+
74
+ Args:
75
+ relative_path: Path to the file relative to base directory
76
+ content: Binary content to write
77
+
78
+ Returns:
79
+ True if successful
80
+ """
81
+ abs_path = get_abs_path(relative_path)
82
+
83
+ # Use RFC routing for development mode
84
+ b64_content = base64.b64encode(content).decode('utf-8')
85
+ return runtime.call_development_function_sync(
86
+ _write_file_binary_impl, abs_path, b64_content
87
+ )
88
+
89
+
90
+def write_file_base64(relative_path: str, content: str) -> bool:
91
+ """
92
+ Write base64 content to a file.
93
+
94
+ Args:
95
+ relative_path: Path to the file relative to base directory
96
+ content: Base64 encoded content to write
97
+
98
+ Returns:
99
+ True if successful
100
+ """
101
+ abs_path = get_abs_path(relative_path)
102
+
103
+ # Use RFC routing for development mode
104
+ return runtime.call_development_function_sync(
105
+ _write_file_from_base64_impl, abs_path, content
106
+ )
107
+
108
+
109
+def delete_file(relative_path: str) -> bool:
110
+ """
111
+ Delete a file.
112
+
113
+ Args:
114
+ relative_path: Path to the file relative to base directory
115
+
116
+ Returns:
117
+ True if successful
118
+ """
119
+ abs_path = get_abs_path(relative_path)
120
+
121
+ # Use RFC routing for development mode
122
+ return runtime.call_development_function_sync(
123
+ _delete_file_impl, abs_path
124
+ )
125
+
126
+
127
+def delete_directory(relative_path: str) -> bool:
128
+ """
129
+ Delete a directory recursively.
130
+
131
+ Args:
132
+ relative_path: Path to the directory relative to base directory
133
+
134
+ Returns:
135
+ True if successful
136
+ """
137
+ abs_path = get_abs_path(relative_path)
138
+
139
+ # Use RFC routing for development mode
140
+ return runtime.call_development_function_sync(
141
+ _delete_folder_impl, abs_path
142
+ )
143
+
144
+
145
+def list_directory(relative_path: str, include_hidden: bool = False) -> list:
146
+ """
147
+ List directory contents.
148
+
149
+ Args:
150
+ relative_path: Path to the directory relative to base directory
151
+ include_hidden: Whether to include hidden files/folders
152
+
153
+ Returns:
154
+ List of directory items with metadata
155
+ """
156
+ abs_path = get_abs_path(relative_path)
157
+
158
+ # Use RFC routing for development mode
159
+ return runtime.call_development_function_sync(
160
+ _list_folder_impl, abs_path, include_hidden
161
+ )
162
+
163
+
164
+def make_directories(relative_path: str) -> bool:
165
+ """
166
+ Create directories recursively.
167
+
168
+ Args:
169
+ relative_path: Path to create relative to base directory
170
+
171
+ Returns:
172
+ True if successful
173
+ """
174
+ abs_path = get_abs_path(relative_path)
175
+
176
+ # Use RFC routing for development mode
177
+ return runtime.call_development_function_sync(
178
+ _make_dirs_impl, abs_path
179
+ )
180
+
181
+
182
+def path_exists(relative_path: str) -> bool:
183
+ """
184
+ Check if a path exists.
185
+
186
+ Args:
187
+ relative_path: Path to check relative to base directory
188
+
189
+ Returns:
190
+ True if path exists
191
+ """
192
+ abs_path = get_abs_path(relative_path)
193
+
194
+ # Use RFC routing for development mode
195
+ return runtime.call_development_function_sync(
196
+ _path_exists_impl, abs_path
197
+ )
198
+
199
+
200
+def file_exists(relative_path: str) -> bool:
201
+ """
202
+ Check if a file exists.
203
+
204
+ Args:
205
+ relative_path: Path to check relative to base directory
206
+
207
+ Returns:
208
+ True if file exists
209
+ """
210
+ abs_path = get_abs_path(relative_path)
211
+
212
+ # Use RFC routing for development mode
213
+ return runtime.call_development_function_sync(
214
+ _file_exists_impl, abs_path
215
+ )
216
+
217
+
218
+def folder_exists(relative_path: str) -> bool:
219
+ """
220
+ Check if a folder exists.
221
+
222
+ Args:
223
+ relative_path: Path to check relative to base directory
224
+
225
+ Returns:
226
+ True if folder exists
227
+ """
228
+ abs_path = get_abs_path(relative_path)
229
+
230
+ # Use RFC routing for development mode
231
+ return runtime.call_development_function_sync(
232
+ _folder_exists_impl, abs_path
233
+ )
234
+
235
+
236
+def get_subdirectories(relative_path: str, include: str | list[str] = "*", exclude: str | list[str] | None = None) -> list[str]:
237
+ """
238
+ Get subdirectories in a directory.
239
+
240
+ Args:
241
+ relative_path: Path to the directory relative to base directory
242
+ include: Pattern(s) to include
243
+ exclude: Pattern(s) to exclude
244
+
245
+ Returns:
246
+ List of subdirectory names
247
+ """
248
+ abs_path = get_abs_path(relative_path)
249
+
250
+ # Use RFC routing for development mode
251
+ return runtime.call_development_function_sync(
252
+ _get_subdirectories_impl, abs_path, include, exclude
253
+ )
254
+
255
+
256
+def zip_directory(relative_path: str) -> str:
257
+ """
258
+ Create a zip archive of a directory.
259
+
260
+ Args:
261
+ relative_path: Path to the directory relative to base directory
262
+
263
+ Returns:
264
+ Path to the created zip file
265
+ """
266
+ abs_path = get_abs_path(relative_path)
267
+
268
+ # Use RFC routing for development mode
269
+ return runtime.call_development_function_sync(
270
+ _zip_dir_impl, abs_path
271
+ )
272
+
273
+
274
+def move_file(source_path: str, destination_path: str) -> bool:
275
+ """
276
+ Move a file from source to destination.
277
+
278
+ Args:
279
+ source_path: Source path relative to base directory
280
+ destination_path: Destination path relative to base directory
281
+
282
+ Returns:
283
+ True if successful
284
+ """
285
+ source_abs = get_abs_path(source_path)
286
+ dest_abs = get_abs_path(destination_path)
287
+
288
+ # Use RFC routing for development mode
289
+ return runtime.call_development_function_sync(
290
+ _move_file_impl, source_abs, dest_abs
291
+ )
292
+
293
+
294
+def read_directory_as_zip(relative_path: str) -> bytes:
295
+ """
296
+ Read entire directory contents as a zip file.
297
+
298
+ Args:
299
+ relative_path: Path to the directory relative to base directory
300
+
301
+ Returns:
302
+ Zip file content as bytes
303
+ """
304
+ abs_path = get_abs_path(relative_path)
305
+
306
+ # Use RFC routing for development mode
307
+ b64_zip = runtime.call_development_function_sync(
308
+ _read_directory_impl, abs_path
309
+ )
310
+ return base64.b64decode(b64_zip)
311
+
312
+
313
+def find_file_in_dirs(file_path: str, backup_dirs: list[str]) -> str:
314
+ """
315
+ Find a file in the main directory or backup directories.
316
+
317
+ Args:
318
+ file_path: Relative file path to search for
319
+ backup_dirs: List of backup directories to search in
320
+
321
+ Returns:
322
+ Absolute path to the found file
323
+
324
+ Raises:
325
+ FileNotFoundError: If file is not found in any directory
326
+ """
327
+ # Try the main path first
328
+ main_path = get_abs_path(file_path)
329
+ if runtime.call_development_function_sync(_file_exists_impl, main_path):
330
+ return main_path
331
+
332
+ # Try backup directories
333
+ for backup_dir in backup_dirs:
334
+ backup_path = os.path.join(backup_dir, file_path)
335
+ if runtime.call_development_function_sync(_file_exists_impl, backup_path):
336
+ return backup_path
337
+
338
+ # File not found anywhere
339
+ raise FileNotFoundError(f"File not found: {file_path}")
340
+
341
+
342
+# =====================================================
343
+# IMPLEMENTATION FUNCTIONS (Container Operations)
344
+# =====================================================
345
+
346
+def _read_file_binary_impl(file_path: str) -> str:
347
+ """
348
+ Implementation function to read a file in binary mode.
349
+ Returns base64 encoded content for RFC transport.
350
+ """
351
+ if not os.path.exists(file_path):
352
+ raise FileNotFoundError(f"File not found: {file_path}")
353
+
354
+ if not os.path.isfile(file_path):
355
+ raise Exception(f"Path is not a file: {file_path}")
356
+
357
+ try:
358
+ with open(file_path, 'rb') as file:
359
+ content = file.read()
360
+ return base64.b64encode(content).decode('utf-8')
361
+ except Exception as e:
362
+ raise Exception(f"Failed to read file {file_path}: {str(e)}")
363
+
364
+
365
+def _write_file_binary_impl(file_path: str, b64_content: str) -> bool:
366
+ """
367
+ Implementation function to write binary content to a file.
368
+ Expects base64 encoded content from RFC transport.
369
+ """
370
+ try:
371
+ # Ensure b64_content is properly UTF-8 encoded before base64 decoding
372
+ if isinstance(b64_content, str):
373
+ b64_content_bytes = b64_content.encode('utf-8')
374
+ else:
375
+ b64_content_bytes = b64_content
376
+
377
+ # Decode base64 content
378
+ content = base64.b64decode(b64_content_bytes)
379
+
380
+ # Create directory if it doesn't exist
381
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
382
+
383
+ # Write file
384
+ with open(file_path, 'wb') as file:
385
+ file.write(content)
386
+
387
+ return True
388
+ except Exception as e:
389
+ raise Exception(f"Failed to write file {file_path}: {str(e)}")
390
+
391
+
392
+def _delete_file_impl(file_path: str) -> bool:
393
+ """
394
+ Implementation function to delete a file.
395
+ """
396
+ if not os.path.exists(file_path):
397
+ raise FileNotFoundError(f"File not found: {file_path}")
398
+
399
+ if not os.path.isfile(file_path):
400
+ raise Exception(f"Path is not a file: {file_path}")
401
+
402
+ try:
403
+ os.remove(file_path)
404
+ return True
405
+ except Exception as e:
406
+ raise Exception(f"Failed to delete file {file_path}: {str(e)}")
407
+
408
+
409
+def _delete_folder_impl(folder_path: str) -> bool:
410
+ """
411
+ Implementation function to delete a folder recursively.
412
+ """
413
+ if not os.path.exists(folder_path):
414
+ raise FileNotFoundError(f"Folder not found: {folder_path}")
415
+
416
+ if not os.path.isdir(folder_path):
417
+ raise Exception(f"Path is not a directory: {folder_path}")
418
+
419
+ try:
420
+ shutil.rmtree(folder_path)
421
+ return True
422
+ except Exception as e:
423
+ raise Exception(f"Failed to delete folder {folder_path}: {str(e)}")
424
+
425
+
426
+def _list_folder_impl(folder_path: str, include_hidden: bool = False) -> list:
427
+ """
428
+ Implementation function to list folder contents.
429
+ """
430
+ if not os.path.exists(folder_path):
431
+ raise FileNotFoundError(f"Folder not found: {folder_path}")
432
+
433
+ if not os.path.isdir(folder_path):
434
+ raise Exception(f"Path is not a directory: {folder_path}")
435
+
436
+ try:
437
+ items = []
438
+ for item_name in os.listdir(folder_path):
439
+ # Skip hidden files if not requested
440
+ if not include_hidden and item_name.startswith('.'):
441
+ continue
442
+
443
+ item_path = os.path.join(folder_path, item_name)
444
+ stat_info = os.stat(item_path)
445
+
446
+ item_info = {
447
+ "name": item_name,
448
+ "path": item_path,
449
+ "is_file": os.path.isfile(item_path),
450
+ "is_dir": os.path.isdir(item_path),
451
+ "size": stat_info.st_size,
452
+ "modified": stat_info.st_mtime
453
+ }
454
+ items.append(item_info)
455
+
456
+ # Sort by name for consistent output
457
+ items.sort(key=lambda x: str(x["name"]).lower())
458
+ return items
459
+
460
+ except Exception as e:
461
+ raise Exception(f"Failed to list folder {folder_path}: {str(e)}")
462
+
463
+
464
+def _make_dirs_impl(folder_path: str) -> bool:
465
+ """
466
+ Implementation function to create directories.
467
+ """
468
+ try:
469
+ os.makedirs(folder_path, exist_ok=True)
470
+ return True
471
+ except Exception as e:
472
+ raise Exception(f"Failed to create directories {folder_path}: {str(e)}")
473
+
474
+
475
+def _path_exists_impl(file_path: str) -> bool:
476
+ """Implementation function to check if path exists."""
477
+ return os.path.exists(file_path)
478
+
479
+
480
+def _file_exists_impl(file_path: str) -> bool:
481
+ """Implementation function to check if file exists."""
482
+ return os.path.exists(file_path) and os.path.isfile(file_path)
483
+
484
+
485
+def _folder_exists_impl(folder_path: str) -> bool:
486
+ """Implementation function to check if folder exists."""
487
+ return os.path.exists(folder_path) and os.path.isdir(folder_path)
488
+
489
+
490
+def _get_subdirectories_impl(folder_path: str, include: str | list[str], exclude: str | list[str] | None) -> list[str]:
491
+ """
492
+ Implementation function to get subdirectories.
493
+ """
494
+ if not os.path.exists(folder_path):
495
+ return []
496
+
497
+ if isinstance(include, str):
498
+ include = [include]
499
+ if isinstance(exclude, str):
500
+ exclude = [exclude]
501
+
502
+ return [
503
+ subdir
504
+ for subdir in os.listdir(folder_path)
505
+ if os.path.isdir(os.path.join(folder_path, subdir))
506
+ and any(fnmatch.fnmatch(subdir, inc) for inc in include)
507
+ and (exclude is None or not any(fnmatch.fnmatch(subdir, exc) for exc in exclude))
508
+ ]
509
+
510
+
511
+def _zip_dir_impl(folder_path: str) -> str:
512
+ """
513
+ Implementation function to create a zip archive of a directory.
514
+ """
515
+ zip_file_path = tempfile.NamedTemporaryFile(suffix=".zip", delete=False).name
516
+ base_name = os.path.basename(folder_path)
517
+
518
+ with zipfile.ZipFile(zip_file_path, "w", compression=zipfile.ZIP_DEFLATED) as zip_file:
519
+ for root, _, files in os.walk(folder_path):
520
+ for file in files:
521
+ file_path = os.path.join(root, file)
522
+ rel_path = os.path.relpath(file_path, folder_path)
523
+ zip_file.write(file_path, os.path.join(base_name, rel_path))
524
+
525
+ return zip_file_path
526
+
527
+
528
+def _move_file_impl(source_path: str, destination_path: str) -> bool:
529
+ """
530
+ Implementation function to move a file.
531
+ """
532
+ try:
533
+ os.makedirs(os.path.dirname(destination_path), exist_ok=True)
534
+ os.rename(source_path, destination_path)
535
+ return True
536
+ except Exception as e:
537
+ raise Exception(f"Failed to move file {source_path} to {destination_path}: {str(e)}")
538
+
539
+
540
+def _read_directory_impl(dir_path: str) -> str:
541
+ """
542
+ Implementation function to zip a directory and return base64 encoded zip.
543
+ """
544
+ if not os.path.exists(dir_path):
545
+ raise FileNotFoundError(f"Directory not found: {dir_path}")
546
+
547
+ if not os.path.isdir(dir_path):
548
+ raise Exception(f"Path is not a directory: {dir_path}")
549
+
550
+ temp_zip_path = None
551
+ try:
552
+ # Create temporary zip file
553
+ with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as temp_zip:
554
+ temp_zip_path = temp_zip.name
555
+
556
+ # Create zip archive
557
+ with zipfile.ZipFile(temp_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
558
+ for root, dirs, files in os.walk(dir_path):
559
+ for file in files:
560
+ file_path = os.path.join(root, file)
561
+ arcname = os.path.relpath(file_path, dir_path)
562
+ zipf.write(file_path, arcname)
563
+
564
+ # Read zip file and encode as base64
565
+ with open(temp_zip_path, 'rb') as zipf:
566
+ zip_content = zipf.read()
567
+ b64_zip = base64.b64encode(zip_content).decode('utf-8')
568
+
569
+ # Clean up temporary file
570
+ os.unlink(temp_zip_path)
571
+
572
+ return b64_zip
573
+
574
+ except Exception as e:
575
+ # Clean up temporary file if it exists
576
+ if temp_zip_path is not None and os.path.exists(temp_zip_path):
577
+ os.unlink(temp_zip_path)
578
+ raise Exception(f"Failed to zip directory {dir_path}: {str(e)}")
579
+
580
+
581
+def _read_file_as_base64_impl(file_path: str) -> str:
582
+ """
583
+ Implementation function to read a file and return its content as base64.
584
+ """
585
+ if not os.path.exists(file_path):
586
+ raise FileNotFoundError(f"File not found: {file_path}")
587
+
588
+ if not os.path.isfile(file_path):
589
+ raise Exception(f"Path is not a file: {file_path}")
590
+
591
+ try:
592
+ with open(file_path, 'rb') as file:
593
+ content = file.read()
594
+ return base64.b64encode(content).decode('utf-8')
595
+ except Exception as e:
596
+ raise Exception(f"Failed to read file {file_path}: {str(e)}")
597
+
598
+
599
+def _write_file_from_base64_impl(file_path: str, content: str) -> bool:
600
+ """
601
+ Implementation function to write base64 content to a file.
602
+ """
603
+ try:
604
+ # Ensure content is properly UTF-8 encoded before base64 decoding
605
+ if isinstance(content, str):
606
+ content_bytes = content.encode('utf-8')
607
+ else:
608
+ content_bytes = content
609
+
610
+ # Decode base64 content
611
+ decoded_content = base64.b64decode(content_bytes)
612
+
613
+ # Create directory if it doesn't exist
614
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
615
+
616
+ # Write file
617
+ with open(file_path, 'wb') as file:
618
+ file.write(decoded_content)
619
+
620
+ return True
621
+ except Exception as e:
622
+ raise Exception(f"Failed to write file {file_path}: {str(e)}")