# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import shlex import tempfile from pathlib import Path from camel.toolkits import FileToolkit from mirage.agents.camel._async import AsyncRunner from mirage.workspace.workspace import Workspace class MirageFileToolkit(FileToolkit): def __init__( self, workspace: Workspace, working_directory: str = "/", timeout: float | None = None, default_encoding: str = "utf-8", backup_enabled: bool = False, ) -> None: self._ws = workspace self._runner = AsyncRunner() self._mirage_root = working_directory self._tmpdir = tempfile.TemporaryDirectory(prefix="mirage-camel-") super().__init__( working_directory=self._tmpdir.name, timeout=timeout, default_encoding=default_encoding, backup_enabled=backup_enabled, ) def close(self) -> None: self._runner.close() self._tmpdir.cleanup() def _to_mirage_path(self, file_path: str) -> str: path_str = file_path if not path_str.startswith("/"): base = self._mirage_root.rstrip("/") or "" path_str = f"{base}/{path_str}" return path_str def _read_mirage_bytes(self, mirage_path: str) -> bytes: quoted = shlex.quote(mirage_path) io = self._runner.run(self._ws.execute(f"cat {quoted}")) if io.exit_code != 0: stderr = io.stderr if isinstance(io.stderr, bytes) else b"" raise FileNotFoundError(stderr.decode("utf-8", errors="replace")) return io.stdout if isinstance(io.stdout, bytes) else b"" def _write_mirage_bytes(self, mirage_path: str, data: bytes) -> None: parent = str(Path(mirage_path).parent) if parent and parent != "/": mkdir_io = self._runner.run( self._ws.execute(f"mkdir -p {shlex.quote(parent)}")) if mkdir_io.exit_code != 0: stderr = mkdir_io.stderr if isinstance(mkdir_io.stderr, bytes) else b"" raise OSError(stderr.decode("utf-8", errors="replace")) quoted = shlex.quote(mirage_path) io = self._runner.run(self._ws.execute(f"cat > {quoted}", stdin=data)) if io.exit_code != 0: stderr = io.stderr if isinstance(io.stderr, bytes) else b"" raise OSError(stderr.decode("utf-8", errors="replace")) def write_to_file( self, title: str, content: str | list[list[str]], filename: str, encoding: str | None = None, use_latex: bool = False, ) -> str: """Write content to filename over Mirage Workspace. Args: title (str): Document title (used by some format writers). content: Content payload. filename (str): Logical Mirage path. encoding (str | None): Override default encoding. use_latex (bool): Forwarded to PDF writer. Returns: str: Success or error message. """ mirage_path = self._to_mirage_path(filename) local_name = Path(filename).name or "out" local_path = Path(self._tmpdir.name) / local_name super_msg = super().write_to_file( title=title, content=content, filename=str(local_path), encoding=encoding, use_latex=use_latex, ) if super_msg.startswith("Error"): return super_msg produced = self._find_produced_file(local_path) if produced is None: return f"Error: format writer produced no file for {local_path}" target = self._adjust_extension(mirage_path, produced) try: self._write_mirage_bytes(target, produced.read_bytes()) except OSError as exc: return f"Error writing {target}: {exc}" return f"Content successfully written to file: {target}" def read_file(self, file_paths: str | list[str]) -> str | dict[str, str]: """Read files from Mirage and return Markdown-rendered text. Args: file_paths (str | list[str]): Single path or list of paths. Returns: str | dict[str, str]: Same shape as camel read_file. """ if isinstance(file_paths, str): return self._read_one(file_paths) out: dict[str, str] = {} for fp in file_paths: out[fp] = self._read_one(fp) return out def _read_one(self, file_path: str) -> str: mirage_path = self._to_mirage_path(file_path) try: data = self._read_mirage_bytes(mirage_path) except FileNotFoundError as exc: return f"Failed to read file: {mirage_path} ({exc})" suffix = Path(mirage_path).suffix or ".txt" local = Path( self._tmpdir.name) / f"read_{abs(hash(mirage_path))}{suffix}" local.write_bytes(data) return super().read_file(file_paths=str(local)) def _find_produced_file(self, expected: Path) -> Path | None: if expected.exists(): return expected for ext in (".md", ".pdf", ".docx", ".csv", ".json", ".html", ".txt"): candidate = expected.with_suffix(ext) if candidate.exists(): return candidate parent = expected.parent if parent.exists(): for child in parent.iterdir(): if child.stem == expected.stem and child.is_file(): return child return None def _adjust_extension(self, mirage_path: str, produced: Path) -> str: target = Path(mirage_path) if target.suffix: return mirage_path return str(target.with_suffix(produced.suffix)) def edit_file(self, file_path: str, old_content: str, new_content: str) -> str: """Replace old_content with new_content in a Mirage file. Args: file_path (str): Logical Mirage path. old_content (str): Exact text to find. new_content (str): Replacement text. Returns: str: Success or error message. """ mirage_path = self._to_mirage_path(file_path) try: data = self._read_mirage_bytes(mirage_path).decode( self.default_encoding) except FileNotFoundError: return f"Error: File {mirage_path} does not exist" if old_content not in data: return f"Error: old_content not found in {mirage_path}" new_data = data.replace(old_content, new_content) self._write_mirage_bytes(mirage_path, new_data.encode(self.default_encoding)) return f"Successfully edited file: {mirage_path}" def search_files(self, file_name: str, path: str | None = None) -> str: """Locate files by name pattern via Mirage's find. Args: file_name (str): Glob pattern passed to find -name. path (str | None): Search root; defaults to working_directory. Returns: str: Newline-separated list of matching paths. """ root = self._to_mirage_path(path or self._mirage_root) cmd = f"find {shlex.quote(root)} -name {shlex.quote(file_name)}" io = self._runner.run(self._ws.execute(cmd)) return _io_text(io) def glob_files(self, pattern: str, path: str | None = None) -> str: """Glob via Mirage's find -name. Args: pattern (str): Glob pattern. path (str | None): Search root. Returns: str: Newline-separated list of matches. """ return self.search_files(file_name=pattern, path=path) def grep_files( self, pattern: str, path: str | None = None, file_pattern: str | None = None, ) -> str: """Regex search via Mirage's grep -rn. Args: pattern (str): Regex. path (str | None): Search root. file_pattern (str | None): --include glob. Returns: str: Concatenated grep output. """ root = self._to_mirage_path(path or self._mirage_root) parts = ["grep", "-rn", shlex.quote(pattern)] if file_pattern: parts.insert(2, f"--include={shlex.quote(file_pattern)}") parts.append(shlex.quote(root)) io = self._runner.run(self._ws.execute(" ".join(parts))) return _io_text(io) def _io_text(io) -> str: stdout = io.stdout if isinstance(io.stdout, bytes) else b"" stderr = io.stderr if isinstance(io.stderr, bytes) else b"" out = stdout.decode("utf-8", errors="replace") err = stderr.decode("utf-8", errors="replace") if err and not out: return err if err: return f"{out}\n{err}" return out