Added web agent

This commit is contained in:
2024-11-01 18:25:37 +01:00
parent f48b87f037
commit aadb841eb8
6 changed files with 448 additions and 103 deletions

View File

@@ -2,6 +2,7 @@ import unittest
from datetime import datetime
from unittest.mock import Mock, MagicMock, patch
import xml.etree.ElementTree as ET
import logging
from sia.web_io_buffer import WebIOBuffer
from sia.working_memory import WorkingMemory
@@ -47,38 +48,48 @@ class BaseAgentTest(unittest.TestCase):
self.mock_metrics = Mock(spec=SystemMetrics)
self.mock_validator = Mock(spec=XMLValidator)
self.io_buffer = WebIOBuffer()
self.working_memory = WorkingMemory()
# 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 parser with IO buffer
self.parser = ResponseParser(self.io_buffer)
# 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 metrics context generator
def generate_context(context_size):
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", str(int(context_size * 100)))
metrics_elem.set("stdin", "0")
return metrics_elem
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
)
self.mock_metrics.generate_context.side_effect = generate_context
# Create test agent with required components
self.agent = TestBaseAgent(
action_schema="test schema",
working_memory=self.working_memory,
system_metrics=self.mock_metrics,
llm=self.mock_llm,
validator=self.mock_validator,
parser=self.parser
)
# Set timestamp for entries
self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0)
# Capture logging output
self.log_records = []
logging.getLogger().handlers = []
logging.getLogger().addHandler(self.log_handler)
def log_handler(self, record):
"""Capture log records for testing."""
self.log_records.append(record)
def tearDown(self):
"""Clean up resources."""
@@ -87,13 +98,11 @@ class BaseAgentTest(unittest.TestCase):
def test_initialization(self):
"""Test agent initialization and component setup."""
self.assertIsInstance(self.agent._working_memory, WorkingMemory)
self.assertEqual(self.agent._working_memory, self.working_memory)
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._parser, self.parser)
self.assertEqual(self.agent._action_schema, "test schema")
def test_cleanup(self):
@@ -107,16 +116,17 @@ class BaseAgentTest(unittest.TestCase):
# Parse context and verify structure
root = ET.fromstring(context)
self.assertEqual(root.tag, "state")
self.assertEqual(root.tag, "context")
# 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 metrics
self.assertEqual(root.get("cpu"), "10")
self.assertEqual(root.get("gpu"), "20")
# Check context size is 0 (empty memory)
self.assertEqual(root.get("context"), "0")
# Check no memory entries
self.assertEqual(len(root.findall("*")), 1) # Only metrics element
self.assertEqual(len(list(root)), 0)
def test_compile_context_with_entries(self):
"""Test context compilation with working memory entries."""
@@ -129,8 +139,8 @@ class BaseAgentTest(unittest.TestCase):
# Parse and verify context
root = ET.fromstring(context)
# Check metrics and entry are present
self.assertEqual(len(root.findall("*")), 2) # Metrics + reasoning
# Check context size reflects one entry
self.assertEqual(root.get("context"), "1")
# Verify entry content
reasoning_elem = root.find("reasoning")
@@ -152,11 +162,12 @@ class BaseAgentTest(unittest.TestCase):
context = self.agent._compile_context()
root = ET.fromstring(context)
# Verify all entries present
self.assertEqual(len(root.findall("reasoning")), 3)
# Check context size reflects three entries
self.assertEqual(root.get("context"), "3")
# Verify entry order maintained
reasoning_elems = root.findall("reasoning")
self.assertEqual(len(reasoning_elems), 3)
for i, elem in enumerate(reasoning_elems):
self.assertEqual(elem.get("id"), f"id-{i}")
self.assertEqual(elem.text, f"<![CDATA[test {i}]]>")

233
test/web_agent_test.py Normal file
View File

