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)}")