Skip to content

Commit 5e01b48

Browse files
KavyaSree2610Kavya Sree Kaitepalli
andauthored
feat: construct training loop orchestrator (#171)
* feat: contsruct trainling loop orchestrator * add tests * resolve comments --------- Co-authored-by: Kavya Sree Kaitepalli <kkaitepalli@microsoft.com>
1 parent 5feff2f commit 5e01b48

10 files changed

Lines changed: 1076 additions & 29 deletions

File tree

src/microbots/auto_memory/callbacks.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ def _run_one(
145145
stdout_path.open("w", encoding="utf-8") as out_fh,
146146
stderr_path.open("w", encoding="utf-8") as err_fh,
147147
):
148+
# Security note: spec.command is intentionally run with
149+
# shell=True for developer convenience (supports pipes,
150+
# redirects, etc.). This runner assumes configs are loaded
151+
# from trusted local files only. Do NOT use with configs
152+
# sourced from untrusted input.
148153
proc = subprocess.run(
149154
spec.command,
150155
shell=True,

src/microbots/auto_memory/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ def validate(self) -> None:
140140
f"'output_path' must be a relative path, got '{self.output_path}'"
141141
)
142142

143+
if ".." in Path(self.output_path).parts:
144+
raise ConfigError(
145+
f"'output_path' must not contain '..', got '{self.output_path}'"
146+
)
147+
143148
if not self.callbacks:
144149
raise ConfigError("'callbacks' must contain at least one entry")
145150

src/microbots/auto_memory/context.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from microbots.auto_memory.config import TaskConfig
88
from microbots.auto_memory.data_models import Feedback
99

10+
_JINJA_ENV = JinjaEnvironment(undefined=StrictUndefined, keep_trailing_newline=True)
11+
1012

1113
def build_iteration_context(
1214
config: TaskConfig,
@@ -40,7 +42,7 @@ def build_iteration_context(
4042
str
4143
The rendered prompt text, ready to be sent to the agent.
4244
"""
43-
env = JinjaEnvironment(undefined=StrictUndefined, keep_trailing_newline=True)
45+
env = _JINJA_ENV
4446
template = env.from_string(config.prompt_template)
4547
return template.render(
4648
task=config.task_definition,

src/microbots/auto_memory/errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class CallbackError(AutoMemoryError):
1717
"""Raised when a callback cannot be spawned or set up (not a failing assertion)."""
1818

1919

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

2323

src/microbots/auto_memory/memory.py

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -105,16 +105,20 @@ def read_all(self) -> list[Feedback]:
105105
"""Return all persisted :class:`~microbots.auto_memory.data_models.Feedback` entries.
106106
107107
Returns an empty list if the feedback file does not exist yet.
108+
Lines that contain invalid JSON, non-object JSON, or fields that cannot
109+
be mapped to :class:`~microbots.auto_memory.data_models.Feedback` are
110+
skipped with a ``WARNING`` log message rather than raising an exception.
108111
109112
Returns
110113
-------
111114
list[Feedback]
112-
All feedback entries in the order they were appended.
115+
Successfully parsed entries in the order they were appended.
116+
Corrupt or malformed lines are omitted.
113117
114118
Raises
115119
------
116120
MemoryStoreError
117-
If not mounted or on I/O / parse error.
121+
If not mounted, or if the file cannot be opened (I/O error).
118122
"""
119123
self._require_mounted()
120124
if not self._feedback_path.exists(): # type: ignore[union-attr]
@@ -130,23 +134,32 @@ def read_all(self) -> list[Feedback]:
130134
try:
131135
data = json.loads(line)
132136
except json.JSONDecodeError as exc:
133-
raise MemoryStoreError(
134-
f"Invalid JSON on line {lineno} of "
135-
f"{self._feedback_path}: {exc}"
136-
) from exc
137+
logger.warning(
138+
"Skipping corrupt JSON on line %d of %s: %s",
139+
lineno,
140+
self._feedback_path,
141+
exc,
142+
)
143+
continue
137144
if not isinstance(data, dict):
138-
raise MemoryStoreError(
139-
f"Expected a JSON object on line {lineno} of "
140-
f"{self._feedback_path}, got {type(data).__name__}"
145+
logger.warning(
146+
"Skipping non-object entry on line %d of %s (got %s)",
147+
lineno,
148+
self._feedback_path,
149+
type(data).__name__,
141150
)
151+
continue
142152
known = {f.name for f in dataclasses.fields(Feedback)}
143153
try:
144154
entries.append(Feedback(**{k: v for k, v in data.items() if k in known}))
145155
except TypeError as exc:
146-
raise MemoryStoreError(
147-
f"Cannot construct Feedback from line {lineno} of "
148-
f"{self._feedback_path}: {exc}"
149-
) from exc
156+
logger.warning(
157+
"Skipping malformed Feedback on line %d of %s: %s",
158+
lineno,
159+
self._feedback_path,
160+
exc,
161+
)
162+
continue
150163
except OSError as exc:
151164
raise MemoryStoreError(f"Failed to read feedback: {exc}") from exc
152165

@@ -190,3 +203,23 @@ def _require_mounted(self) -> None:
190203
raise MemoryStoreError(
191204
"MemoryStore has not been mounted; call mount() first"
192205
)
206+
207+
@property
208+
def memory_dir(self) -> Path:
209+
"""Absolute path to the memory directory.
210+
211+
Returns
212+
-------
213+
Path
214+
The directory that holds the feedback file.
215+
216+
Raises
217+
------
218+
MemoryStoreError
219+
If the store has not been mounted yet.
220+
"""
221+
if self._memory_dir is None:
222+
raise MemoryStoreError(
223+
"MemoryStore has not been mounted; call mount() first"
224+
)
225+
return self._memory_dir

0 commit comments

Comments
 (0)