@@ -0,0 +1,233 @@
import unittest
from datetime import datetime
from unittest.mock import Mock, MagicMock, patch, call
import xml.etree.ElementTree as ET
from typing import Iterator
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.web_agent import WebAgent, WebAgentState
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.entry import Entry
class MockCommand(Command):
"""Mock command for testing execution flow."""
def __init__(self, result: CommandResult):
self.result = result
self.executed = False
def execute(self, memory: WorkingMemory) -> CommandResult:
self.executed = True
return self.result
class WebAgentTest(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()
self.working_memory = WorkingMemory()
# Mock validator to pass by default
self.mock_validator.validate.return_value = None
# Create parser with IO buffer
self.parser = ResponseParser(self.io_buffer)
# Mock metrics context generator
def generate_context(context_size):
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", str(int(context_size * 100)))
metrics_elem.set("stdin", "0")
return metrics_elem
self.mock_metrics.generate_context.side_effect = generate_context
# Create agent with all components
self.agent = WebAgent(
system_prompt="test prompt",
action_schema="test schema",
working_memory=self.working_memory,
system_metrics=self.mock_metrics,
llm=self.mock_llm,
validator=self.mock_validator,
parser=self.parser
)
# Set timestamp for entries
self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0)
# Handler tracking
self.state_changes = []
self.response_changes = []
def state_change_handler(self, state: WebAgentState):
"""Track state changes for verification."""
self.state_changes.append(state)
def response_change_handler(self, response: str):
"""Track response changes for verification."""
self.response_changes.append(response)
def test_initialization(self):
"""Test initial state of web agent."""
self.assertEqual(self.agent.current_state, WebAgentState.UPDATE)
self.assertEqual(self.agent.context, "")
self.assertEqual(self.agent.response, "")
self.assertIsNone(self.agent._validation_error)
self.assertEqual(len(self.agent._state_change_handlers), 0)
self.assertEqual(len(self.agent._response_change_handlers), 0)
def test_handler_registration(self):
"""Test adding state and response change handlers."""
# Add handlers
self.agent.add_state_change_handler(self.state_change_handler)
self.agent.add_response_change_handler(self.response_change_handler)
# Verify handlers added
self.assertEqual(len(self.agent._state_change_handlers), 1)
self.assertEqual(len(self.agent._response_change_handlers), 1)
# Adding same handler twice should not duplicate
self.agent.add_state_change_handler(self.state_change_handler)
self.agent.add_response_change_handler(self.response_change_handler)
self.assertEqual(len(self.agent._state_change_handlers), 1)
self.assertEqual(len(self.agent._response_change_handlers), 1)
def test_approve_context_state_error(self):
"""Test approving context in wrong state raises error."""
with self.assertRaises(Exception) as context:
self.agent.approve_context()
self.assertIn("Not in CONTEXT_APPROVAL state", str(context.exception))
def test_approve_response_state_error(self):
"""Test approving response in wrong state raises error."""
with self.assertRaises(Exception) as context:
self.agent.approve_response()
self.assertIn("Not in RESPONSE_APPROVAL state", str(context.exception))
def test_context_approval_flow(self):
"""Test complete context approval flow with state transitions."""
# Register handlers
self.agent.add_state_change_handler(self.state_change_handler)
self.agent.add_response_change_handler(self.response_change_handler)
# Setup LLM response
def mock_infer(prompt: str, context: str) -> Iterator[str]:
yield "<reasoning>test reasoning</reasoning>"
self.mock_llm.infer.side_effect = mock_infer
# Set initial state
self.agent._current_state = WebAgentState.CONTEXT_APPROVAL
# Approve context
self.agent.approve_context()
# Verify state transitions
expected_states = [
WebAgentState.INFERENCE,
WebAgentState.RESPONSE_APPROVAL
]
self.assertEqual(self.state_changes, expected_states)
# Verify response updates
self.assertEqual(len(self.response_changes), 2) # Empty + final response
self.assertEqual(self.response_changes[0], "")
self.assertEqual(self.response_changes[1], "<reasoning>test reasoning</reasoning>")
def test_response_approval_flow_command(self):
"""Test response approval flow with command execution."""
# Register handlers
self.agent.add_state_change_handler(self.state_change_handler)
# Setup command result
command_result = CommandResult.success()
mock_command = MockCommand(command_result)
# Mock parser to return command
self.parser.parse = Mock(return_value=mock_command)
# Set initial state and response
self.agent._current_state = WebAgentState.RESPONSE_APPROVAL
self.agent._response = "<stop/>"
# Approve response
self.agent.approve_response()
# Verify command executed
self.assertTrue(mock_command.executed)
# Verify state transitions
expected_states = [
WebAgentState.UPDATE,
WebAgentState.CONTEXT_APPROVAL
]
self.assertEqual(self.state_changes, expected_states)
# Verify command result stored
self.assertEqual(self.agent.command_result, command_result)
def test_response_approval_flow_entry(self):
"""Test response approval flow with entry creation."""
# Register handlers
self.agent.add_state_change_handler(self.state_change_handler)
# Set initial state and response
self.agent._current_state = WebAgentState.RESPONSE_APPROVAL
self.agent._response = "<reasoning>test reasoning</reasoning>"
# Approve response
self.agent.approve_response()
# Verify entry added to working memory
self.assertEqual(self.working_memory.get_entries_count(), 1)
entry = self.working_memory.get_entries()[0]
self.assertIsInstance(entry, ReasoningEntry)
self.assertEqual(entry.content, "test reasoning")
# Verify state transitions
expected_states = [
WebAgentState.UPDATE,
WebAgentState.CONTEXT_APPROVAL
]
self.assertEqual(self.state_changes, expected_states)
def test_response_validation(self):
"""Test response validation during response setting."""
# Register handlers
self.agent.add_response_change_handler(self.response_change_handler)
# Mock validator to fail
error_message = "Invalid XML"
self.mock_validator.validate.return_value = error_message
# Set response
self.agent._set_response("<invalid>")
# Verify validation error stored
self.assertEqual(self.agent._validation_error, error_message)
# Verify response updated
self.assertEqual(self.response_changes, ["<invalid>"])
def tearDown(self):
"""Clean up resources."""
if hasattr(self, 'agent'):
del self.agent