diff --git a/old_architecture.md b/old_architecture.md deleted file mode 100644 index 506eacc..0000000 --- a/old_architecture.md +++ /dev/null @@ -1,44 +0,0 @@ -## Architecture - -An overview of the key components and their interactions. - -![SIA Component Model](./diagrams/SIA_Component_Model.svg) - -### Core Actions - -Core actions are implemented each in a separate class. -Each action in the context is an instance of the corresponding class. -The `delete` and `stop` actions are exceptions to this. -They are implemented as functions in the `AgentCore`. - -Actions have a `template(id)` method that returns an XML Element. -If they require updating in each iteration, they do so at the start of the `template` method. - -### Agent Core - -The `AgentCore` manages the state of the agent. - -It can also run a basic main loop consisting of: -- Running template on all actions -- Collecting system information -- Building the context -- Running the LLM -- Splitting the LLM output in reasoning and actions -- Instantiating the new actions and listing id's to delete -- Deleting the old actions - -### Web System - -The `WebSystem` uses an `AgentCore` but doesn't run the main loop. -It runs a modified main loop and interacts with the WebSystem. -It also instantiates alternative actions for stdio to interact with the WebSystem. - -### LLM Engine - -The `LLMEngine` does the LLM inference. -It takes a context as string and returns an iterator of tokens. - -### Inference Result - -An `InferenceResult` object contains the resoning and parsed actions. -Parsing is part of the Inference Result constructor. \ No newline at end of file diff --git a/readme.md b/readme.md index 334985a..1a5f807 100644 --- a/readme.md +++ b/readme.md @@ -288,14 +288,18 @@ classDiagram class BaseAgent { <> - #working_memory: WorkingMemory - #metrics: SystemMetrics - #llm: LLMEngine - #parser: ResponseParser - #validator: XMLValidator - #io_buffer: IOBuffer + -working_memory: WorkingMemory + -metrics: SystemMetrics + -llm: LLMEngine + -parser: ResponseParser + -validator: XMLValidator + -io_buffer: IOBuffer + -system_prompt: str + -action_schema: str - #compile_context() str + #_compile_context() str + #_process_llm_response(response str) Optional~Command~ + #_update() void } class WorkingMemory { @@ -386,14 +390,18 @@ stateDiagram-v2 classDiagram class BaseAgent { <> - #working_memory: WorkingMemory - #metrics: SystemMetrics - #llm: LLMEngine - #parser: ResponseParser - #validator: XMLValidator - #io_buffer: IOBuffer + -working_memory: WorkingMemory + -metrics: SystemMetrics + -llm: LLMEngine + -parser: ResponseParser + -validator: XMLValidator + -io_buffer: IOBuffer + -system_prompt: str + -action_schema: str - #compile_context() str + #_compile_context() str + #_process_llm_response(response str) Optional~Command~ + #_update() void } class StandardAgent { diff --git a/sia/base_agent.py b/sia/base_agent.py new file mode 100644 index 0000000..2c5ba2f --- /dev/null +++ b/sia/base_agent.py @@ -0,0 +1,89 @@ +from abc import ABC, abstractmethod +from typing import Iterator, Callable, Optional, List +import xml.etree.ElementTree as ET + +from .command import Command +from .llm_engine import LlmEngine +from .system_metrics import SystemMetrics +from .working_memory import WorkingMemory +from .xml_validator import XMLValidator +from .response_parser import ResponseParser +from .parse_error_entry import ParseErrorEntry +from .io_buffer import IOBuffer + +class BaseAgent(ABC): + """ + Abstract base class for SIA agents. + + Provides core functionality for maintaining working memory, system metrics, + and coordinating components for LLM inference. + + Private Attributes: + _working_memory: Collection of current entries + _metrics: System resource monitoring + _llm: LLM inference engine + _parser: XML response parser + _validator: XML response validator + _io_buffer: Input/output operations buffer + _system_prompt: System prompt template + _action_schema: XML schema for action validation + """ + + def __init__(self, + model_path: str, + system_prompt: str, + action_schema: str, + io_buffer: IOBuffer): + """ + Initialize agent with required components. + + Args: + model_path: Path to LLM model + system_prompt: System prompt template + action_schema: XML schema for actions + io_buffer: IO buffer implementation to use + """ + # Initialize components + self._working_memory = WorkingMemory() + self._metrics = SystemMetrics() + self._llm = LlmEngine(model_path) + self._validator = XMLValidator(action_schema) + self._io_buffer = io_buffer + self._parser = ResponseParser(io_buffer) + + # Store prompts + self._system_prompt = system_prompt + self._action_schema = action_schema + + def __del__(self): + """Clean up resources on deletion.""" + if hasattr(self, '_metrics'): + self._metrics.stop() + + def _compile_context(self) -> str: + """ + Compile the current context for LLM inference. + Includes system metrics and working memory entries. + + Returns: + str: Complete context as XML string + """ + # Get usage details to include in context + context_size = 0 # TODO: Implement context size tracking + + # Get current system metrics + metrics_context = self._metrics.generate_context(context_size) + + # Get working memory entries + memory_context = self._working_memory.generate_context() + + # Create root element + root = ET.Element("state") + + # Add metrics and memory entries + root.append(metrics_context) + for entry in memory_context: + root.append(entry) + + # Convert to string with basic formatting + return ET.tostring(root, encoding="unicode") \ No newline at end of file diff --git a/sia/io_buffer.py b/sia/io_buffer.py new file mode 100644 index 0000000..9f1f654 --- /dev/null +++ b/sia/io_buffer.py @@ -0,0 +1,41 @@ +from abc import ABC, abstractmethod + +class IOBuffer(ABC): + """ + Abstract base class defining the interface for input/output operations. + + This interface allows for different implementations of IO handling, + such as direct system IO or buffered web interface communication. + """ + + @abstractmethod + def read(self) -> str: + """ + Read and return available input. + + Should clear the input buffer after reading. + + Returns: + str: Content from input buffer, or empty string if no input available + """ + pass + + @abstractmethod + def write(self, content: str) -> None: + """ + Write content to output. + + Args: + content: String content to write + """ + pass + + @abstractmethod + def buffer_length(self) -> int: + """ + Get the current length of buffered input. + + Returns: + int: Number of characters in the input buffer + """ + pass \ No newline at end of file diff --git a/sia/standard_io_buffer.py b/sia/standard_io_buffer.py index 5b93944..4400035 100644 --- a/sia/standard_io_buffer.py +++ b/sia/standard_io_buffer.py @@ -1,6 +1,8 @@ import sys -class StandardIOBuffer: +from .io_buffer import IOBuffer + +class StandardIOBuffer(IOBuffer): """ IOBuffer implementation that uses system standard input/output. diff --git a/sia/web_io_buffer.py b/sia/web_io_buffer.py index bcb60d1..420b749 100644 --- a/sia/web_io_buffer.py +++ b/sia/web_io_buffer.py @@ -1,4 +1,6 @@ -class WebIOBuffer: +from .io_buffer import IOBuffer + +class WebIOBuffer(IOBuffer): """ IOBuffer implementation for web interface communication. diff --git a/test/base_agent_test.py b/test/base_agent_test.py new file mode 100644 index 0000000..2ff4731 --- /dev/null +++ b/test/base_agent_test.py @@ -0,0 +1,162 @@ +import unittest +from datetime import datetime +from unittest.mock import Mock, MagicMock, patch +import xml.etree.ElementTree as ET + +from sia.web_io_buffer import WebIOBuffer +from sia.working_memory import WorkingMemory +from sia.system_metrics import SystemMetrics +from sia.llm_engine import LlmEngine +from sia.xml_validator import XMLValidator +from sia.response_parser import ResponseParser +from sia.base_agent import BaseAgent +from sia.command import Command +from sia.command_result import CommandResult +from sia.reasoning_entry import ReasoningEntry +from sia.parse_error_entry import ParseErrorEntry +from sia.delete_command import DeleteCommand +from sia.entry import Entry + +class TestBaseAgent(BaseAgent): + """Concrete implementation of BaseAgent for testing.""" + def run(self) -> None: + pass + +class MockEntry(Entry): + """Mock entry class that properly extends Entry.""" + def __init__(self, id: str, timestamp: datetime, update_behavior=None): + super().__init__(id, timestamp) + self._update_behavior = update_behavior + self.update_called = False + + def update(self) -> None: + self.update_called = True + if self._update_behavior: + self._update_behavior() + + def generate_context(self) -> ET.Element: + elem = ET.Element("mock_entry", {"id": self.id}) + elem.text = "" + return elem + +class BaseAgentTest(unittest.TestCase): + def setUp(self): + """Set up test cases with mocked components.""" + # Create mocks + self.mock_llm = Mock(spec=LlmEngine) + self.mock_metrics = Mock(spec=SystemMetrics) + self.mock_validator = Mock(spec=XMLValidator) + self.io_buffer = WebIOBuffer() + + # Mock metrics context + metrics_elem = ET.Element("context") + metrics_elem.set("time", "2024-10-31T12:00:00Z") + metrics_elem.set("cpu", "10") + metrics_elem.set("gpu", "20") + metrics_elem.set("memory_used", "1000") + metrics_elem.set("memory_total", "2000") + metrics_elem.set("disk_used", "5000") + metrics_elem.set("disk_total", "10000") + metrics_elem.set("context", "50") + metrics_elem.set("stdin", "0") + self.mock_metrics.generate_context.return_value = metrics_elem + + # Create test agent with mocked components + with patch('sia.base_agent.LlmEngine') as mock_llm_class, \ + patch('sia.base_agent.SystemMetrics') as mock_metrics_class, \ + patch('sia.base_agent.XMLValidator') as mock_validator_class: + + mock_llm_class.return_value = self.mock_llm + mock_metrics_class.return_value = self.mock_metrics + mock_validator_class.return_value = self.mock_validator + + self.agent = TestBaseAgent( + model_path="/test/model", + system_prompt="test prompt", + action_schema="test schema", + io_buffer=self.io_buffer + ) + + # Set timestamp for entries + self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0) + + def tearDown(self): + """Clean up resources.""" + if hasattr(self, 'agent'): + del self.agent + + def test_initialization(self): + """Test agent initialization and component setup.""" + self.assertIsInstance(self.agent._working_memory, WorkingMemory) + self.assertEqual(self.agent._metrics, self.mock_metrics) + self.assertEqual(self.agent._llm, self.mock_llm) + self.assertEqual(self.agent._validator, self.mock_validator) + self.assertEqual(self.agent._io_buffer, self.io_buffer) + self.assertIsInstance(self.agent._parser, ResponseParser) + self.assertEqual(self.agent._system_prompt, "test prompt") + self.assertEqual(self.agent._action_schema, "test schema") + + def test_cleanup(self): + """Test cleanup on agent deletion.""" + del self.agent + self.mock_metrics.stop.assert_called_once() + + def test_compile_context_empty_memory(self): + """Test context compilation with empty working memory.""" + context = self.agent._compile_context() + + # Parse context and verify structure + root = ET.fromstring(context) + self.assertEqual(root.tag, "state") + + # Check metrics element + context_elem = root.find("context") + self.assertIsNotNone(context_elem) + self.assertEqual(context_elem.get("cpu"), "10") + self.assertEqual(context_elem.get("gpu"), "20") + + # Check no memory entries + self.assertEqual(len(root.findall("*")), 1) # Only metrics element + + def test_compile_context_with_entries(self): + """Test context compilation with working memory entries.""" + # Add test entry to working memory + entry = ReasoningEntry("test reasoning", "test-id", self.test_timestamp) + self.agent._working_memory.add_entry(entry) + + context = self.agent._compile_context() + + # Parse and verify context + root = ET.fromstring(context) + + # Check metrics and entry are present + self.assertEqual(len(root.findall("*")), 2) # Metrics + reasoning + + # Verify entry content + reasoning_elem = root.find("reasoning") + self.assertIsNotNone(reasoning_elem) + self.assertEqual(reasoning_elem.get("id"), "test-id") + self.assertEqual(reasoning_elem.text, "") + + def test_multiple_entries(self): + """Test handling multiple entries in working memory.""" + # Add multiple entries + entries = [ + ReasoningEntry(f"test {i}", f"id-{i}", self.test_timestamp) + for i in range(3) + ] + + for entry in entries: + self.agent._working_memory.add_entry(entry) + + context = self.agent._compile_context() + root = ET.fromstring(context) + + # Verify all entries present + self.assertEqual(len(root.findall("reasoning")), 3) + + # Verify entry order maintained + reasoning_elems = root.findall("reasoning") + for i, elem in enumerate(reasoning_elems): + self.assertEqual(elem.get("id"), f"id-{i}") + self.assertEqual(elem.text, f"") \ No newline at end of file diff --git a/test/parse_error_entry_test.py b/test/parse_error_entry_test.py index cb7ea58..c49643c 100644 --- a/test/parse_error_entry_test.py +++ b/test/parse_error_entry_test.py @@ -25,13 +25,13 @@ class ParseErrorEntryTest(unittest.TestCase): entry = ParseErrorEntry(content, error, self.test_id, self.test_timestamp) # Store initial state - initial_context = str(entry.generate_context()) + initial_context = entry.content # Perform update entry.update() # Verify state hasn't changed - self.assertEqual(str(entry.generate_context()), initial_context) + self.assertEqual(entry.content, initial_context) def test_generate_context(self): """Test XML context generation"""