forked from cleveragents/cleveragents-core
462 lines
18 KiB
Python
462 lines
18 KiB
Python
"""
|
|
Unit tests for agents/llm.py
|
|
|
|
Tests the LLMAgent class.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, patch, AsyncMock
|
|
|
|
from cleveragents.agents.llm import LLMAgent, DEFAULT_SYSTEM_MESSAGE
|
|
from cleveragents.core.exceptions import ConfigurationError, ExecutionError
|
|
from cleveragents.templates.renderer import TemplateRenderer
|
|
|
|
|
|
class TestLLMAgent:
|
|
"""Test suite for the LLMAgent class."""
|
|
|
|
@pytest.fixture
|
|
def template_renderer(self):
|
|
"""Create a basic template renderer."""
|
|
return TemplateRenderer()
|
|
|
|
@pytest.fixture
|
|
def openai_config(self):
|
|
"""Create an OpenAI configuration."""
|
|
return {
|
|
"type": "llm",
|
|
"provider": "openai",
|
|
"model": "gpt-4",
|
|
"temperature": 0.7,
|
|
"max_tokens": 1000,
|
|
}
|
|
|
|
@pytest.fixture
|
|
def anthropic_config(self):
|
|
"""Create an Anthropic configuration."""
|
|
return {
|
|
"type": "llm",
|
|
"provider": "anthropic",
|
|
"model": "claude-3-5-sonnet-20241022",
|
|
"temperature": 0.8,
|
|
}
|
|
|
|
@pytest.fixture
|
|
def google_config(self):
|
|
"""Create a Google configuration."""
|
|
return {
|
|
"type": "llm",
|
|
"provider": "google",
|
|
"model": "gemini-1.5-flash",
|
|
}
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
def test_llm_agent_initialization_openai(self, mock_chat_openai, openai_config, template_renderer):
|
|
"""Test LLMAgent initialization with OpenAI."""
|
|
mock_model = Mock()
|
|
mock_chat_openai.return_value = mock_model
|
|
|
|
agent = LLMAgent("test_llm", openai_config, template_renderer)
|
|
|
|
assert agent.name == "test_llm"
|
|
assert agent.provider == "openai"
|
|
assert agent.model == "gpt-4"
|
|
assert agent.temperature == 0.7
|
|
assert agent.max_tokens == 1000
|
|
assert agent.system_message == DEFAULT_SYSTEM_MESSAGE
|
|
mock_chat_openai.assert_called_once()
|
|
|
|
@patch('cleveragents.agents.llm.ChatAnthropic')
|
|
def test_llm_agent_initialization_anthropic(self, mock_chat_anthropic, anthropic_config, template_renderer):
|
|
"""Test LLMAgent initialization with Anthropic."""
|
|
mock_model = Mock()
|
|
mock_chat_anthropic.return_value = mock_model
|
|
|
|
agent = LLMAgent("test_llm", anthropic_config, template_renderer)
|
|
|
|
assert agent.provider == "anthropic"
|
|
assert agent.model == "claude-3-5-sonnet-20241022"
|
|
mock_chat_anthropic.assert_called_once()
|
|
|
|
@patch('cleveragents.agents.llm.ChatGoogleGenerativeAI')
|
|
def test_llm_agent_initialization_google(self, mock_chat_google, google_config, template_renderer):
|
|
"""Test LLMAgent initialization with Google."""
|
|
mock_model = Mock()
|
|
mock_chat_google.return_value = mock_model
|
|
|
|
agent = LLMAgent("test_llm", google_config, template_renderer)
|
|
|
|
assert agent.provider == "google"
|
|
assert agent.model == "gemini-1.5-flash"
|
|
mock_chat_google.assert_called_once()
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
def test_llm_agent_default_values(self, mock_chat_openai, template_renderer):
|
|
"""Test LLMAgent with default values."""
|
|
mock_model = Mock()
|
|
mock_chat_openai.return_value = mock_model
|
|
|
|
config = {"type": "llm"}
|
|
agent = LLMAgent("test_llm", config, template_renderer)
|
|
|
|
assert agent.provider == "openai"
|
|
assert agent.model == "gpt-3.5-turbo" # Default model
|
|
assert agent.temperature == 0.7
|
|
assert agent.max_tokens == 1000
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
def test_llm_agent_custom_system_message(self, mock_chat_openai, template_renderer):
|
|
"""Test LLMAgent with custom system message."""
|
|
mock_model = Mock()
|
|
mock_chat_openai.return_value = mock_model
|
|
|
|
config = {
|
|
"type": "llm",
|
|
"system_prompt": "You are a code reviewer.",
|
|
}
|
|
agent = LLMAgent("test_llm", config, template_renderer)
|
|
|
|
assert agent.system_message == "You are a code reviewer."
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
def test_llm_agent_get_capabilities(self, mock_chat_openai, openai_config, template_renderer):
|
|
"""Test getting agent capabilities."""
|
|
mock_model = Mock()
|
|
mock_chat_openai.return_value = mock_model
|
|
|
|
agent = LLMAgent("test_llm", openai_config, template_renderer)
|
|
capabilities = agent.get_capabilities()
|
|
|
|
assert "text-generation" in capabilities
|
|
assert "conversation" in capabilities
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
@pytest.mark.asyncio
|
|
async def test_llm_agent_process_message(self, mock_chat_openai, openai_config, template_renderer):
|
|
"""Test processing a message."""
|
|
# Mock the chat model
|
|
mock_model = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = "This is the AI response"
|
|
mock_model.ainvoke = AsyncMock(return_value=mock_response)
|
|
mock_chat_openai.return_value = mock_model
|
|
|
|
agent = LLMAgent("test_llm", openai_config, template_renderer)
|
|
result = await agent.process_message("Hello, AI!")
|
|
|
|
assert result == "This is the AI response"
|
|
mock_model.ainvoke.assert_called_once()
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
@pytest.mark.asyncio
|
|
async def test_llm_agent_process_message_with_context(self, mock_chat_openai, openai_config, template_renderer):
|
|
"""Test processing a message with context."""
|
|
mock_model = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = "Response with context"
|
|
mock_model.ainvoke = AsyncMock(return_value=mock_response)
|
|
mock_chat_openai.return_value = mock_model
|
|
|
|
agent = LLMAgent("test_llm", openai_config, template_renderer)
|
|
context = {"user_name": "Alice"}
|
|
result = await agent.process_message("Hello", context)
|
|
|
|
assert result == "Response with context"
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
def test_llm_agent_get_default_model(self, mock_chat_openai, template_renderer):
|
|
"""Test getting default models for different providers."""
|
|
mock_chat_openai.return_value = Mock()
|
|
|
|
# Test OpenAI default
|
|
config = {"type": "llm", "provider": "openai"}
|
|
agent = LLMAgent("test", config, template_renderer)
|
|
assert agent.model == "gpt-3.5-turbo"
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
def test_llm_agent_unsupported_provider(self, mock_chat_openai, template_renderer):
|
|
"""Test that unsupported provider raises error."""
|
|
mock_chat_openai.return_value = Mock()
|
|
|
|
config = {"type": "llm", "provider": "unsupported"}
|
|
|
|
with pytest.raises(ConfigurationError) as exc_info:
|
|
LLMAgent("test", config, template_renderer)
|
|
|
|
assert "unsupported" in str(exc_info.value).lower()
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
@pytest.mark.asyncio
|
|
async def test_llm_agent_execution_error(self, mock_chat_openai, openai_config, template_renderer):
|
|
"""Test that LangChain exceptions are wrapped in ExecutionError."""
|
|
from langchain_core.exceptions import LangChainException
|
|
|
|
mock_model = Mock()
|
|
mock_model.ainvoke = AsyncMock(side_effect=LangChainException("API Error"))
|
|
mock_chat_openai.return_value = mock_model
|
|
|
|
agent = LLMAgent("test_llm", openai_config, template_renderer)
|
|
|
|
with pytest.raises(ExecutionError) as exc_info:
|
|
await agent.process_message("Hello")
|
|
|
|
assert "LLM processing failed" in str(exc_info.value)
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
@pytest.mark.asyncio
|
|
async def test_llm_agent_process_with_memory_context(self, mock_chat_openai, openai_config, template_renderer):
|
|
"""Test LLM processes with memory context."""
|
|
mock_model = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = "Response with memory"
|
|
mock_model.ainvoke = AsyncMock(return_value=mock_response)
|
|
mock_chat_openai.return_value = mock_model
|
|
|
|
agent = LLMAgent("test_llm", openai_config, template_renderer)
|
|
|
|
# Add some memory
|
|
agent.memory["previous_message"] = "Previous context"
|
|
|
|
result = await agent.process_message("New message")
|
|
|
|
assert result == "Response with memory"
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
def test_llm_agent_get_metadata(self, mock_chat_openai, openai_config, template_renderer):
|
|
"""Test getting LLM agent metadata."""
|
|
mock_model = Mock()
|
|
mock_chat_openai.return_value = mock_model
|
|
|
|
agent = LLMAgent("test_llm", openai_config, template_renderer)
|
|
metadata = agent.get_metadata()
|
|
|
|
assert metadata["name"] == "test_llm"
|
|
assert metadata["type"] == "LLMAgent"
|
|
assert "capabilities" in metadata
|
|
|
|
@patch('cleveragents.agents.llm.ChatAnthropic')
|
|
def test_llm_agent_default_anthropic_model(self, mock_anthropic, template_renderer):
|
|
"""Test default model for Anthropic provider."""
|
|
config = {"type": "llm", "provider": "anthropic"}
|
|
agent = LLMAgent("test_llm", config, template_renderer)
|
|
|
|
assert agent.model == "claude-3-5-sonnet-20241022"
|
|
|
|
@patch('cleveragents.agents.llm.ChatGoogleGenerativeAI')
|
|
def test_llm_agent_default_google_model(self, mock_google, template_renderer):
|
|
"""Test default model for Google provider."""
|
|
config = {"type": "llm", "provider": "google"}
|
|
agent = LLMAgent("test_llm", config, template_renderer)
|
|
|
|
assert agent.model == "gemini-1.5-flash"
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
@pytest.mark.asyncio
|
|
async def test_llm_agent_empty_message(self, mock_chat_openai, openai_config, template_renderer):
|
|
"""Test processing an empty message."""
|
|
mock_model = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = "Response to empty"
|
|
mock_model.ainvoke = AsyncMock(return_value=mock_response)
|
|
mock_chat_openai.return_value = mock_model
|
|
|
|
agent = LLMAgent("test_llm", openai_config, template_renderer)
|
|
result = await agent.process_message("")
|
|
|
|
assert result == "Response to empty"
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
@pytest.mark.asyncio
|
|
async def test_llm_agent_long_message(self, mock_chat_openai, openai_config, template_renderer):
|
|
"""Test processing a long message."""
|
|
mock_model = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = "Response to long message"
|
|
mock_model.ainvoke = AsyncMock(return_value=mock_response)
|
|
mock_chat_openai.return_value = mock_model
|
|
|
|
agent = LLMAgent("test_llm", openai_config, template_renderer)
|
|
long_message = "x" * 10000
|
|
result = await agent.process_message(long_message)
|
|
|
|
assert result == "Response to long message"
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
def test_llm_agent_config_values(self, mock_chat_openai, template_renderer):
|
|
"""Test that config values are stored correctly."""
|
|
config = {
|
|
"type": "llm",
|
|
"provider": "openai",
|
|
"model": "gpt-4-turbo",
|
|
"temperature": 0.9,
|
|
"max_tokens": 2000,
|
|
"system_prompt": "Custom system",
|
|
}
|
|
|
|
agent = LLMAgent("test_llm", config, template_renderer)
|
|
|
|
assert agent.provider == "openai"
|
|
assert agent.model == "gpt-4-turbo"
|
|
assert agent.temperature == 0.9
|
|
assert agent.max_tokens == 2000
|
|
assert agent.system_message == "Custom system"
|
|
|
|
|
|
class TestLLMAgentProcessMessageExtended:
|
|
"""Extended tests for LLMAgent process_message method."""
|
|
|
|
@pytest.fixture
|
|
def template_renderer(self):
|
|
"""Provide template renderer for tests."""
|
|
return TemplateRenderer()
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
@pytest.mark.asyncio
|
|
async def test_process_message_with_template_in_config(self, mock_chat_openai, template_renderer):
|
|
"""Test process_message where template is in self.config."""
|
|
mock_model = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = "Template processed response"
|
|
mock_model.ainvoke = AsyncMock(return_value=mock_response)
|
|
mock_chat_openai.return_value = mock_model
|
|
|
|
# Mock template rendering
|
|
template_renderer.render = Mock(return_value="Rendered: Test message")
|
|
|
|
config = {
|
|
"provider": "openai",
|
|
"model": "gpt-4",
|
|
"template": "test_template",
|
|
"template_vars": {"extra": "value"}
|
|
}
|
|
|
|
agent = LLMAgent("test_llm", config, template_renderer)
|
|
result = await agent.process_message("Test message", {"key": "value"})
|
|
|
|
# Verify template was called
|
|
template_renderer.render.assert_called_once()
|
|
assert "test_template" in str(template_renderer.render.call_args)
|
|
assert result == "Template processed response"
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
@pytest.mark.asyncio
|
|
async def test_process_message_with_conversation_history_in_context(self, mock_chat_openai, template_renderer):
|
|
"""Test process_message where conversation_history is in context."""
|
|
mock_model = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = "Response with context history"
|
|
mock_model.ainvoke = AsyncMock(return_value=mock_response)
|
|
mock_chat_openai.return_value = mock_model
|
|
|
|
config = {"provider": "openai", "model": "gpt-4"}
|
|
agent = LLMAgent("test_llm", config, template_renderer)
|
|
|
|
context = {
|
|
"conversation_history": [
|
|
{"role": "user", "content": "Previous message"},
|
|
{"role": "assistant", "content": "Previous response"}
|
|
]
|
|
}
|
|
|
|
result = await agent.process_message("New message", context)
|
|
|
|
# Verify the model was called with history
|
|
call_args = mock_model.ainvoke.call_args[0][0]
|
|
assert len(call_args) == 4 # System + 2 history + current
|
|
assert result == "Response with context history"
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
@pytest.mark.asyncio
|
|
async def test_process_message_with_memory_enabled_no_context_history(self, mock_chat_openai, template_renderer):
|
|
"""Test where conversation_history is not in context but memory_enabled is true."""
|
|
mock_model = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = "Response from memory"
|
|
mock_model.ainvoke = AsyncMock(return_value=mock_response)
|
|
mock_chat_openai.return_value = mock_model
|
|
|
|
config = {
|
|
"provider": "openai",
|
|
"model": "gpt-4",
|
|
"memory_enabled": True
|
|
}
|
|
|
|
agent = LLMAgent("test_llm", config, template_renderer)
|
|
|
|
# Pre-populate memory with conversation history
|
|
agent.memory["conversation_history"] = [
|
|
{"role": "user", "content": "Memory message"},
|
|
{"role": "assistant", "content": "Memory response"}
|
|
]
|
|
|
|
# Call without context history
|
|
result = await agent.process_message("New message", {})
|
|
|
|
assert result == "Response from memory"
|
|
# Verify memory was used
|
|
assert "conversation_history" in agent.memory
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
@pytest.mark.asyncio
|
|
async def test_process_message_with_existing_history(self, mock_chat_openai, template_renderer):
|
|
"""Test where history exists."""
|
|
mock_model = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = "Response with history"
|
|
mock_model.ainvoke = AsyncMock(return_value=mock_response)
|
|
mock_chat_openai.return_value = mock_model
|
|
|
|
config = {"provider": "openai", "model": "gpt-4"}
|
|
agent = LLMAgent("test_llm", config, template_renderer)
|
|
|
|
# Provide context with history
|
|
context = {
|
|
"conversation_history": [
|
|
{"role": "user", "content": "First"},
|
|
{"role": "assistant", "content": "Second"},
|
|
{"role": "user", "content": "Third"}
|
|
]
|
|
}
|
|
|
|
result = await agent.process_message("Fourth message", context)
|
|
|
|
# Verify history was included in the call
|
|
call_args = mock_model.ainvoke.call_args[0][0]
|
|
assert len(call_args) == 5 # System + 3 history + current
|
|
assert result == "Response with history"
|
|
|
|
@patch('cleveragents.agents.llm.ChatOpenAI')
|
|
@pytest.mark.asyncio
|
|
async def test_process_message_with_memory_enabled_updates_memory(self, mock_chat_openai, template_renderer):
|
|
"""Test where self.config.get('memory_enabled') is true and memory is updated."""
|
|
mock_model = Mock()
|
|
mock_response = Mock()
|
|
mock_response.content = "Memory updated response"
|
|
mock_model.ainvoke = AsyncMock(return_value=mock_response)
|
|
mock_chat_openai.return_value = mock_model
|
|
|
|
config = {
|
|
"provider": "openai",
|
|
"model": "gpt-4",
|
|
"memory_enabled": True,
|
|
"max_history": 10
|
|
}
|
|
|
|
agent = LLMAgent("test_llm", config, template_renderer)
|
|
|
|
result = await agent.process_message("Test message")
|
|
|
|
# Verify memory was updated
|
|
assert "last_message" in agent.memory
|
|
assert "last_response" in agent.memory
|
|
assert "conversation_history" in agent.memory
|
|
assert agent.memory["last_message"] == "Test message"
|
|
assert agent.memory["last_response"] == "Memory updated response"
|
|
assert len(agent.memory["conversation_history"]) == 2 # user + assistant
|
|
assert result == "Memory updated response"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|
|
|