Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/microbots/auto_memory/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ def _run_one(
stdout_path.open("w", encoding="utf-8") as out_fh,
stderr_path.open("w", encoding="utf-8") as err_fh,
):
# Security note: spec.command is intentionally run with
# shell=True for developer convenience (supports pipes,
# redirects, etc.). This runner assumes configs are loaded
# from trusted local files only. Do NOT use with configs
# sourced from untrusted input.
proc = subprocess.run(
spec.command,
shell=True,
Expand Down
5 changes: 5 additions & 0 deletions src/microbots/auto_memory/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ def validate(self) -> None:
f"'output_path' must be a relative path, got '{self.output_path}'"
)

if ".." in Path(self.output_path).parts:
raise ConfigError(
f"'output_path' must not contain '..', got '{self.output_path}'"
)

if not self.callbacks:
raise ConfigError("'callbacks' must contain at least one entry")

Expand Down
4 changes: 3 additions & 1 deletion src/microbots/auto_memory/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from microbots.auto_memory.config import TaskConfig
from microbots.auto_memory.data_models import Feedback

_JINJA_ENV = JinjaEnvironment(undefined=StrictUndefined, keep_trailing_newline=True)


def build_iteration_context(
config: TaskConfig,
Expand Down Expand Up @@ -40,7 +42,7 @@ def build_iteration_context(
str
The rendered prompt text, ready to be sent to the agent.
"""
env = JinjaEnvironment(undefined=StrictUndefined, keep_trailing_newline=True)
env = _JINJA_ENV
template = env.from_string(config.prompt_template)
return template.render(
task=config.task_definition,
Expand Down
2 changes: 1 addition & 1 deletion src/microbots/auto_memory/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class CallbackError(AutoMemoryError):
"""Raised when a callback cannot be spawned or set up (not a failing assertion)."""


class TimeoutError(AutoMemoryError): # noqa: A001 — intentional shadow of builtin
class AutoMemoryTimeoutError(AutoMemoryError):
"""Raised when the per-iteration or total run timeout is exceeded."""


Expand Down
59 changes: 46 additions & 13 deletions src/microbots/auto_memory/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,20 @@ def read_all(self) -> list[Feedback]:
"""Return all persisted :class:`~microbots.auto_memory.data_models.Feedback` entries.

Returns an empty list if the feedback file does not exist yet.
Lines that contain invalid JSON, non-object JSON, or fields that cannot
be mapped to :class:`~microbots.auto_memory.data_models.Feedback` are
skipped with a ``WARNING`` log message rather than raising an exception.

Returns
-------
list[Feedback]
All feedback entries in the order they were appended.
Successfully parsed entries in the order they were appended.
Corrupt or malformed lines are omitted.

Raises
------
MemoryStoreError
If not mounted or on I/O / parse error.
If not mounted, or if the file cannot be opened (I/O error).
"""
self._require_mounted()
if not self._feedback_path.exists(): # type: ignore[union-attr]
Expand All @@ -130,23 +134,32 @@ def read_all(self) -> list[Feedback]:
try:
data = json.loads(line)
except json.JSONDecodeError as exc:
raise MemoryStoreError(
f"Invalid JSON on line {lineno} of "
f"{self._feedback_path}: {exc}"
) from exc
logger.warning(
"Skipping corrupt JSON on line %d of %s: %s",
lineno,
self._feedback_path,
exc,
Comment thread
KavyaSree2610 marked this conversation as resolved.
)
continue
if not isinstance(data, dict):
raise MemoryStoreError(
f"Expected a JSON object on line {lineno} of "
f"{self._feedback_path}, got {type(data).__name__}"
logger.warning(
"Skipping non-object entry on line %d of %s (got %s)",
lineno,
self._feedback_path,
type(data).__name__,
)
continue
known = {f.name for f in dataclasses.fields(Feedback)}
try:
entries.append(Feedback(**{k: v for k, v in data.items() if k in known}))
except TypeError as exc:
raise MemoryStoreError(
f"Cannot construct Feedback from line {lineno} of "
f"{self._feedback_path}: {exc}"
) from exc
logger.warning(
"Skipping malformed Feedback on line %d of %s: %s",
lineno,
self._feedback_path,
exc,
)
continue
except OSError as exc:
raise MemoryStoreError(f"Failed to read feedback: {exc}") from exc

Expand Down Expand Up @@ -190,3 +203,23 @@ def _require_mounted(self) -> None:
raise MemoryStoreError(
"MemoryStore has not been mounted; call mount() first"
)

@property
def memory_dir(self) -> Path:
"""Absolute path to the memory directory.

Returns
-------
Path
The directory that holds the feedback file.

Raises
------
MemoryStoreError
If the store has not been mounted yet.
"""
if self._memory_dir is None:
raise MemoryStoreError(
"MemoryStore has not been mounted; call mount() first"
)
return self._memory_dir
Loading
Loading