···11+import builtins
22+import importlib
33+import logging
44+import os
55+import sys
66+import traceback
77+import types
88+import pickle
99+import subprocess
1010+import base64
1111+1212+SANDBOX_TIMEOUT = 10
1313+1414+# Configure a logger for the sandbox (in real use, configure handlers/level as needed)
1515+logger = logging.getLogger(__name__)
1616+logger.setLevel(logging.INFO) # or DEBUG for more verbosity
1717+1818+1919+def _run_user_code(
2020+ code: str,
2121+ allow_installs: bool,
2222+ allowed_path: str,
2323+ blacklist: list,
2424+ available_functions: dict,
2525+ log: bool = False,
2626+) -> tuple[dict, str]:
2727+ """
2828+ Execute code under sandboxed conditions (limited file access, optional installs,
2929+ and blacklisting) and return the resulting locals and an error message.
3030+ """
3131+ try:
3232+ # Optional: apply working directory and file access restriction
3333+ if allowed_path:
3434+ allowed = os.path.abspath(allowed_path)
3535+ try:
3636+ os.chdir(allowed) # Change working dir to the allowed_path
3737+ except Exception as e:
3838+ # If we cannot chdir, log but continue (the open wrapper will still enforce path)
3939+ logger.warning(
4040+ "Could not change working directory to %s: %s", allowed, e
4141+ )
4242+ # Wrap builtins.open to restrict file access
4343+ orig_open = builtins.open
4444+4545+ def secure_open(file, *args, **kwargs):
4646+ """Open that restricts file access to allowed_path."""
4747+ # If file is a file object or path-like, get its string path
4848+ path = (
4949+ file if isinstance(file, str) else getattr(file, "name", str(file))
5050+ )
5151+ full_path = os.path.abspath(path if path is not None else "")
5252+ if not full_path.startswith(allowed):
5353+ raise PermissionError(
5454+ f"Access to '{full_path}' is denied by sandbox."
5555+ )
5656+ return orig_open(file, *args, **kwargs)
5757+5858+ builtins.open = secure_open
5959+6060+ # Optionally, restrict other file-related functions (remove, rename, etc.) similarly
6161+ # We'll patch a couple of common ones as an example:
6262+ orig_remove = os.remove
6363+6464+ def secure_remove(path, *args, **kwargs):
6565+ full_path = os.path.abspath(path)
6666+ if not full_path.startswith(allowed):
6767+ raise PermissionError(
6868+ f"Removal of '{full_path}' is denied by sandbox."
6969+ )
7070+ return orig_remove(path, *args, **kwargs)
7171+7272+ os.remove = secure_remove
7373+7474+ orig_rename = os.rename
7575+7676+ def secure_rename(src, dst, *args, **kwargs):
7777+ full_src = os.path.abspath(src)
7878+ full_dst = os.path.abspath(dst)
7979+ if not full_src.startswith(allowed) or not full_dst.startswith(allowed):
8080+ raise PermissionError(
8181+ "Rename operation outside allowed path is denied by sandbox."
8282+ )
8383+ return orig_rename(src, dst, *args, **kwargs)
8484+8585+ os.rename = secure_rename
8686+8787+ # Apply blacklist restrictions by removing or disabling blacklisted builtins or attributes
8888+ if blacklist:
8989+ for name in blacklist:
9090+ # If the name has a dot, like "os.system", handle module attributes
9191+ if "." in name:
9292+ mod_name, attr_name = name.split(".", 1)
9393+ try:
9494+ mod_obj = importlib.import_module(mod_name)
9595+ except ImportError:
9696+ mod_obj = None
9797+ # If module is imported in sandbox, remove the attribute
9898+ if mod_obj and hasattr(mod_obj, attr_name):
9999+ try:
100100+ setattr(
101101+ mod_obj, attr_name, None
102102+ ) # simple way: nullify the attribute
103103+ except Exception:
104104+ pass # if we cannot set it, ignore (might be read-only)
105105+ else:
106106+ # It's a built-in or global name; remove from builtins if present
107107+ if name in builtins.__dict__:
108108+ builtins.__dict__[name] = (
109109+ None # or we could del, but setting None prevents use
110110+ )
111111+ # Additionally, we can ensure __builtins__ in the exec env doesn't contain them (handled below in exec)
112112+113113+ # If allowed, handle package installations inside sandbox (in case code itself triggers ImportError)
114114+ if allow_installs:
115115+ # We will install missing imports on the fly during execution if an ImportError occurs.
116116+ # One approach: wrap __import__ to catch failed imports and pip install.
117117+ orig_import = builtins.__import__
118118+119119+ def custom_import(name, globals=None, locals=None, fromlist=(), level=0):
120120+ try:
121121+ return orig_import(name, globals, locals, fromlist, level)
122122+ except ImportError as e:
123123+ pkg = name.split(".")[0]
124124+ logger.info(
125125+ "Sandbox: attempting to install missing package '%s'", pkg
126126+ )
127127+ try:
128128+ subprocess.run(
129129+ [sys.executable, "-m", "pip", "install", pkg],
130130+ check=True,
131131+ stdout=subprocess.DEVNULL,
132132+ stderr=subprocess.DEVNULL,
133133+ )
134134+ except Exception as inst_err:
135135+ # If installation fails, re-raise the original ImportError
136136+ logger.error(
137137+ "Sandbox: failed to install package %s: %s", pkg, inst_err
138138+ )
139139+ raise e
140140+ # Retry the import after installation
141141+ return orig_import(name, globals, locals, fromlist, level)
142142+143143+ builtins.__import__ = custom_import
144144+145145+ # Prepare an isolated execution namespace. We use an empty globals dict with a fresh builtins.
146146+ exec_globals = {"__builtins__": builtins.__dict__}
147147+148148+ # Add any provided functions to the execution environment
149149+ if available_functions:
150150+ exec_globals.update(available_functions)
151151+152152+ exec_locals = {} # local variables will be collected here
153153+154154+ error_msg = None
155155+ try:
156156+ exec(code, exec_globals, exec_locals) # Execute the user's code
157157+ except Exception as e:
158158+ # Catch any exception and format it
159159+ tb = traceback.format_exc()
160160+ error_msg = f"Exception in sandboxed code:\n{tb}"
161161+ if log:
162162+ logger.error("Sandbox: code raised an exception: %s", e)
163163+ except SystemExit as e:
164164+ # Handle sys.exit calls (which raise SystemExit)
165165+ code_val = e.code if isinstance(e.code, int) or e.code else 0
166166+ if code_val != 0:
167167+ error_msg = f"Sandboxed code called sys.exit({code_val})"
168168+ if log:
169169+ logger.warning(
170170+ "Sandbox: code exited with non-zero status %s", code_val
171171+ )
172172+ # For sys.exit(0), we treat it as normal termination (no error)
173173+174174+ # Clean up any blacklisted or internal entries in locals
175175+ exec_locals.pop("__builtins__", None)
176176+177177+ # Collect only picklable locals for returning
178178+ safe_locals = {}
179179+ for var, val in exec_locals.items():
180180+ try:
181181+ pickle.dumps(val) # test picklability
182182+ safe_locals[var] = val
183183+ except Exception:
184184+ safe_locals[var] = repr(val) # fallback: use string representation
185185+186186+ if log:
187187+ logger.info("Sandbox execution finished")
188188+189189+ return safe_locals, error_msg
190190+191191+ except Exception as e:
192192+ # Catch any unhandled exceptions in the worker process
193193+ if log:
194194+ logger.error(
195195+ "Unhandled exception in sandbox worker: %s", traceback.format_exc()
196196+ )
197197+ return None, f"Sandbox worker error: {str(e)}"
198198+199199+200200+def execute_sandboxed_code(
201201+ code: str,
202202+ timeout: int = SANDBOX_TIMEOUT,
203203+ allow_installs: bool = False,
204204+ requirements_path: str = None,
205205+ allowed_path: str = None,
206206+ blacklist: list = None,
207207+ available_functions: dict = None,
208208+ import_module: str = None,
209209+ log: bool = False,
210210+) -> tuple[dict, str]:
211211+ """
212212+ Execute the given Python code string in a sandboxed subprocess with specified restrictions.
213213+214214+ Parameters:
215215+ code (str): The Python code to execute.
216216+ timeout (int): Maximum execution time in seconds for the sandboxed code (default 10 seconds).
217217+ allow_installs (bool): If True, allow installing missing packages via pip (default False).
218218+ requirements_path (str): Path to a requirements.txt file to install before execution.
219219+ allowed_path (str): Directory path that the code is allowed to access for file I/O.
220220+ File operations outside this path will be blocked. If None, no extra file restrictions are applied.
221221+ blacklist (list): List of names (builtins or module attributes) that are disallowed in the code.
222222+ If the code uses any of these, it will be prevented or result in an error.
223223+ available_functions (dict): Dictionary of functions to make available in the sandboxed environment.
224224+ The keys are the function names, and the values are the function objects.
225225+ import_module (str): Name of a Python module to import and make all its functions available in the sandbox.
226226+227227+ Returns:
228228+ (dict, str): A tuple containing the dictionary of local variables from the executed code (or None on failure),
229229+ and an error message (str) if an error/exception occurred, or None if execution was successful.
230230+ """
231231+ # Step 1: If package installs are allowed, handle requirements and prepare environment
232232+ if requirements_path:
233233+ if os.path.isfile(requirements_path):
234234+ logger.info(
235235+ "Installing packages from requirements file: %s", requirements_path
236236+ )
237237+ try:
238238+ subprocess.run(
239239+ [sys.executable, "-m", "pip", "install", "-r", requirements_path],
240240+ check=True,
241241+ stdout=subprocess.DEVNULL,
242242+ stderr=subprocess.DEVNULL,
243243+ )
244244+ except Exception as e:
245245+ logger.error(
246246+ "Failed to install requirements from %s: %s", requirements_path, e
247247+ )
248248+ # If requirements fail to install, we can choose to abort or continue. Here, abort execution.
249249+ return None, f"Failed to install requirements: {e}"
250250+ else:
251251+ logger.error("Requirements file %s not found.", requirements_path)
252252+ return None, f"Requirements file not found: {requirements_path}"
253253+254254+ # If a module name is provided, import it and add its functions to available_functions
255255+ if isinstance(available_functions, str) and not import_module:
256256+ import_module = available_functions
257257+ available_functions = None
258258+259259+ if import_module:
260260+ try:
261261+ module = importlib.import_module(import_module)
262262+ if available_functions is None:
263263+ available_functions = {}
264264+ for name in dir(module):
265265+ if not name.startswith("_"):
266266+ attr = getattr(module, name)
267267+ if callable(attr):
268268+ available_functions[name] = attr
269269+ except ImportError as e:
270270+ logger.error(f"Failed to import module {import_module}: {e}")
271271+ return None, f"Failed to import module {import_module}: {e}"
272272+273273+ # Step 2: Execute the code in a separate Python subprocess
274274+ params = {
275275+ "code": code,
276276+ "allow_installs": allow_installs,
277277+ "allowed_path": allowed_path,
278278+ "blacklist": blacklist or [],
279279+ "available_functions": available_functions or {},
280280+ "log": log,
281281+ }
282282+283283+ env = os.environ.copy()
284284+ env["SANDBOX_PARAMS"] = base64.b64encode(pickle.dumps(params)).decode()
285285+286286+ try:
287287+ result = subprocess.run(
288288+ [sys.executable, "-m", "mem_agent.engine"],
289289+ stdout=subprocess.PIPE,
290290+ stderr=subprocess.PIPE,
291291+ timeout=timeout,
292292+ env=env,
293293+ )
294294+ except subprocess.TimeoutExpired:
295295+ logger.error(
296296+ "Sandboxed code exceeded time limit of %d seconds; terminating.", timeout
297297+ )
298298+ return None, f"TimeoutError: Code execution exceeded {timeout} seconds."
299299+300300+ if result.returncode != 0:
301301+ return None, result.stderr.decode().strip()
302302+303303+ print("stderr:", result.stderr.decode())
304304+ print("stdout:", result.stdout[:200])
305305+306306+ try:
307307+ local_vars, error_msg = pickle.loads(result.stdout)
308308+ except Exception as e:
309309+ return None, f"Failed to decode sandbox output: {e}"
310310+311311+ if error_msg is None:
312312+ error_msg = ""
313313+314314+ return local_vars, error_msg
315315+316316+def _subprocess_entry() -> None:
317317+ """Entry point for sandbox subprocess."""
318318+ params_b64 = os.environ.get("SANDBOX_PARAMS")
319319+ if not params_b64:
320320+ sys.exit(1)
321321+ params = pickle.loads(base64.b64decode(params_b64))
322322+ locals_dict, error = _run_user_code(
323323+ params["code"],
324324+ params.get("allow_installs", False),
325325+ params.get("allowed_path"),
326326+ params.get("blacklist", []),
327327+ params.get("available_functions", {}),
328328+ params.get("log", False),
329329+ )
330330+ sys.stdout.buffer.write(pickle.dumps((locals_dict, error)))
331331+332332+333333+if __name__ == "__main__":
334334+ _subprocess_entry()
+359
server/mem_agent/tools.py
···11+import os
22+import tempfile
33+import uuid
44+import subprocess
55+from pathlib import Path
66+from typing import Union
77+88+from server.mem_agent.utils import MEMORY_PATH, check_size_limits, create_memory_if_not_exists
99+1010+def get_size(file_or_dir_path: str) -> int:
1111+ """
1212+ Get the size of a file or directory.
1313+1414+ Args:
1515+ file_or_dir_path: The path to the file or directory.
1616+ If empty string, returns total memory directory size.
1717+1818+ Returns:
1919+ The size of the file or directory in bytes.
2020+ """
2121+ # Handle empty string by returning total memory size
2222+ if not file_or_dir_path or file_or_dir_path == "":
2323+ # Get the current working directory (which should be the memory root)
2424+ cwd = os.getcwd()
2525+ total_size = 0
2626+ for dirpath, dirnames, filenames in os.walk(cwd):
2727+ for filename in filenames:
2828+ file_path = os.path.join(dirpath, filename)
2929+ try:
3030+ total_size += os.path.getsize(file_path)
3131+ except OSError:
3232+ pass
3333+ return total_size
3434+3535+ # Otherwise check the specific path
3636+ if os.path.isfile(file_or_dir_path):
3737+ return os.path.getsize(file_or_dir_path)
3838+ elif os.path.isdir(file_or_dir_path):
3939+ total_size = 0
4040+ for dirpath, dirnames, filenames in os.walk(file_or_dir_path):
4141+ for filename in filenames:
4242+ file_path = os.path.join(dirpath, filename)
4343+ try:
4444+ total_size += os.path.getsize(file_path)
4545+ except OSError:
4646+ pass
4747+ return total_size
4848+ else:
4949+ raise FileNotFoundError(f"Path not found: {file_or_dir_path}")
5050+5151+def create_file(file_path: str, content: str = "") -> bool:
5252+ """
5353+ Create a new file in the memory with the given content (if any).
5454+ First create a temporary file with the given content, check if
5555+ the size limits are respected, if so, move the temporary file to
5656+ the final destination.
5757+5858+ Args:
5959+ file_path: The path to the file.
6060+ content: The content of the file.
6161+6262+ Returns:
6363+ True if the file was created successfully, False otherwise.
6464+ """
6565+ temp_file_path = None
6666+ try:
6767+ # Create parent directories if they don't exist
6868+ parent_dir = os.path.dirname(file_path)
6969+ if parent_dir and not os.path.exists(parent_dir):
7070+ os.makedirs(parent_dir, exist_ok=True)
7171+7272+ # Create a unique temporary file name in the same directory as the target file
7373+ # This ensures the temp file is within the sandbox's allowed path
7474+ target_dir = os.path.dirname(os.path.abspath(file_path)) or "."
7575+ temp_file_path = os.path.join(target_dir, f"temp_{uuid.uuid4().hex[:8]}.txt")
7676+7777+ with open(temp_file_path, "w") as f:
7878+ f.write(content)
7979+8080+ if check_size_limits(temp_file_path):
8181+ # Move the content to the final destination
8282+ with open(file_path, "w") as f:
8383+ f.write(content)
8484+ os.remove(temp_file_path)
8585+ return True
8686+ else:
8787+ os.remove(temp_file_path)
8888+ raise Exception(f"File {file_path} is too large to create")
8989+ except Exception as e:
9090+ # Clean up temp file if it exists
9191+ if temp_file_path and os.path.exists(temp_file_path):
9292+ try:
9393+ os.remove(temp_file_path)
9494+ except Exception as e:
9595+ raise Exception(f"Error removing temp file {temp_file_path}: {e}")
9696+ raise Exception(f"Error creating file {file_path}: {e}")
9797+9898+def create_dir(dir_path: str) -> bool:
9999+ """
100100+ Create a new directory in the memory.
101101+102102+ Args:
103103+ dir_path: The path to the directory.
104104+105105+ Returns:
106106+ True if the directory was created successfully, False otherwise.
107107+ """
108108+ try:
109109+ os.makedirs(dir_path, exist_ok=True)
110110+ return True
111111+ except Exception:
112112+ return False
113113+114114+115115+def update_file(file_path: str, old_content: str, new_content: str) -> Union[bool, str]:
116116+ """
117117+ Simple find-and-replace update method for files.
118118+119119+ This is an easier alternative to write_to_file() that doesn't require
120120+ creating git-style diffs. It performs a simple string replacement.
121121+122122+ Parameters
123123+ ----------
124124+ file_path : str
125125+ Path to the file to update.
126126+ old_content : str
127127+ The exact text to find and replace in the file.
128128+ new_content : str
129129+ The text to replace old_content with.
130130+131131+ Returns
132132+ -------
133133+ Union[bool, str]
134134+ True if successful, error message string if failed.
135135+136136+ Examples
137137+ --------
138138+ # Add a new row to a table
139139+ old = "| TKT-1056 | 2024-09-25 | Late Delivery | Resolved |"
140140+ new = "| TKT-1056 | 2024-09-25 | Late Delivery | Resolved |\\n| TKT-1057 | 2024-11-11 | Damaged Item | Open |"
141141+ result = update_file("user.md", old, new)
142142+ """
143143+ try:
144144+ # Read the current file content
145145+ if not os.path.exists(file_path):
146146+ return f"Error: File '{file_path}' does not exist"
147147+148148+ if not os.path.isfile(file_path):
149149+ return f"Error: '{file_path}' is not a file"
150150+151151+ with open(file_path, "r") as f:
152152+ current_content = f.read()
153153+154154+ # Check if old_content exists in the file
155155+ if old_content not in current_content:
156156+ # Provide helpful context about what wasn't found
157157+ preview_length = 50
158158+ preview = old_content[:preview_length] + "..." if len(old_content) > preview_length else old_content
159159+ return f"Error: Could not find the specified content in the file. Looking for: '{preview}'"
160160+161161+ # Count occurrences to warn about multiple matches
162162+ occurrences = current_content.count(old_content)
163163+ if occurrences > 1:
164164+ # Still proceed but warn the user
165165+ print(f"Warning: Found {occurrences} occurrences of the content. Replacing only the first one.")
166166+167167+ # Perform the replacement (only first occurrence)
168168+ updated_content = current_content.replace(old_content, new_content, 1)
169169+170170+ # Check if replacement actually changed anything
171171+ if updated_content == current_content:
172172+ return "Error: No changes were made to the file"
173173+174174+ # Write the updated content back
175175+ with open(file_path, "w") as f:
176176+ f.write(updated_content)
177177+178178+ return True
179179+180180+ except PermissionError:
181181+ return f"Error: Permission denied writing to '{file_path}'"
182182+ except Exception as e:
183183+ return f"Error: Unexpected error - {str(e)}"
184184+185185+def read_file(file_path: str) -> str:
186186+ """
187187+ Read a file in the memory.
188188+189189+ Args:
190190+ file_path: The path to the file.
191191+192192+ Returns:
193193+ The content of the file, or an error message if the file cannot be read.
194194+ """
195195+ try:
196196+ # Ensure the file path is properly resolved
197197+ if not os.path.exists(file_path):
198198+ return f"Error: File {file_path} does not exist"
199199+200200+ if not os.path.isfile(file_path):
201201+ return f"Error: {file_path} is not a file"
202202+203203+ with open(file_path, "r") as f:
204204+ return f.read()
205205+ except PermissionError:
206206+ return f"Error: Permission denied accessing {file_path}"
207207+ except Exception as e:
208208+ return f"Error: {e}"
209209+210210+def list_files() -> str:
211211+ """
212212+ Display all files and directories in the current working directory as a tree structure.
213213+214214+ Example output:
215215+ ```
216216+ ./
217217+ ├── user.md
218218+ └── entities/
219219+ ├── 452_willow_creek_dr.md
220220+ └── frank_miller_plumbing.md
221221+ ```
222222+223223+ Returns:
224224+ A string representation of the directory tree.
225225+ """
226226+ try:
227227+ # Always use current working directory
228228+ dir_path = os.getcwd()
229229+230230+ def build_tree(start_path, prefix="", is_last=True):
231231+ """Recursively build tree structure"""
232232+ entries = []
233233+ try:
234234+ items = sorted(os.listdir(start_path))
235235+ # Filter out hidden files and __pycache__
236236+ items = [item for item in items if not item.startswith('.') and item != '__pycache__']
237237+ except PermissionError:
238238+ return f"{prefix}[Permission Denied]\n"
239239+240240+ if not items:
241241+ return ""
242242+243243+ for i, item in enumerate(items):
244244+ item_path = os.path.join(start_path, item)
245245+ is_last_item = i == len(items) - 1
246246+247247+ # Choose the right prefix characters
248248+ if is_last_item:
249249+ current_prefix = prefix + "└── "
250250+ extension = prefix + " "
251251+ else:
252252+ current_prefix = prefix + "├── "
253253+ extension = prefix + "│ "
254254+255255+ if os.path.isdir(item_path):
256256+ # Check if directory is empty
257257+ try:
258258+ dir_contents = [f for f in os.listdir(item_path)
259259+ if not f.startswith('.') and f != '__pycache__']
260260+ if not dir_contents:
261261+ entries.append(f"{current_prefix}{item}/ (empty)\n")
262262+ else:
263263+ entries.append(f"{current_prefix}{item}/\n")
264264+ # Recursively add subdirectory contents
265265+ entries.append(build_tree(item_path, extension, is_last_item))
266266+ except PermissionError:
267267+ entries.append(f"{current_prefix}{item}/ [Permission Denied]\n")
268268+ else:
269269+ entries.append(f"{current_prefix}{item}\n")
270270+271271+ return "".join(entries)
272272+273273+ # Start with the root directory
274274+ tree = f"./\n{build_tree(dir_path)}"
275275+ return tree.rstrip() # Remove trailing newline
276276+277277+ except Exception as e:
278278+ return f"Error: {e}"
279279+280280+def delete_file(file_path: str) -> bool:
281281+ """
282282+ Delete a file in the memory.
283283+284284+ Args:
285285+ file_path: The path to the file.
286286+287287+ Returns:
288288+ True if the file was deleted successfully, False otherwise.
289289+ """
290290+ try:
291291+ os.remove(file_path)
292292+ return True
293293+ except Exception:
294294+ return False
295295+296296+def go_to_link(link_string: str) -> str:
297297+ """
298298+ Go to a link in the memory and return the content of the note Y. A link in a note X to a note Y, with the
299299+ path path/to/note/Y.md, is structured like this:
300300+ [[path/to/note/Y]]
301301+302302+ Args:
303303+ link_string: The link to go to.
304304+305305+ Returns:
306306+ The content of the note Y, or an error message if the link cannot be accessed.
307307+ """
308308+ try:
309309+ # Handle Obsidian-style links: [[path/to/note]] -> path/to/note.md
310310+ if link_string.startswith("[[") and link_string.endswith("]]"):
311311+ file_path = link_string[2:-2] # Remove [[ and ]]
312312+ if not file_path.endswith('.md'):
313313+ file_path += '.md'
314314+ else:
315315+ file_path = link_string
316316+317317+ # Ensure the file path is properly resolved
318318+ if not os.path.exists(file_path):
319319+ return f"Error: File {file_path} not found"
320320+321321+ if not os.path.isfile(file_path):
322322+ return f"Error: {file_path} is not a file"
323323+324324+ with open(file_path, "r") as f:
325325+ return f.read()
326326+ except PermissionError:
327327+ return f"Error: Permission denied accessing {link_string}"
328328+ except Exception as e:
329329+ return f"Error: {e}"
330330+331331+def check_if_file_exists(file_path: str) -> bool:
332332+ """
333333+ Check if a file exists in the given filepath.
334334+335335+ Args:
336336+ file_path: The path to the file.
337337+338338+ Returns:
339339+ True if the file exists and is a file, False otherwise.
340340+ """
341341+ try:
342342+ return os.path.exists(file_path) and os.path.isfile(file_path)
343343+ except (OSError, TypeError, ValueError):
344344+ return False
345345+346346+def check_if_dir_exists(dir_path: str) -> bool:
347347+ """
348348+ Check if a directory exists in the given filepath.
349349+350350+ Args:
351351+ dir_path: The path to the directory.
352352+353353+ Returns:
354354+ True if the directory exists and is a directory, False otherwise.
355355+ """
356356+ try:
357357+ return os.path.exists(dir_path) and os.path.isdir(dir_path)
358358+ except (OSError, TypeError, ValueError):
359359+ return False
+203
server/mem_agent/utils.py
···11+import os
22+import shutil
33+44+import black
55+from ..config import MEMORY_PATH
66+77+# Memory
88+FILE_SIZE_LIMIT = 1024 * 1024 # 1MB
99+DIR_SIZE_LIMIT = 1024 * 1024 * 10 # 10MB
1010+MEMORY_SIZE_LIMIT = 1024 * 1024 * 100 # 100MB
1111+1212+1313+def check_file_size_limit(file_path: str) -> bool:
1414+ """
1515+ Check if the file size limit is respected.
1616+ """
1717+ return os.path.getsize(file_path) <= FILE_SIZE_LIMIT
1818+1919+2020+def check_dir_size_limit(dir_path: str) -> bool:
2121+ """
2222+ Check if the directory size limit is respected.
2323+ """
2424+ return os.path.getsize(dir_path) <= DIR_SIZE_LIMIT
2525+2626+2727+def check_memory_size_limit() -> bool:
2828+ """
2929+ Check if the memory size limit is respected.
3030+ """
3131+ current_working_dir = os.getcwd()
3232+ return os.path.getsize(current_working_dir) <= MEMORY_SIZE_LIMIT
3333+3434+3535+def check_size_limits(file_or_dir_path: str) -> bool:
3636+ """
3737+ Check if the size limits are respected.
3838+ """
3939+ if file_or_dir_path == "":
4040+ return check_memory_size_limit()
4141+ elif os.path.isdir(file_or_dir_path):
4242+ return check_dir_size_limit(file_or_dir_path) and check_memory_size_limit()
4343+ elif os.path.isfile(file_or_dir_path):
4444+ parent_dir = os.path.dirname(file_or_dir_path)
4545+ if not parent_dir == "":
4646+ return (
4747+ check_file_size_limit(file_or_dir_path)
4848+ and check_dir_size_limit(parent_dir)
4949+ and check_memory_size_limit()
5050+ )
5151+ else:
5252+ return check_file_size_limit(file_or_dir_path) and check_memory_size_limit()
5353+ else:
5454+ return False
5555+5656+5757+def create_memory_if_not_exists(path: str = MEMORY_PATH):
5858+ """
5959+ Create the memory if it doesn't exist.
6060+6161+ Args:
6262+ path: The path to create. Defaults to MEMORY_PATH.
6363+6464+ Returns:
6565+ None
6666+ """
6767+ try:
6868+ if not os.path.exists(path):
6969+ os.makedirs(path, exist_ok=True)
7070+ except Exception as e:
7171+ print(f"Error creating memory directory at {path}: {e}")
7272+7373+7474+def delete_memory(path: str = MEMORY_PATH) -> None:
7575+ """
7676+ Delete the memory.
7777+7878+ Args:
7979+ path: The path to delete. Defaults to MEMORY_PATH.
8080+ """
8181+ if os.path.exists(path):
8282+ shutil.rmtree(path)
8383+8484+8585+def _format_python_code_with_black(code: str) -> str:
8686+ """
8787+ Format Python code using Black formatter.
8888+8989+ Args:
9090+ code: The Python code to format
9191+9292+ Returns:
9393+ The formatted Python code, or original code if formatting fails
9494+ """
9595+ if not code.strip():
9696+ return code
9797+9898+ try:
9999+ # For incomplete code fragments, wrap them in a function to make them valid Python
100100+ # This helps Black parse and format them correctly
101101+ lines = code.strip().split('\n')
102102+103103+ # Check if code looks like complete statements or just expressions/fragments
104104+ needs_wrapping = True
105105+ for line in lines:
106106+ stripped = line.strip()
107107+ if (stripped.startswith(('def ', 'class ', 'import ', 'from ')) or
108108+ stripped.startswith(('if ', 'for ', 'while ', 'try:', 'with ')) or
109109+ '=' in stripped or stripped.startswith(('print(', 'return '))):
110110+ needs_wrapping = False
111111+ break
112112+113113+ if needs_wrapping:
114114+ # Wrap in a function to make it valid Python for Black
115115+ wrapped_code = f"def temp_function():\n" + "\n".join(f" {line}" for line in lines)
116116+117117+ try:
118118+ formatted_wrapped = black.format_str(
119119+ wrapped_code,
120120+ mode=black.FileMode(
121121+ line_length=88,
122122+ string_normalization=True,
123123+ is_pyi=False,
124124+ )
125125+ )
126126+ # Extract the formatted content back out, removing the wrapper
127127+ formatted_lines = formatted_wrapped.split('\n')[1:] # Skip "def temp_function():"
128128+ formatted_code = '\n'.join(line[4:] if line.startswith(' ') else line
129129+ for line in formatted_lines if line.strip()).strip()
130130+ return formatted_code
131131+ except:
132132+ # If wrapping fails, try formatting as-is
133133+ pass
134134+135135+ # Try formatting the code as-is
136136+ formatted_code = black.format_str(
137137+ code,
138138+ mode=black.FileMode(
139139+ line_length=88,
140140+ string_normalization=True,
141141+ is_pyi=False,
142142+ )
143143+ )
144144+ return formatted_code
145145+146146+ except (black.InvalidInput, ValueError, SyntaxError, Exception) as e:
147147+ # If Black fails to format (e.g., invalid syntax), return original code
148148+ # This ensures we don't break the training pipeline
149149+ return code
150150+151151+152152+def extract_python_code(response: str) -> str:
153153+ """
154154+ Extract the python code from the response and format it with Black.
155155+156156+ Args:
157157+ response: The response from the model.
158158+159159+ Returns:
160160+ The formatted python code from the response.
161161+ """
162162+ if "<python>" in response and "</python>" in response:
163163+ response = response.split("<python>")[1].split("</python>")[0]
164164+ if "```" in response:
165165+ code = response.split("```")[1].split("```")[0]
166166+ else:
167167+ code = response
168168+169169+ # Format the extracted code with Black
170170+ return _format_python_code_with_black(code)
171171+ else:
172172+ return ""
173173+174174+175175+def extract_reply(response: str) -> str:
176176+ """
177177+ Extract the reply from the response.
178178+ """
179179+ if "<reply>" in response and "</reply>" in response:
180180+ return response.split("<reply>")[1].split("</reply>")[0]
181181+ else:
182182+ return ""
183183+184184+185185+def extract_thoughts(response: str) -> str:
186186+ """
187187+ Extract the thoughts from the response.
188188+ """
189189+ if "<think>" in response and "</think>" in response:
190190+ return response.split("<think>")[1].split("</think>")[0]
191191+ else:
192192+ return ""
193193+194194+195195+def format_results(results: dict, error_msg: str = "") -> str:
196196+ """
197197+ Format the results into a string.
198198+ """
199199+ return (
200200+ "<result>\n(" + str(results) + ", {" + error_msg + "})\n</result>"
201201+ if error_msg
202202+ else "<result>\n" + str(results) + "\n</result>"
203203+ )