diff --git a/sia/iteration_parser.py b/sia/iteration_parser.py new file mode 100644 index 0000000..ed37f21 --- /dev/null +++ b/sia/iteration_parser.py @@ -0,0 +1,146 @@ +from datetime import datetime +import xml.etree.ElementTree as ET +from typing import List + +from .entry import Entry +from .entry.background_entry import BackgroundEntry +from .entry.parse_error_entry import ParseErrorEntry +from .entry.read_entry import ReadEntry +from .entry.reasoning_entry import ReasoningEntry +from .entry.repeat_entry import RepeatEntry +from .entry.single_entry import SingleEntry +from .entry.write_entry import WriteEntry + +class IterationParser: + """Parses iteration XML files into entries""" + + @staticmethod + def parse_iteration(content: str, io_buffer) -> tuple[str, str, List[Entry]]: + """ + Parse iteration XML content into context, response and entries + + Args: + content: Raw XML content as string + io_buffer: Buffer to use for IO operations + + Returns: + Tuple of (context_str, response_str, entry_list) + """ + root = ET.fromstring(content) + context_elem = root.find("context") + + if context_elem is None: + raise ValueError("Invalid iteration file - missing context") + + context = ET.fromstring(context_elem.text) + + entries = [] + for elem in context: + if elem.tag == "background": + entries.append(IterationParser._parse_background(elem)) + elif elem.tag == "parse_error": + entries.append(IterationParser._parse_parse_error(elem)) + elif elem.tag == "read_stdin": + entries.append(IterationParser._parse_read(elem, io_buffer)) + elif elem.tag == "reasoning": + entries.append(IterationParser._parse_reasoning(elem)) + elif elem.tag == "repeat": + entries.append(IterationParser._parse_repeat(elem)) + elif elem.tag == "single": + entries.append(IterationParser._parse_single(elem)) + elif elem.tag == "write_stdout": + entries.append(IterationParser._parse_write(elem, io_buffer)) + + return entries + + @staticmethod + def _parse_background(elem: ET.Element) -> BackgroundEntry: + entry = BackgroundEntry( + id=elem.get("id"), + script=elem.text + ) + if elem.get("exit_code"): + entry.exit_code = int(elem.get("exit_code")) + stdout = elem.find("stdout") + if stdout is not None: + entry.stdout = stdout.text or "" + stderr = elem.find("stderr") + if stderr is not None: + entry.stderr = stderr.text or "" + return entry + + @staticmethod + def _parse_parse_error(elem: ET.Element) -> ParseErrorEntry: + error = elem.find("error") + content = elem.find("content") + return ParseErrorEntry( + id=elem.get("id"), + content=content.text if content is not None else "", + error=error.text if error is not None else "" + ) + + @staticmethod + def _parse_read(elem: ET.Element, io_buffer) -> ReadEntry: + entry = ReadEntry( + id=elem.get("id"), + io_buffer=io_buffer + ) + entry.content = elem.text or "" + entry.read = True + return entry + + @staticmethod + def _parse_reasoning(elem: ET.Element) -> ReasoningEntry: + return ReasoningEntry( + id=elem.get("id"), + content=elem.text or "" + ) + + @staticmethod + def _parse_repeat(elem: ET.Element) -> RepeatEntry: + entry = RepeatEntry( + id=elem.get("id"), + script=elem.text, + timeout=float(elem.get("timeout")) if elem.get("timeout") else None, + limit=int(elem.get("limit")) if elem.get("limit") else None + ) + if elem.get("exit_code"): + entry.exit_code = int(elem.get("exit_code")) + entry.timed_out = elem.get("timed_out") == "true" + stdout = elem.find("stdout") + if stdout is not None: + entry.stdout = stdout.text or "" + stderr = elem.find("stderr") + if stderr is not None: + entry.stderr = stderr.text or "" + return entry + + @staticmethod + def _parse_single(elem: ET.Element) -> SingleEntry: + entry = SingleEntry( + id=elem.get("id"), + script=elem.text, + timeout=float(elem.get("timeout")) if elem.get("timeout") else None, + limit=int(elem.get("limit")) if elem.get("limit") else None + ) + if elem.get("exit_code"): + entry.exit_code = int(elem.get("exit_code")) + entry.executed = True + entry.timed_out = elem.get("timed_out") == "true" + stdout = elem.find("stdout") + if stdout is not None: + entry.stdout = stdout.text or "" + stderr = elem.find("stderr") + if stderr is not None: + entry.stderr = stderr.text or "" + return entry + + @staticmethod + def _parse_write(elem: ET.Element, io_buffer) -> WriteEntry: + entry = WriteEntry( + id=elem.get("id"), + content=elem.text or "", + io_buffer=io_buffer + ) + entry.written = True + return entry \ No newline at end of file diff --git a/sia/web/api.py b/sia/web/api.py index 9ccc64f..7ea1033 100644 --- a/sia/web/api.py +++ b/sia/web/api.py @@ -2,11 +2,13 @@ from aiohttp import web import json import asyncio + from ..auto_approver import AutoApprover +from ..entry.entry_factory import EntryFactory +from ..iteration_parser import IterationParser from ..web_agent import WebAgent from ..web_io_buffer import WebIOBuffer from ..working_memory import WorkingMemory -from ..entry.entry_factory import EntryFactory class Api: def __init__( @@ -47,6 +49,7 @@ class Api: self._app.router.add_delete("/api/memory/entry/{id}", self._delete_entry) self._app.router.add_post("/api/memory/entry/{id}/reset", self._reset_entry) self._app.router.add_post("/api/memory/entry/{id}/update", self._update_entry) + self._app.router.add_post("/api/memory/load_iteration", self._load_iteration) async def _run_inference(self, request: web.Request) -> web.Response: """Start inference on specified LLM.""" @@ -253,4 +256,18 @@ class Api: entry.notify_change() return web.Response(status=200) except ValueError as e: - return web.Response(status=400, text=str(e)) \ No newline at end of file + return web.Response(status=400, text=str(e)) + + async def _load_iteration(self, request: web.Request) -> web.Response: + """Load entries from iteration XML content into working memory""" + data = await request.json() + content = data.get("content") + if not content: + return web.Response(status=400, text="Missing content in request body") + + entries = IterationParser.parse_iteration(content, self._io_buffer) + + for entry in entries: + self._working_memory.add_entry(entry) + + return web.Response(status=200) diff --git a/web/src/components/editors/MemoryEditor.jsx b/web/src/components/editors/MemoryEditor.jsx index 8eb4ec7..787d67e 100644 --- a/web/src/components/editors/MemoryEditor.jsx +++ b/web/src/components/editors/MemoryEditor.jsx @@ -43,6 +43,28 @@ export const MemoryEditor = ({ setShowCreateDialog(false); }; + const handleLoadIteration = async (event) => { + const file = event.target.files[0]; + if (file) { + const content = await file.text(); + const response = await fetch('/api/memory/load_iteration', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content: content }) + }); + if (!response.ok) throw new Error('Failed to load iteration'); + } + }; + + + const handleLoadIterationClick = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.xml'; + input.onchange = handleLoadIteration; + input.click(); + }; + const generateId = () => { const now = new Date(); return now.getFullYear() + @@ -160,10 +182,16 @@ export const MemoryEditor = ({ return (