New web interface, move llm engine to separate process
This commit is contained in:
@@ -4,14 +4,14 @@ from threading import Event
|
||||
import time
|
||||
|
||||
from sia.auto_approver import AutoApprover
|
||||
from sia.web_agent import WebAgent, LlmState
|
||||
from sia.web_agent import WebAgent, AgentState
|
||||
|
||||
class AutoApproverTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""Create mock agent and approver for each test"""
|
||||
self.mock_agent = Mock(spec=WebAgent)
|
||||
# Set up llms attribute as a dictionary
|
||||
self.mock_agent.llms = {"default": LlmState.IDLE}
|
||||
self.mock_agent.llms = {"default": AgentState.IDLE}
|
||||
self.approver = AutoApprover(self.mock_agent)
|
||||
|
||||
def test_timeout_setters(self):
|
||||
@@ -30,7 +30,7 @@ class AutoApproverTest(unittest.TestCase):
|
||||
def test_enable_starts_thread_in_correct_state(self):
|
||||
"""Test enabling only starts thread in matching state"""
|
||||
# Mock the agent's llm state
|
||||
self.mock_agent.llms["default"] = LlmState.IDLE
|
||||
self.mock_agent.llms["default"] = AgentState.IDLE
|
||||
self.approver.context_enabled = True
|
||||
time.sleep(0.1) # Allow thread to start
|
||||
self.assertIsNotNone(self.approver._context_thread)
|
||||
@@ -40,7 +40,7 @@ class AutoApproverTest(unittest.TestCase):
|
||||
def test_state_change_stops_threads(self):
|
||||
"""Test state changes stop irrelevant threads"""
|
||||
# Start in idle state with context thread
|
||||
self.mock_agent.llms["default"] = LlmState.IDLE
|
||||
self.mock_agent.llms["default"] = AgentState.IDLE
|
||||
|
||||
# Mock _stop_context_thread to verify it's called
|
||||
with patch.object(self.approver, '_stop_context_thread') as mock_stop:
|
||||
@@ -48,31 +48,31 @@ class AutoApproverTest(unittest.TestCase):
|
||||
time.sleep(0.1)
|
||||
|
||||
# Change state and verify stop method was called
|
||||
self.mock_agent.llms["default"] = LlmState.INFERENCE
|
||||
self.approver._handle_llm_state_change("default", LlmState.INFERENCE)
|
||||
self.mock_agent.llms["default"] = AgentState.INFERENCE
|
||||
self.approver._handle_state_change(AgentState.INFERENCE)
|
||||
mock_stop.assert_called_once()
|
||||
|
||||
def test_quick_state_cycle(self):
|
||||
"""Test thread stops when state changes before timeout"""
|
||||
self.approver.context_timeout = 5.0
|
||||
self.mock_agent.llms["default"] = LlmState.IDLE
|
||||
self.mock_agent.llms["default"] = AgentState.IDLE
|
||||
self.approver.context_enabled = True
|
||||
|
||||
# Change state quickly
|
||||
time.sleep(0.1)
|
||||
self.mock_agent.llms["default"] = LlmState.INFERENCE
|
||||
self.approver._handle_llm_state_change("default", LlmState.INFERENCE)
|
||||
self.mock_agent.llms["default"] = AgentState.INFERENCE
|
||||
self.approver._handle_state_change(AgentState.INFERENCE)
|
||||
|
||||
# Verify no approval happened
|
||||
self.mock_agent.run_inference.assert_not_called()
|
||||
|
||||
def test_disable_stops_thread(self):
|
||||
"""Test disabling stops active thread"""
|
||||
self.mock_agent.llms["default"] = LlmState.IDLE
|
||||
self.mock_agent.llms["default"] = AgentState.IDLE
|
||||
|
||||
# Create a new instance with a fresh mock for this test
|
||||
mock_agent = Mock(spec=WebAgent)
|
||||
mock_agent.llms = {"default": LlmState.IDLE}
|
||||
mock_agent.llms = {"default": AgentState.IDLE}
|
||||
test_approver = AutoApprover(mock_agent)
|
||||
|
||||
# Start with a mocked stop method
|
||||
@@ -91,7 +91,7 @@ class AutoApproverTest(unittest.TestCase):
|
||||
def test_successful_auto_approval(self):
|
||||
"""Test thread approves after timeout when conditions met"""
|
||||
self.approver.context_timeout = 0.1
|
||||
self.mock_agent.llms["default"] = LlmState.IDLE
|
||||
self.mock_agent.llms["default"] = AgentState.IDLE
|
||||
self.approver.llm_name = "default"
|
||||
|
||||
# Use threading Event to properly synchronize the test
|
||||
|
||||
@@ -8,7 +8,6 @@ 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
|
||||
@@ -57,7 +56,6 @@ class BaseAgentTest(unittest.TestCase):
|
||||
self.mock_llm = Mock(spec=LlmEngine)
|
||||
self.mock_llm.token_count.return_value = 100
|
||||
self.mock_llm.token_limit.return_value = 1000
|
||||
self.mock_validator = Mock(spec=XMLValidator)
|
||||
self.io_buffer = WebIOBuffer()
|
||||
self.working_memory = WorkingMemory()
|
||||
self.work_dir = Path("/tmp") # Use a temp directory for tests
|
||||
@@ -79,7 +77,6 @@ class BaseAgentTest(unittest.TestCase):
|
||||
action_schema="test schema",
|
||||
working_memory=self.working_memory,
|
||||
metrics=self.mock_metrics,
|
||||
validator=self.mock_validator,
|
||||
parser=self.parser
|
||||
)
|
||||
|
||||
@@ -100,7 +97,6 @@ class BaseAgentTest(unittest.TestCase):
|
||||
"""Test agent initialization and component setup."""
|
||||
self.assertEqual(self.agent._working_memory, self.working_memory)
|
||||
self.assertEqual(self.agent._metrics, self.mock_metrics)
|
||||
self.assertEqual(self.agent._validator, self.mock_validator)
|
||||
self.assertEqual(self.agent._parser, self.parser)
|
||||
self.assertEqual(self.agent._action_schema, "test schema")
|
||||
|
||||
@@ -114,20 +110,19 @@ class BaseAgentTest(unittest.TestCase):
|
||||
context = self.agent._compile_context(self.mock_llm)
|
||||
|
||||
# Parse context and verify structure
|
||||
root = ET.fromstring(context)
|
||||
self.assertEqual(root.tag, "context")
|
||||
self.assertEqual(context.tag, "context")
|
||||
|
||||
# Check metrics
|
||||
self.assertEqual(root.get("memory_used"), "1000")
|
||||
self.assertEqual(root.get("memory_total"), "2000")
|
||||
self.assertEqual(root.get("disk_used"), "5000")
|
||||
self.assertEqual(root.get("disk_total"), "10000")
|
||||
self.assertEqual(context.get("memory_used"), "1000")
|
||||
self.assertEqual(context.get("memory_total"), "2000")
|
||||
self.assertEqual(context.get("disk_used"), "5000")
|
||||
self.assertEqual(context.get("disk_total"), "10000")
|
||||
|
||||
# Check context size
|
||||
self.assertEqual(root.get("context"), "10.0%")
|
||||
self.assertEqual(context.get("context"), "10.0%")
|
||||
|
||||
# Check no memory entries
|
||||
self.assertEqual(len(list(root)), 0)
|
||||
self.assertEqual(len(list(context)), 0)
|
||||
|
||||
def test_compile_context_with_entries(self):
|
||||
"""Test context compilation with a single entry."""
|
||||
@@ -135,13 +130,12 @@ class BaseAgentTest(unittest.TestCase):
|
||||
self.agent._working_memory.add_entry(entry)
|
||||
|
||||
context = self.agent._compile_context(self.mock_llm)
|
||||
root = ET.fromstring(context)
|
||||
|
||||
# Check context size reflects one entry
|
||||
self.assertEqual(root.get("context"), "10.0%")
|
||||
self.assertEqual(context.get("context"), "10.0%")
|
||||
|
||||
# Verify entry content
|
||||
reasoning_elem = root.find("reasoning")
|
||||
reasoning_elem = context.find("reasoning")
|
||||
self.assertIsNotNone(reasoning_elem)
|
||||
self.assertEqual(reasoning_elem.get("id"), "test-id")
|
||||
self.assertEqual(reasoning_elem.text.strip(), "test reasoning")
|
||||
@@ -157,13 +151,12 @@ class BaseAgentTest(unittest.TestCase):
|
||||
self.agent._working_memory.add_entry(entry)
|
||||
|
||||
context = self.agent._compile_context(self.mock_llm)
|
||||
root = ET.fromstring(context)
|
||||
|
||||
# Check context size reflects three entries
|
||||
self.assertEqual(root.get("context"), "10.0%")
|
||||
self.assertEqual(context.get("context"), "10.0%")
|
||||
|
||||
# Verify entry order maintained
|
||||
reasoning_elems = root.findall("reasoning")
|
||||
reasoning_elems = context.findall("reasoning")
|
||||
self.assertEqual(len(reasoning_elems), 3)
|
||||
for i, elem in enumerate(reasoning_elems):
|
||||
self.assertEqual(elem.get("id"), f"id-{i}")
|
||||
|
||||
237
test/llm_engine_test.py
Normal file
237
test/llm_engine_test.py
Normal file
@@ -0,0 +1,237 @@
|
||||
import unittest
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from sia.llm_engine import LlmEngine
|
||||
|
||||
class TestLlmEngine(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""Set up test environment before each test."""
|
||||
# Create a temporary bash script to simulate the LLM engine
|
||||
self.temp_script = self._create_mock_llm_script()
|
||||
os.chmod(self.temp_script, 0o755) # Make it executable
|
||||
|
||||
# Initialize the LlmEngine with our mock script
|
||||
self.engine = LlmEngine(self.temp_script, "/root/sia/action_schema.xsd")
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after the test."""
|
||||
if hasattr(self, 'engine') and self.engine:
|
||||
self.engine._terminate_process()
|
||||
|
||||
def _create_mock_llm_script(self):
|
||||
"""Create a bash script that simulates an LLM engine."""
|
||||
fd, path = tempfile.mkstemp(suffix='.sh')
|
||||
with os.fdopen(fd, 'w') as f:
|
||||
# Paste the content from the mock_llm_script_debug artifact
|
||||
# This is a placeholder - you'll replace this with the actual script content
|
||||
f.write('''#!/bin/bash
|
||||
|
||||
# Mock LLM engine for testing
|
||||
# This script simulates an LLM engine subprocess that responds to the three commands:
|
||||
# <token_limit/>, <token_count>...</token_count>, and <infer_xml>...</infer_xml>
|
||||
|
||||
# Function to read XML input until a complete closing tag is found
|
||||
read_xml_input() {
|
||||
local input=""
|
||||
local line
|
||||
local char_count=0
|
||||
|
||||
# Read until we get a complete XML command
|
||||
while IFS= read -r line; do
|
||||
input="${input}${line}"
|
||||
char_count=$((char_count + ${#line}))
|
||||
|
||||
# Debug the actual content (first 30 chars)
|
||||
|
||||
if [[ "$input" == *"<token_limit/>"* ]]; then
|
||||
printf "1024"
|
||||
printf "\004" # EOT character (hex 04)
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$input" == *"<token_count>"*"</token_count>"* ]]; then
|
||||
printf "405"
|
||||
printf "\004" # EOT character (hex 04)
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$input" == *"<infer_xml>"*"</infer_xml>"* ]]; then
|
||||
generate_response
|
||||
return
|
||||
fi
|
||||
|
||||
done
|
||||
}
|
||||
|
||||
# Function to generate a response token by token
|
||||
generate_response() {
|
||||
printf "<"
|
||||
sleep 0.01
|
||||
|
||||
printf "reason"
|
||||
sleep 0.01
|
||||
|
||||
printf "ing"
|
||||
sleep 0.01
|
||||
|
||||
printf ">"
|
||||
sleep 0.01
|
||||
|
||||
printf "This"
|
||||
sleep 0.01
|
||||
|
||||
printf " is"
|
||||
sleep 0.01
|
||||
|
||||
printf " a"
|
||||
sleep 0.01
|
||||
|
||||
printf " test"
|
||||
sleep 0.01
|
||||
|
||||
printf " response."
|
||||
sleep 0.01
|
||||
|
||||
printf "</"
|
||||
sleep 0.01
|
||||
|
||||
printf "reason"
|
||||
sleep 0.01
|
||||
|
||||
printf "ing"
|
||||
sleep 0.01
|
||||
|
||||
printf ">"
|
||||
sleep 0.01
|
||||
|
||||
printf "\004"
|
||||
}
|
||||
|
||||
# Main loop - keep reading input and responding
|
||||
iteration=0
|
||||
while true; do
|
||||
iteration=$((iteration + 1))
|
||||
read_xml_input
|
||||
done''')
|
||||
return path
|
||||
|
||||
def test_token_limit(self):
|
||||
"""Test retrieving the token limit from the LLM engine."""
|
||||
limit = self.engine.token_limit()
|
||||
self.assertEqual(limit, 1024, "Token limit should be 1024")
|
||||
|
||||
def test_token_count(self):
|
||||
"""Test counting tokens in a prompt."""
|
||||
system_prompt = "You are a helpful AI assistant."
|
||||
|
||||
# Create a simple context ElementTree
|
||||
context_et = ET.Element("context")
|
||||
context_et.set("time", "2024-10-18T12:00:00Z")
|
||||
context_et.text = "Test context"
|
||||
|
||||
prefix = ""
|
||||
|
||||
count = self.engine.token_count(system_prompt, context_et, prefix)
|
||||
self.assertEqual(count, 405, "Token count should be 405")
|
||||
|
||||
def test_inference_token_by_token(self):
|
||||
"""Test inference with the LLM engine, ensuring tokens come one by one."""
|
||||
system_prompt = "You are a helpful AI assistant."
|
||||
|
||||
# Create a simple context ElementTree
|
||||
context_et = ET.Element("context")
|
||||
context_et.set("time", "2024-10-18T12:00:00Z")
|
||||
context_et.text = "Test context"
|
||||
|
||||
prefix = ""
|
||||
|
||||
# Get the generator for token-by-token output
|
||||
token_generator = self.engine.infer(system_prompt, context_et, prefix)
|
||||
|
||||
# Collect tokens
|
||||
response = ""
|
||||
tokens_received = 0
|
||||
|
||||
for token in token_generator:
|
||||
response += token
|
||||
tokens_received += 1
|
||||
# Each "token" should be small (in the mock script, it's up to 10 characters)
|
||||
self.assertLessEqual(len(token), 10, "Each token should be small")
|
||||
|
||||
# Verify we received multiple tokens
|
||||
self.assertGreater(tokens_received, 10, "Should receive multiple tokens")
|
||||
|
||||
expected = "<reasoning>This is a test response.</reasoning>"
|
||||
self.assertEqual(response, expected, "Inference response should match expected output")
|
||||
|
||||
def test_multiple_inferences(self):
|
||||
"""Test running multiple inference cycles one after another."""
|
||||
system_prompt = "You are a helpful AI assistant."
|
||||
|
||||
# Create a simple context ElementTree
|
||||
context_et = ET.Element("context")
|
||||
context_et.set("time", "2024-10-18T12:00:00Z")
|
||||
context_et.text = "Test context"
|
||||
|
||||
prefix = ""
|
||||
|
||||
# Run first inference
|
||||
token_generator = self.engine.infer(system_prompt, context_et, prefix)
|
||||
response = ""
|
||||
for token in token_generator:
|
||||
response += token
|
||||
|
||||
expected = "<reasoning>This is a test response.</reasoning>"
|
||||
self.assertEqual(response, expected, "First inference response should match expected output")
|
||||
|
||||
# Add a small delay between inferences to see if it helps
|
||||
time.sleep(0.1)
|
||||
|
||||
# Run second inference
|
||||
token_generator = self.engine.infer(system_prompt, context_et, prefix)
|
||||
response = ""
|
||||
for token in token_generator:
|
||||
response += token
|
||||
|
||||
self.assertEqual(response, expected, "Second inference response should match expected output")
|
||||
|
||||
# We should be able to get the token limit again after inference
|
||||
limit = self.engine.token_limit()
|
||||
self.assertEqual(limit, 1024, "Token limit should still be 1024 after multiple inferences")
|
||||
|
||||
def test_restart(self):
|
||||
"""Test restarting the LLM engine."""
|
||||
# First get a token limit
|
||||
limit = self.engine.token_limit()
|
||||
self.assertEqual(limit, 1024, "Token limit should be 1024")
|
||||
|
||||
# Restart the engine
|
||||
self.engine.restart()
|
||||
|
||||
# Get token limit again
|
||||
limit = self.engine.token_limit()
|
||||
self.assertEqual(limit, 1024, "Token limit should still be 1024 after restart")
|
||||
|
||||
# Engine should still work for inference after restart
|
||||
system_prompt = "You are a helpful AI assistant."
|
||||
|
||||
# Create a simple context ElementTree
|
||||
context_et = ET.Element("context")
|
||||
context_et.set("time", "2024-10-18T12:00:00Z")
|
||||
context_et.text = "Test context"
|
||||
|
||||
prefix = ""
|
||||
|
||||
token_generator = self.engine.infer(system_prompt, context_et, prefix)
|
||||
response = ""
|
||||
for token in token_generator:
|
||||
response += token
|
||||
|
||||
expected = "<reasoning>This is a test response.</reasoning>"
|
||||
self.assertEqual(response, expected, "Inference should work after restart")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,53 +0,0 @@
|
||||
import unittest
|
||||
|
||||
from itertools import tee
|
||||
|
||||
from . import test_data
|
||||
|
||||
from sia.llm_engine import LlmEngine
|
||||
from sia.llm_engine.local_llm_engine import LocalLlmEngine
|
||||
|
||||
class LocalLlmEngineTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.model_path = "/root/models/current"
|
||||
|
||||
@unittest.skip("Takes too long")
|
||||
def test_initialization(self):
|
||||
llm_engine = LocalLlmEngine(self.model_path, 0.2, 1024)
|
||||
self.assertIsInstance(llm_engine, LlmEngine)
|
||||
|
||||
@unittest.skip("Takes too long")
|
||||
def test_infer(self):
|
||||
main_context = "This is a test"
|
||||
llm_engine = LocalLlmEngine(self.model_path, 0.2, 1024)
|
||||
tokens = llm_engine.infer(test_data.echo_system_prompt, main_context, "")
|
||||
print_tokens, result_tokens = tee(tokens)
|
||||
for token in print_tokens:
|
||||
print(token, end="", flush=True)
|
||||
result = ''.join(result_tokens)
|
||||
self.assertEqual(result, f"{main_context}<test_tag>{main_context}</test_tag>")
|
||||
|
||||
@unittest.skip("Takes too long")
|
||||
def test_token_count(self):
|
||||
"""Test token counting returns reasonable numbers"""
|
||||
llm_engine = LocalLlmEngine(self.model_path, 0.2, 1024)
|
||||
|
||||
# Test with short inputs
|
||||
count = llm_engine.token_count("Short prompt", "Brief context")
|
||||
self.assertGreater(count, 5)
|
||||
self.assertLess(count, 50)
|
||||
|
||||
# Test with longer inputs
|
||||
long_prompt = "A detailed system prompt with multiple sentences. " * 5
|
||||
long_context = "An extensive context containing various details. " * 10
|
||||
count = llm_engine.token_count(long_prompt, long_context)
|
||||
self.assertGreater(count, 100)
|
||||
self.assertLess(count, 1000)
|
||||
|
||||
@unittest.skip("Takes too long")
|
||||
def test_token_limit(self):
|
||||
"""Test token limit is within expected range for modern LLMs"""
|
||||
llm_engine = LocalLlmEngine(self.model_path, 0.2, 1024)
|
||||
limit = llm_engine.token_limit()
|
||||
self.assertGreaterEqual(limit, 2048)
|
||||
self.assertLessEqual(limit, 1048576)
|
||||
@@ -5,37 +5,6 @@ import xml.etree.ElementTree as ET
|
||||
from sia import util
|
||||
|
||||
class UtilTest(unittest.TestCase):
|
||||
def test_stop_before_value(self):
|
||||
# Helper function to create iterator from list
|
||||
def create_iterator(items: list) -> Iterator[str]:
|
||||
for item in items:
|
||||
yield item
|
||||
|
||||
# Test case 1: Stop value in middle of sequence
|
||||
input_sequence = ['hello', 'world', 'STOP', 'ignored']
|
||||
result = list(util.stop_before_value(create_iterator(input_sequence), 'STOP'))
|
||||
self.assertEqual(result, ['hello', 'world'])
|
||||
|
||||
# Test case 2: Stop value not in sequence
|
||||
input_sequence = ['hello', 'world']
|
||||
result = list(util.stop_before_value(create_iterator(input_sequence), 'STOP'))
|
||||
self.assertEqual(result, ['hello', 'world'])
|
||||
|
||||
# Test case 3: Stop value at start of sequence
|
||||
input_sequence = ['STOP', 'ignored']
|
||||
result = list(util.stop_before_value(create_iterator(input_sequence), 'STOP'))
|
||||
self.assertEqual(result, [])
|
||||
|
||||
# Test case 4: Stop value as part of an item
|
||||
input_sequence = ['hello', 'woSTOPrld', 'ignored']
|
||||
result = list(util.stop_before_value(create_iterator(input_sequence), 'STOP'))
|
||||
self.assertEqual(result, ['hello', 'wo'])
|
||||
|
||||
# Test case 5: Empty sequence
|
||||
input_sequence = []
|
||||
result = list(util.stop_before_value(create_iterator(input_sequence), 'STOP'))
|
||||
self.assertEqual(result, [])
|
||||
|
||||
def test_pretty_print_element_basic(self):
|
||||
"""Test basic element printing"""
|
||||
elem = ET.Element('root')
|
||||
|
||||
@@ -10,10 +10,9 @@ 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.iteration_logger import IterationLogger
|
||||
from sia.web_agent import WebAgent, LlmState
|
||||
from sia.web_agent import WebAgent, AgentState
|
||||
from sia.command import Command
|
||||
from sia.command_result import CommandResult
|
||||
from sia.entry.reasoning_entry import ReasoningEntry
|
||||
@@ -22,7 +21,7 @@ from sia.entry import Entry
|
||||
from sia.response_buffer import ResponseBuffer
|
||||
from pathlib import Path
|
||||
|
||||
class MockLlmEngine(LlmEngine):
|
||||
class MockLlmEngine:
|
||||
"""Mock LLM engine for testing."""
|
||||
|
||||
def __init__(self, output: str = "<reasoning>test reasoning</reasoning>"):
|
||||
@@ -67,14 +66,10 @@ class WebAgentTest(unittest.TestCase):
|
||||
'default': MockLlmEngine(),
|
||||
'alternative': MockLlmEngine("<reasoning>alternative reasoning</reasoning>")
|
||||
}
|
||||
self.mock_validator = Mock(spec=XMLValidator)
|
||||
self.io_buffer = WebIOBuffer()
|
||||
self.working_memory = WorkingMemory()
|
||||
self.work_dir = Path("/tmp") # Use temp directory for tests
|
||||
|
||||
# Mock validator to pass by default
|
||||
self.mock_validator.validate.return_value = None
|
||||
|
||||
# Create parser with IO buffer
|
||||
self.parser = ResponseParser(self.work_dir, self.io_buffer)
|
||||
|
||||
@@ -98,7 +93,6 @@ class WebAgentTest(unittest.TestCase):
|
||||
working_memory=self.working_memory,
|
||||
metrics=self.mock_metrics,
|
||||
llms=self.mock_llms,
|
||||
validator=self.mock_validator,
|
||||
parser=self.parser,
|
||||
iteration_logger=self.mock_iteration_logger
|
||||
)
|
||||
@@ -107,7 +101,7 @@ class WebAgentTest(unittest.TestCase):
|
||||
self.llm_state_changes = []
|
||||
self.context_changes = []
|
||||
|
||||
def llm_change_handler(self, llm_name: str, state: LlmState):
|
||||
def llm_change_handler(self, llm_name: str, state: AgentState):
|
||||
"""Track LLM state changes for verification."""
|
||||
self.llm_state_changes.append((llm_name, state))
|
||||
|
||||
@@ -124,23 +118,11 @@ class WebAgentTest(unittest.TestCase):
|
||||
# Check LLM states
|
||||
llm_states = self.agent.llms
|
||||
self.assertEqual(len(llm_states), 2)
|
||||
self.assertEqual(llm_states['default'], LlmState.IDLE)
|
||||
self.assertEqual(llm_states['alternative'], LlmState.IDLE)
|
||||
self.assertEqual(llm_states['default'], AgentState.IDLE)
|
||||
self.assertEqual(llm_states['alternative'], AgentState.IDLE)
|
||||
|
||||
# Check response buffer
|
||||
self.assertIsInstance(self.agent.response_buffer, ResponseBuffer)
|
||||
|
||||
def test_handler_registration(self):
|
||||
"""Test adding state and context change handlers."""
|
||||
self.agent.add_llm_change_handler(self.llm_change_handler)
|
||||
self.agent.add_context_change_handler(self.context_change_handler)
|
||||
|
||||
# Adding same handler twice should not duplicate
|
||||
self.agent.add_llm_change_handler(self.llm_change_handler)
|
||||
self.agent.add_context_change_handler(self.context_change_handler)
|
||||
|
||||
self.assertEqual(len(self.agent._llm_change_handlers), 1)
|
||||
self.assertEqual(len(self.agent._context_change_handlers), 1)
|
||||
|
||||
def test_run_inference(self):
|
||||
"""Test running inference."""
|
||||
@@ -153,8 +135,8 @@ class WebAgentTest(unittest.TestCase):
|
||||
|
||||
# Check state changes
|
||||
self.assertEqual(len(self.llm_state_changes), 2) # IDLE -> INFERENCE -> IDLE
|
||||
self.assertEqual(self.llm_state_changes[0], ('default', LlmState.INFERENCE))
|
||||
self.assertEqual(self.llm_state_changes[1], ('default', LlmState.IDLE))
|
||||
self.assertEqual(self.llm_state_changes[0], ('default', AgentState.INFERENCE))
|
||||
self.assertEqual(self.llm_state_changes[1], ('default', AgentState.IDLE))
|
||||
|
||||
# Verify LLM was called
|
||||
self.assertTrue(self.mock_llms['default'].infer_called)
|
||||
@@ -171,7 +153,7 @@ class WebAgentTest(unittest.TestCase):
|
||||
def test_run_inference_busy_llm(self):
|
||||
"""Test running inference on a busy LLM."""
|
||||
# Set LLM state to INFERENCE
|
||||
with patch.object(self.agent, '_llm_states', {'default': LlmState.INFERENCE}):
|
||||
with patch.object(self.agent, '_llm_states', {'default': AgentState.INFERENCE}):
|
||||
with self.assertRaises(RuntimeError):
|
||||
self.agent.run_inference('default')
|
||||
|
||||
@@ -187,7 +169,6 @@ class WebAgentTest(unittest.TestCase):
|
||||
working_memory=WorkingMemory(),
|
||||
metrics=self.mock_metrics,
|
||||
llms={'test': mock_engine},
|
||||
validator=self.mock_validator,
|
||||
parser=self.parser,
|
||||
iteration_logger=self.mock_iteration_logger
|
||||
)
|
||||
@@ -224,7 +205,6 @@ class WebAgentTest(unittest.TestCase):
|
||||
working_memory=WorkingMemory(),
|
||||
metrics=self.mock_metrics,
|
||||
llms=self.mock_llms,
|
||||
validator=self.mock_validator,
|
||||
parser=self.parser,
|
||||
iteration_logger=self.mock_iteration_logger
|
||||
)
|
||||
@@ -254,7 +234,6 @@ class WebAgentTest(unittest.TestCase):
|
||||
working_memory=WorkingMemory(),
|
||||
metrics=self.mock_metrics,
|
||||
llms=self.mock_llms,
|
||||
validator=self.mock_validator,
|
||||
parser=self.parser,
|
||||
iteration_logger=self.mock_iteration_logger
|
||||
)
|
||||
@@ -290,7 +269,7 @@ class WebAgentTest(unittest.TestCase):
|
||||
self.assertTrue(mock_command.executed)
|
||||
|
||||
# Check command result was stored
|
||||
self.assertEqual(self.agent.command_result, command_result)
|
||||
self.assertEqual(self.agent._command_result, command_result)
|
||||
|
||||
# Verify iteration was logged
|
||||
self.mock_iteration_logger.log_iteration.assert_called_once()
|
||||
@@ -329,7 +308,6 @@ class WebAgentTest(unittest.TestCase):
|
||||
working_memory=test_memory,
|
||||
metrics=self.mock_metrics,
|
||||
llms=self.mock_llms,
|
||||
validator=self.mock_validator,
|
||||
parser=self.parser,
|
||||
iteration_logger=self.mock_iteration_logger
|
||||
)
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import unittest
|
||||
|
||||
from sia.xml_validator import XMLValidator
|
||||
|
||||
class XMLValidatorTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""Set up test schema"""
|
||||
self.test_schema = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="test_element">
|
||||
<xs:complexType>
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:string">
|
||||
<xs:attribute name="id" type="xs:string" use="required"/>
|
||||
<xs:attribute name="count" type="xs:integer"/>
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="simple_element">
|
||||
<xs:complexType>
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:string"/>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>"""
|
||||
|
||||
self.validator = XMLValidator(self.test_schema)
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test validator initialization and root element extraction"""
|
||||
valid_elements = self.validator.get_valid_root_elements()
|
||||
self.assertEqual(valid_elements, {'test_element', 'simple_element'})
|
||||
|
||||
def test_invalid_schema(self):
|
||||
"""Test handling invalid schema"""
|
||||
with self.assertRaises(ValueError):
|
||||
XMLValidator("invalid schema")
|
||||
|
||||
def test_valid_xml(self):
|
||||
"""Test validation of valid XML"""
|
||||
xml = '<test_element id="123" count="42">test content</test_element>'
|
||||
result = self.validator.validate(xml)
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Test without optional attribute
|
||||
xml = '<test_element id="123">test content</test_element>'
|
||||
result = self.validator.validate(xml)
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Test simple element
|
||||
xml = '<simple_element>test content</simple_element>'
|
||||
result = self.validator.validate(xml)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_invalid_root_element(self):
|
||||
"""Test validation with invalid root element"""
|
||||
xml = '<invalid_element>test content</invalid_element>'
|
||||
result = self.validator.validate(xml)
|
||||
self.assertIsNotNone(result)
|
||||
self.assertIn("Invalid root element", result)
|
||||
|
||||
def test_missing_required_attribute(self):
|
||||
"""Test validation with missing required attribute"""
|
||||
xml = '<test_element>test content</test_element>'
|
||||
result = self.validator.validate(xml)
|
||||
self.assertIsNotNone(result)
|
||||
self.assertIn("Missing required attribute", result)
|
||||
|
||||
def test_invalid_integer_attribute(self):
|
||||
"""Test validation with invalid integer attribute"""
|
||||
xml = '<test_element id="123" count="not_a_number">test content</test_element>'
|
||||
result = self.validator.validate(xml)
|
||||
self.assertIsNotNone(result)
|
||||
self.assertIn("Invalid integer value", result)
|
||||
|
||||
def test_unexpected_attribute(self):
|
||||
"""Test validation with unexpected attribute"""
|
||||
xml = '<test_element id="123" invalid_attr="value">test content</test_element>'
|
||||
result = self.validator.validate(xml)
|
||||
self.assertIsNotNone(result)
|
||||
self.assertIn("Unexpected attribute", result)
|
||||
|
||||
def test_invalid_xml_syntax(self):
|
||||
"""Test validation with invalid XML syntax"""
|
||||
xml = '<test_element>unclosed tag'
|
||||
result = self.validator.validate(xml)
|
||||
self.assertIsNotNone(result)
|
||||
self.assertIn("Invalid XML", result)
|
||||
|
||||
def test_whitespace_handling(self):
|
||||
"""Test validation with whitespace in XML"""
|
||||
xml = """
|
||||
<test_element id="123">
|
||||
test content
|
||||
</test_element>
|
||||
"""
|
||||
result = self.validator.validate(xml)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_empty_content(self):
|
||||
"""Test validation with empty element content"""
|
||||
xml = '<test_element id="123"></test_element>'
|
||||
result = self.validator.validate(xml)
|
||||
self.assertIsNone(result)
|
||||
|
||||
xml = '<test_element id="123"/>'
|
||||
result = self.validator.validate(xml)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_special_characters(self):
|
||||
"""Test validation with special characters"""
|
||||
xml = '<test_element id="123"><![CDATA[Special & < > " \' chars]]></test_element>'
|
||||
result = self.validator.validate(xml)
|
||||
self.assertIsNone(result)
|
||||
Reference in New Issue
Block a user