Added base agent

This commit is contained in:
2024-11-01 16:27:56 +01:00
parent a95c9676b4
commit f48b87f037
8 changed files with 322 additions and 62 deletions

View File

@@ -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.

View File

@@ -288,14 +288,18 @@ classDiagram
class BaseAgent {
<<abstract>>
#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 {
<<abstract>>
#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 {

89
sia/base_agent.py Normal file
View File

@@ -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")

41
sia/io_buffer.py Normal file
View File

@@ -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

View File

@@ -1,6 +1,8 @@
import sys
class StandardIOBuffer:
from .io_buffer import IOBuffer
class StandardIOBuffer(IOBuffer):
"""
IOBuffer implementation that uses system standard input/output.

View File

@@ -1,4 +1,6 @@
class WebIOBuffer:
from .io_buffer import IOBuffer
class WebIOBuffer(IOBuffer):
"""
IOBuffer implementation for web interface communication.

162
test/base_agent_test.py Normal file
View File

@@ -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 = "<![CDATA[mock content]]>"
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, "<![CDATA[test reasoning]]>")
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"<![CDATA[test {i}]]>")

View File

@@ -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"""