""" Copyright 2024-2026 ReplyPier Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ import pytest import sys from unittest.mock import patch, MagicMock, AsyncMock from agno.agent import Agent from app.utils import agno_utils from fastapi import HTTPException import importlib class TestCreateModel: """Test cases for the create_model function""" def test_create_model_openai_with_response_format(self): """Test creating OpenAI model with response format""" with patch("app.utils.agno_utils.OpenAIChat") as mock_openai: mock_model = MagicMock() mock_openai.return_value = mock_model result = agno_utils.create_model( model_type="OPENAI", api_key="test-api-key", model_name="gpt-4", max_tokens=2000, response_format={"type": "json_object"} ) mock_openai.assert_called_once_with( api_key="test-api-key", id="gpt-4", max_tokens=2000, response_format={"type": "json_object"} ) assert result == mock_model def test_create_model_openai_without_response_format(self): """Test creating OpenAI model without response format""" with patch("app.utils.agno_utils.OpenAIChat") as mock_openai: mock_model = MagicMock() mock_openai.return_value = mock_model result = agno_utils.create_model( model_type="OPENAI", api_key="test-api-key", model_name="gpt-3.5-turbo" ) mock_openai.assert_called_once_with( api_key="test-api-key", id="gpt-3.5-turbo", max_tokens=1000 ) assert result == mock_model def test_create_model_openai_compatible(self): """Test creating an OPENAI_COMPATIBLE model passes base_url through""" with patch("app.utils.agno_utils.OpenAIChat") as mock_openai: mock_model = MagicMock() mock_openai.return_value = mock_model result = agno_utils.create_model( model_type="OPENAI_COMPATIBLE", api_key="test-api-key", model_name="local-model", base_url="https://openrouter.ai/api/v1" ) mock_openai.assert_called_once_with( api_key="test-api-key", id="local-model", base_url="https://openrouter.ai/api/v1", max_tokens=1000 ) assert result == mock_model def test_create_model_openai_compatible_strips_chat_completions_suffix(self): """A base_url accidentally including /chat/completions is normalized""" with patch("app.utils.agno_utils.OpenAIChat") as mock_openai: mock_model = MagicMock() mock_openai.return_value = mock_model agno_utils.create_model( model_type="OPENAI_COMPATIBLE", api_key="test-api-key", model_name="local-model", base_url="https://openrouter.ai/api/v1/chat/completions/" ) mock_openai.assert_called_once_with( api_key="test-api-key", id="local-model", base_url="https://openrouter.ai/api/v1", max_tokens=1000 ) def test_create_model_openai_compatible_requires_base_url(self): """OPENAI_COMPATIBLE without a base_url raises""" with pytest.raises(HTTPException): agno_utils.create_model( model_type="OPENAI_COMPATIBLE", api_key="test-api-key", model_name="local-model" ) def test_create_model_replypier(self): """Test creating REPLYPIER model (should use OpenAI)""" with patch("app.utils.agno_utils.OpenAIChat") as mock_openai: mock_model = MagicMock() mock_openai.return_value = mock_model result = agno_utils.create_model( model_type="REPLYPIER", api_key="test-api-key", model_name="replypier-model" ) mock_openai.assert_called_once_with( api_key="test-api-key", id="replypier-model", max_tokens=1000 ) assert result == mock_model def test_create_model_anthropic(self): """Test creating Anthropic model""" # Mock the import statement inside the function mock_claude_class = MagicMock() mock_model = MagicMock() mock_claude_class.return_value = mock_model with patch.dict('sys.modules', {'agno.models.anthropic': MagicMock(Claude=mock_claude_class)}): result = agno_utils.create_model( model_type="ANTHROPIC", api_key="test-api-key", model_name="claude-3-opus", max_tokens=1500 ) mock_claude_class.assert_called_once_with( api_key="test-api-key", id="claude-3-opus", max_tokens=1500 ) assert result == mock_model def test_create_model_deepseek(self): """Test creating DeepSeek model""" with patch("agno.models.deepseek.DeepSeek") as mock_deepseek: mock_model = MagicMock() mock_deepseek.return_value = mock_model result = agno_utils.create_model( model_type="DEEPSEEK", api_key="test-api-key", model_name="deepseek-chat" ) mock_deepseek.assert_called_once_with( api_key="test-api-key", id="deepseek-chat", max_tokens=1000 ) assert result == mock_model def test_create_model_google(self): """Test creating Google Gemini model""" with patch("agno.models.google.Gemini") as mock_gemini: mock_model = MagicMock() mock_gemini.return_value = mock_model result = agno_utils.create_model( model_type="GOOGLE", api_key="test-api-key", model_name="gemini-pro" ) mock_gemini.assert_called_once_with( api_key="test-api-key", id="gemini-pro", max_output_tokens=1000 # Gemini's agno model uses max_output_tokens ) assert result == mock_model def test_create_model_googlevertex(self): """Test creating Google Vertex AI model (should trigger import error)""" with pytest.raises(HTTPException) as excinfo: agno_utils.create_model( model_type="GOOGLEVERTEX", api_key="test-api-key", model_name="gemini-pro-vertex" ) assert excinfo.value.status_code == 400 assert "is not available in this installation" in excinfo.value.detail def test_create_model_groq_with_response_format(self): """Test creating Groq model with response format (uses PatchedGroq)""" result = agno_utils.create_model( model_type="GROQ", api_key="test-api-key", model_name="mixtral-8x7b", response_format={"type": "json_object"} ) # Should return a PatchedGroq instance assert result.__class__.__name__ == "PatchedGroq" assert result.api_key == "test-api-key" assert result.id == "mixtral-8x7b" assert result.max_tokens == 1000 def test_create_model_groq_without_response_format(self): """Test creating Groq model without response format (uses PatchedGroq)""" result = agno_utils.create_model( model_type="GROQ", api_key="test-api-key", model_name="llama2-70b" ) # Should return a PatchedGroq instance assert result.__class__.__name__ == "PatchedGroq" assert result.api_key == "test-api-key" assert result.id == "llama2-70b" assert result.max_tokens == 1000 def test_patched_groq_response_format_tools_conflict(self): """Test that PatchedGroq handles response_format + tools conflict correctly""" result = agno_utils.create_model( model_type="GROQ", api_key="test-api-key", model_name="llama-3.3-70b-versatile" ) # Test with tools - should NOT include response_format params_with_tools = result.get_request_params( response_format={"type": "json_object"}, tools=[{"type": "function", "function": {"name": "test_tool"}}] ) assert "tools" in params_with_tools assert "response_format" not in params_with_tools # Test without tools - should include response_format params_without_tools = result.get_request_params( response_format={"type": "json_object"} ) assert "response_format" in params_without_tools assert "tools" not in params_without_tools def test_create_model_mistral(self): """Test creating Mistral model (mistralai is now a dependency)""" with patch("agno.models.mistral.MistralChat") as mock_mistral: mock_model = MagicMock() mock_mistral.return_value = mock_model result = agno_utils.create_model( model_type="MISTRAL", api_key="test-api-key", model_name="mistral-large-latest" ) mock_mistral.assert_called_once_with( api_key="test-api-key", id="mistral-large-latest", max_tokens=1000 ) assert result == mock_model def test_create_model_huggingface(self): """Test creating HuggingFace model""" with patch("agno.models.huggingface.HuggingFace") as mock_hf: mock_model = MagicMock() mock_hf.return_value = mock_model result = agno_utils.create_model( model_type="HUGGINGFACE", api_key="test-api-key", model_name="microsoft/DialoGPT-medium" ) mock_hf.assert_called_once_with( api_key="test-api-key", id="microsoft/DialoGPT-medium", max_tokens=1000 ) assert result == mock_model def test_create_model_ollama(self): """Test creating Ollama model (no API key needed)""" with patch("agno.models.ollama.Ollama") as mock_ollama: mock_model = MagicMock() mock_ollama.return_value = mock_model result = agno_utils.create_model( model_type="OLLAMA", api_key="not-needed", model_name="llama2" ) mock_ollama.assert_called_once_with(id="llama2") assert result == mock_model def test_create_model_xai(self): """Test creating xAI model""" with patch("agno.models.xai.xAI") as mock_xai: mock_model = MagicMock() mock_xai.return_value = mock_model result = agno_utils.create_model( model_type="XAI", api_key="test-api-key", model_name="grok-1" ) mock_xai.assert_called_once_with( api_key="test-api-key", id="grok-1", max_tokens=1000 ) assert result == mock_model def test_create_model_case_insensitive(self): """Test that model type is case insensitive""" with patch("app.utils.agno_utils.OpenAIChat") as mock_openai: mock_model = MagicMock() mock_openai.return_value = mock_model # Test lowercase result = agno_utils.create_model( model_type="openai", api_key="test-api-key", model_name="gpt-4" ) assert result == mock_model # Test mixed case mock_openai.reset_mock() result = agno_utils.create_model( model_type="OpenAI", api_key="test-api-key", model_name="gpt-4" ) assert result == mock_model def test_create_model_unsupported_type(self): """Test creating an unsupported model type""" with pytest.raises(HTTPException) as excinfo: agno_utils.create_model( model_type="UNSUPPORTED_MODEL", api_key="test-api-key", model_name="test-model" ) assert excinfo.value.status_code == 500 assert "Failed to initialize model" in excinfo.value.detail def test_create_model_import_error(self): """Test handling import errors""" # Mock the module to raise ImportError when Claude is accessed mock_module = MagicMock() mock_module.Claude.side_effect = ImportError("Module not found") with patch.dict('sys.modules', {'agno.models.anthropic': mock_module}): with pytest.raises(HTTPException) as excinfo: agno_utils.create_model( model_type="ANTHROPIC", api_key="test-api-key", model_name="claude-3" ) assert excinfo.value.status_code == 400 assert "is not available in this installation" in excinfo.value.detail def test_create_model_general_exception(self): """Test handling general exceptions during model creation""" with patch("app.utils.agno_utils.OpenAIChat", side_effect=ValueError("Invalid configuration")): with pytest.raises(HTTPException) as excinfo: agno_utils.create_model( model_type="OPENAI", api_key="test-api-key", model_name="gpt-4" ) assert excinfo.value.status_code == 500 assert "Failed to initialize model" in excinfo.value.detail class TestModelApiKey: """Test cases for the test_model_api_key function""" @pytest.mark.asyncio async def test_test_model_api_key_success(self): """Test successful API key validation""" with patch("app.utils.agno_utils.create_model") as mock_create_model, \ patch("app.utils.agno_utils.Agent") as mock_agent_class: # Setup mocks mock_model = MagicMock() mock_create_model.return_value = mock_model # Create an async mock for arun method mock_agent = MagicMock() mock_agent.arun = AsyncMock(return_value="Test successful.") mock_agent_class.return_value = mock_agent # Call the function result = await agno_utils.test_model_api_key("test-api-key", "OPENAI", "gpt-4") # Assert the function was called with correct parameters mock_create_model.assert_called_once_with("OPENAI", "test-api-key", "gpt-4", base_url=None) mock_agent_class.assert_called_once_with( name="Test Agent", model=mock_model, instructions="You are a test agent. Just respond with 'Test successful.'", debug_mode=False ) mock_agent.arun.assert_called_once_with(message="This is a test message.") assert result is True @pytest.mark.asyncio async def test_test_model_api_key_create_model_failure(self): """Test API key validation when model creation fails""" with patch("app.utils.agno_utils.create_model", side_effect=Exception("Invalid API key")): result = await agno_utils.test_model_api_key("invalid-api-key", "OPENAI", "gpt-4") assert result is False @pytest.mark.asyncio async def test_test_model_api_key_agent_creation_failure(self): """Test API key validation when agent creation fails""" with patch("app.utils.agno_utils.create_model") as mock_create_model, \ patch("app.utils.agno_utils.Agent", side_effect=Exception("Agent creation failed")): mock_model = MagicMock() mock_create_model.return_value = mock_model result = await agno_utils.test_model_api_key("test-api-key", "OPENAI", "gpt-4") assert result is False @pytest.mark.asyncio async def test_test_model_api_key_agent_run_failure(self): """Test API key validation when agent run fails""" with patch("app.utils.agno_utils.create_model") as mock_create_model, \ patch("app.utils.agno_utils.Agent") as mock_agent_class: # Setup mocks mock_model = MagicMock() mock_create_model.return_value = mock_model # Create an async mock that raises an exception mock_agent = MagicMock() mock_agent.arun = AsyncMock(side_effect=Exception("API request failed")) mock_agent_class.return_value = mock_agent # Call the function result = await agno_utils.test_model_api_key("invalid-api-key", "OPENAI", "gpt-4") # Assert result assert result is False @pytest.mark.asyncio async def test_test_model_api_key_different_model_types(self): """Test API key validation with different model types""" test_cases = [ ("ANTHROPIC", "claude-3"), ("GROQ", "mixtral-8x7b"), ("GOOGLE", "gemini-pro"), ("DEEPSEEK", "deepseek-chat") ] for model_type, model_name in test_cases: with patch("app.utils.agno_utils.create_model") as mock_create_model, \ patch("app.utils.agno_utils.Agent") as mock_agent_class: # Setup mocks mock_model = MagicMock() mock_create_model.return_value = mock_model mock_agent = MagicMock() mock_agent.arun = AsyncMock(return_value="Test successful.") mock_agent_class.return_value = mock_agent # Call the function result = await agno_utils.test_model_api_key("test-api-key", model_type, model_name) # Assert mock_create_model.assert_called_once_with(model_type, "test-api-key", model_name, base_url=None) assert result is True class TestEdgeCases: """Test edge cases and boundary conditions""" def test_create_model_empty_strings(self): """Test create_model with empty strings""" with patch("app.utils.agno_utils.OpenAIChat") as mock_openai: mock_model = MagicMock() mock_openai.return_value = mock_model result = agno_utils.create_model( model_type="OPENAI", api_key="", model_name="" ) mock_openai.assert_called_once_with( api_key="", id="", max_tokens=1000 ) assert result == mock_model def test_create_model_custom_max_tokens(self): """Test create_model with custom max_tokens""" with patch("app.utils.agno_utils.OpenAIChat") as mock_openai: mock_model = MagicMock() mock_openai.return_value = mock_model result = agno_utils.create_model( model_type="OPENAI", api_key="test-key", model_name="gpt-4", max_tokens=4000 ) mock_openai.assert_called_once_with( api_key="test-key", id="gpt-4", max_tokens=4000 ) assert result == mock_model def test_create_model_zero_max_tokens(self): """Test create_model with zero max_tokens""" with patch("app.utils.agno_utils.OpenAIChat") as mock_openai: mock_model = MagicMock() mock_openai.return_value = mock_model result = agno_utils.create_model( model_type="OPENAI", api_key="test-key", model_name="gpt-4", max_tokens=0 ) mock_openai.assert_called_once_with( api_key="test-key", id="gpt-4", max_tokens=0 ) assert result == mock_model @pytest.mark.asyncio async def test_test_model_api_key_empty_strings(self): """Test test_model_api_key with empty strings""" with patch("app.utils.agno_utils.create_model") as mock_create_model, \ patch("app.utils.agno_utils.Agent") as mock_agent_class: mock_model = MagicMock() mock_create_model.return_value = mock_model mock_agent = MagicMock() mock_agent.arun = AsyncMock(return_value="Test successful.") mock_agent_class.return_value = mock_agent result = await agno_utils.test_model_api_key("", "", "") mock_create_model.assert_called_once_with("", "", "", base_url=None) assert result is True