Added base agent
This commit is contained in:
@@ -1,44 +0,0 @@
|
|||||||
## Architecture
|
|
||||||
|
|
||||||
An overview of the key components and their interactions.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 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.
|
|
||||||
36
readme.md
36
readme.md
@@ -288,14 +288,18 @@ classDiagram
|
|||||||
|
|
||||||
class BaseAgent {
|
class BaseAgent {
|
||||||
<<abstract>>
|
<<abstract>>
|
||||||
#working_memory: WorkingMemory
|
-working_memory: WorkingMemory
|
||||||
#metrics: SystemMetrics
|
-metrics: SystemMetrics
|
||||||
#llm: LLMEngine
|
-llm: LLMEngine
|
||||||
#parser: ResponseParser
|
-parser: ResponseParser
|
||||||
#validator: XMLValidator
|
-validator: XMLValidator
|
||||||
#io_buffer: IOBuffer
|
-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 {
|
class WorkingMemory {
|
||||||
@@ -386,14 +390,18 @@ stateDiagram-v2
|
|||||||
classDiagram
|
classDiagram
|
||||||
class BaseAgent {
|
class BaseAgent {
|
||||||
<<abstract>>
|
<<abstract>>
|
||||||
#working_memory: WorkingMemory
|
-working_memory: WorkingMemory
|
||||||
#metrics: SystemMetrics
|
-metrics: SystemMetrics
|
||||||
#llm: LLMEngine
|
-llm: LLMEngine
|
||||||
#parser: ResponseParser
|
-parser: ResponseParser
|
||||||
#validator: XMLValidator
|
-validator: XMLValidator
|
||||||
#io_buffer: IOBuffer
|
-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 {
|
class StandardAgent {
|
||||||
|
|||||||
89
sia/base_agent.py
Normal file
89
sia/base_agent.py
Normal 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
41
sia/io_buffer.py
Normal 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
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
class StandardIOBuffer:
|
from .io_buffer import IOBuffer
|
||||||
|
|
||||||
|
class StandardIOBuffer(IOBuffer):
|
||||||
"""
|
"""
|
||||||
IOBuffer implementation that uses system standard input/output.
|
IOBuffer implementation that uses system standard input/output.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
class WebIOBuffer:
|
from .io_buffer import IOBuffer
|
||||||
|
|
||||||
|
class WebIOBuffer(IOBuffer):
|
||||||
"""
|
"""
|
||||||
IOBuffer implementation for web interface communication.
|
IOBuffer implementation for web interface communication.
|
||||||
|
|
||||||
|
|||||||
162
test/base_agent_test.py
Normal file
162
test/base_agent_test.py
Normal 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}]]>")
|
||||||
@@ -25,13 +25,13 @@ class ParseErrorEntryTest(unittest.TestCase):
|
|||||||
entry = ParseErrorEntry(content, error, self.test_id, self.test_timestamp)
|
entry = ParseErrorEntry(content, error, self.test_id, self.test_timestamp)
|
||||||
|
|
||||||
# Store initial state
|
# Store initial state
|
||||||
initial_context = str(entry.generate_context())
|
initial_context = entry.content
|
||||||
|
|
||||||
# Perform update
|
# Perform update
|
||||||
entry.update()
|
entry.update()
|
||||||
|
|
||||||
# Verify state hasn't changed
|
# Verify state hasn't changed
|
||||||
self.assertEqual(str(entry.generate_context()), initial_context)
|
self.assertEqual(entry.content, initial_context)
|
||||||
|
|
||||||
def test_generate_context(self):
|
def test_generate_context(self):
|
||||||
"""Test XML context generation"""
|
"""Test XML context generation"""
|
||||||
|
|||||||
Reference in New Issue
Block a user