chemgraph-loop / src /ui /file_utils.py
rockyaaos's picture
ChemGraph Loop: guarded real-agent API (EMT/TBLite single-point energy)
c509967 verified
Raw
History Blame Contribute Delete
5.41 kB
"""File-system helpers for the ChemGraph Streamlit UI.
Functions for resolving output paths, finding XYZ files, checking file
recency, and extracting directory paths from agent messages.
"""
import os
import re
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Optional
def resolve_output_path(path: str) -> str:
"""Resolve output paths relative to CHEMGRAPH_LOG_DIR when set.
Parameters
----------
path : str
Absolute or relative output path.
Returns
-------
str
Resolved output path.
"""
if not path:
return path
if os.path.isabs(path):
return path
log_dir = os.environ.get("CHEMGRAPH_LOG_DIR")
if log_dir:
return os.path.join(log_dir, path)
return path
def changed_recently(path: str = "ir_spectrum.png", window_seconds: int = 300) -> bool:
"""Return True if a file was modified within a recent time window.
Parameters
----------
path : str, optional
File path to inspect.
window_seconds : int, optional
Recency window in seconds.
Returns
-------
bool
``True`` when the file exists and is recent.
"""
p = Path(resolve_output_path(path))
if not p.exists():
return False
mtime = datetime.fromtimestamp(p.stat().st_mtime, tz=timezone.utc)
now = datetime.now(timezone.utc)
return (now - mtime) <= timedelta(seconds=window_seconds)
def find_latest_xyz_file() -> Optional[str]:
"""Find the most recently modified ``.xyz`` file in the log dir or cwd."""
search_dirs: list[str] = []
log_dir = os.environ.get("CHEMGRAPH_LOG_DIR")
if log_dir:
search_dirs.append(log_dir)
search_dirs.append(os.getcwd())
latest_path: Optional[str] = None
latest_mtime = -1.0
for base in search_dirs:
if not base or not os.path.isdir(base):
continue
for path in Path(base).rglob("*.xyz"):
try:
mtime = path.stat().st_mtime
except OSError:
continue
if mtime > latest_mtime:
latest_mtime = mtime
latest_path = str(path)
return latest_path
def find_latest_xyz_file_in_dir(directory: str) -> Optional[str]:
"""Find the most recently modified ``.xyz`` file under a directory.
Parameters
----------
directory : str
Directory to search recursively.
Returns
-------
str or None
Latest XYZ file path, or ``None`` when none is found.
"""
if not directory or not os.path.isdir(directory):
return None
latest_path: Optional[str] = None
latest_mtime = -1.0
for path in Path(directory).rglob("*.xyz"):
try:
mtime = path.stat().st_mtime
except OSError:
continue
if mtime > latest_mtime:
latest_mtime = mtime
latest_path = str(path)
return latest_path
def extract_log_dir_from_messages(messages: Any) -> Optional[str]:
"""Extract a log directory from messages that reference output files.
Parameters
----------
messages : Any
Message object, dictionary, list, or text to scan.
Returns
-------
str or None
Parent directory of a referenced output file, or ``None``.
"""
if not messages:
return None
patterns = [
r"(/[^\s'\"`]+?\.json)",
r"(/[^\s'\"`]+?\.xyz)",
r"(/[^\s'\"`]+?\.html)",
r"(/[^\s'\"`]+?\.csv)",
]
def _scan_value(value: Any) -> Optional[str]:
"""Recursively scan a value for absolute output-file references.
Parameters
----------
value : Any
Message content, mapping, list, or scalar to scan.
Returns
-------
str or None
Parent directory of a referenced file, or ``None``.
"""
if isinstance(value, str):
for pattern in patterns:
match = re.search(pattern, value)
if match:
path = match.group(1)
if os.path.isabs(path):
return str(Path(path).parent)
elif isinstance(value, dict):
for v in value.values():
found = _scan_value(v)
if found:
return found
elif isinstance(value, list):
for v in value:
found = _scan_value(v)
if found:
return found
return None
for message in reversed(messages):
content = ""
if hasattr(message, "content"):
content = getattr(message, "content", "")
elif isinstance(message, dict):
content = message.get("content", "")
elif isinstance(message, str):
content = message
else:
content = str(message)
if not content:
continue
found = _scan_value(content)
if found:
return found
# Also scan structured tool outputs if present
if hasattr(message, "additional_kwargs"):
found = _scan_value(message.additional_kwargs)
if found:
return found
if isinstance(message, dict):
found = _scan_value(message)
if found:
return found
return None