336 lines
10 KiB
Python
336 lines
10 KiB
Python
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
from pydantic import BaseModel
|
|
|
|
from ragas.llms.base import llm_factory
|
|
|
|
|
|
class LLMResponseModel(BaseModel):
|
|
response: str
|
|
|
|
|
|
class MockClient:
|
|
"""Mock client that simulates an LLM client."""
|
|
|
|
def __init__(self, is_async=False):
|
|
self.is_async = is_async
|
|
self.chat = Mock()
|
|
self.chat.completions = Mock()
|
|
self.messages = Mock()
|
|
self.messages.create = Mock()
|
|
if is_async:
|
|
|
|
async def async_create(*args, **kwargs):
|
|
return LLMResponseModel(response="Mock response")
|
|
|
|
self.chat.completions.create = async_create
|
|
self.messages.create = async_create
|
|
else:
|
|
|
|
def sync_create(*args, **kwargs):
|
|
return LLMResponseModel(response="Mock response")
|
|
|
|
self.chat.completions.create = sync_create
|
|
self.messages.create = sync_create
|
|
|
|
|
|
class MockInstructor:
|
|
"""Mock instructor client that wraps the base client."""
|
|
|
|
def __init__(self, client):
|
|
self.client = client
|
|
self.chat = Mock()
|
|
self.chat.completions = Mock()
|
|
|
|
if client.is_async:
|
|
# Async client - create a proper async function
|
|
async def async_create(*args, **kwargs):
|
|
return LLMResponseModel(response="Instructor response")
|
|
|
|
self.chat.completions.create = async_create
|
|
else:
|
|
# Sync client - create a regular function
|
|
def sync_create(*args, **kwargs):
|
|
return LLMResponseModel(response="Instructor response")
|
|
|
|
self.chat.completions.create = sync_create
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_sync_client():
|
|
"""Create a mock synchronous client."""
|
|
return MockClient(is_async=False)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_async_client():
|
|
"""Create a mock asynchronous client."""
|
|
return MockClient(is_async=True)
|
|
|
|
|
|
def test_llm_factory_initialization(mock_sync_client, monkeypatch):
|
|
"""Test llm_factory initialization."""
|
|
|
|
def mock_from_openai(client, mode=None):
|
|
return MockInstructor(client)
|
|
|
|
monkeypatch.setattr("instructor.from_openai", mock_from_openai)
|
|
|
|
llm = llm_factory("gpt-4", provider="openai", client=mock_sync_client)
|
|
|
|
assert llm.model == "gpt-4" # type: ignore
|
|
assert llm.client is not None # type: ignore
|
|
assert not llm.is_async # type: ignore
|
|
|
|
|
|
def test_llm_factory_async_detection(mock_async_client, monkeypatch):
|
|
"""Test that llm_factory correctly detects async clients."""
|
|
|
|
def mock_from_openai(client, mode=None):
|
|
return MockInstructor(client)
|
|
|
|
monkeypatch.setattr("instructor.from_openai", mock_from_openai)
|
|
|
|
llm = llm_factory("gpt-4", provider="openai", client=mock_async_client)
|
|
|
|
assert llm.is_async # type: ignore
|
|
|
|
|
|
def test_llm_factory_with_model_args(mock_sync_client, monkeypatch):
|
|
"""Test llm_factory with model arguments."""
|
|
|
|
def mock_from_openai(client, mode=None):
|
|
return MockInstructor(client)
|
|
|
|
monkeypatch.setattr("instructor.from_openai", mock_from_openai)
|
|
|
|
llm = llm_factory(
|
|
"gpt-4", provider="openai", client=mock_sync_client, temperature=0.7
|
|
)
|
|
|
|
assert llm.model == "gpt-4" # type: ignore
|
|
assert llm.model_args.get("temperature") == 0.7 # type: ignore
|
|
|
|
|
|
def test_unsupported_provider(monkeypatch):
|
|
"""Test that invalid clients are handled gracefully for unknown providers."""
|
|
mock_client = Mock()
|
|
mock_client.chat = None
|
|
mock_client.messages = None
|
|
|
|
with pytest.raises(ValueError, match="Failed to initialize"):
|
|
llm_factory("test-model", provider="unsupported", client=mock_client)
|
|
|
|
|
|
def test_sync_llm_generate(mock_sync_client, monkeypatch):
|
|
"""Test sync LLM generation."""
|
|
|
|
def mock_from_openai(client, mode=None):
|
|
return MockInstructor(client)
|
|
|
|
monkeypatch.setattr("instructor.from_openai", mock_from_openai)
|
|
|
|
llm = llm_factory("gpt-4", provider="openai", client=mock_sync_client)
|
|
|
|
result = llm.generate("Test prompt", LLMResponseModel)
|
|
|
|
assert isinstance(result, LLMResponseModel)
|
|
assert result.response == "Instructor response"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_llm_agenerate(mock_async_client, monkeypatch):
|
|
"""Test async LLM generation."""
|
|
|
|
def mock_from_openai(client, mode=None):
|
|
return MockInstructor(client)
|
|
|
|
monkeypatch.setattr("instructor.from_openai", mock_from_openai)
|
|
|
|
llm = llm_factory("gpt-4", provider="openai", client=mock_async_client)
|
|
|
|
result = await llm.agenerate("Test prompt", LLMResponseModel)
|
|
|
|
assert isinstance(result, LLMResponseModel)
|
|
assert result.response == "Instructor response"
|
|
|
|
|
|
def test_sync_client_agenerate_error(mock_sync_client, monkeypatch):
|
|
"""Test that using agenerate with sync client raises TypeError."""
|
|
|
|
def mock_from_openai(client, mode=None):
|
|
return MockInstructor(client)
|
|
|
|
monkeypatch.setattr("instructor.from_openai", mock_from_openai)
|
|
|
|
llm = llm_factory("gpt-4", provider="openai", client=mock_sync_client)
|
|
|
|
with pytest.raises(
|
|
TypeError, match="Cannot use agenerate\\(\\) with a synchronous client"
|
|
):
|
|
import asyncio
|
|
|
|
asyncio.run(llm.agenerate("Test prompt", LLMResponseModel))
|
|
|
|
|
|
def test_provider_support(monkeypatch):
|
|
"""Test that major providers are supported."""
|
|
import instructor
|
|
|
|
# Mock all provider-specific methods
|
|
def mock_from_openai(client, mode=None):
|
|
return MockInstructor(client)
|
|
|
|
def mock_from_anthropic(client):
|
|
return MockInstructor(client)
|
|
|
|
def mock_from_gemini(client):
|
|
return MockInstructor(client)
|
|
|
|
def mock_from_litellm(client, mode=None):
|
|
return MockInstructor(client)
|
|
|
|
# Use setattr with the module object directly to avoid attribute existence checks
|
|
monkeypatch.setattr(instructor, "from_openai", mock_from_openai, raising=False)
|
|
monkeypatch.setattr(
|
|
instructor, "from_anthropic", mock_from_anthropic, raising=False
|
|
)
|
|
monkeypatch.setattr(instructor, "from_gemini", mock_from_gemini, raising=False)
|
|
monkeypatch.setattr(instructor, "from_litellm", mock_from_litellm, raising=False)
|
|
|
|
# Test all major providers
|
|
for provider in ["openai", "anthropic", "google", "gemini", "litellm"]:
|
|
mock_client = MockClient(is_async=False)
|
|
llm = llm_factory("test-model", provider=provider, client=mock_client)
|
|
assert llm.model == "test-model" # type: ignore
|
|
|
|
|
|
def test_llm_model_args_storage(mock_sync_client, monkeypatch):
|
|
"""Test that model arguments are properly stored."""
|
|
|
|
def mock_from_openai(client, mode=None):
|
|
return MockInstructor(client)
|
|
|
|
monkeypatch.setattr("instructor.from_openai", mock_from_openai)
|
|
|
|
model_args = {"temperature": 0.7, "max_tokens": 1000, "top_p": 0.9}
|
|
|
|
llm = llm_factory("gpt-4", provider="openai", client=mock_sync_client, **model_args)
|
|
|
|
assert llm.model_args == model_args # type: ignore
|
|
|
|
|
|
def test_llm_factory_missing_client():
|
|
"""Test that missing client raises ValueError."""
|
|
with pytest.raises(ValueError, match="requires a client instance"):
|
|
llm_factory("gpt-4", provider="openai")
|
|
|
|
|
|
def test_llm_factory_missing_model():
|
|
"""Test that missing model raises ValueError."""
|
|
mock_client = Mock()
|
|
|
|
with pytest.raises(ValueError, match="model parameter is required"):
|
|
llm_factory("", provider="openai", client=mock_client)
|
|
|
|
|
|
def test_openai_compatible_providers_with_openai_client(monkeypatch):
|
|
"""
|
|
Test that OpenAI-compatible providers (DeepSeek, Groq, Mistral, etc.)
|
|
work correctly with OpenAI SDK clients.
|
|
|
|
This tests the fix for issue #2560 where provider="deepseek" with
|
|
AsyncOpenAI client was failing with "'AsyncOpenAI' object has no attribute 'messages'"
|
|
"""
|
|
|
|
def mock_from_openai(client, mode=None):
|
|
return MockInstructor(client)
|
|
|
|
monkeypatch.setattr("instructor.from_openai", mock_from_openai)
|
|
|
|
# Test OpenAI-compatible providers that use chat.completions.create
|
|
openai_compatible_providers = ["deepseek", "groq", "mistral", "cohere", "xai"]
|
|
|
|
for provider in openai_compatible_providers:
|
|
# Create a mock client with OpenAI-style API (chat.completions.create)
|
|
mock_client = MockClient(is_async=True)
|
|
# Remove messages attribute to simulate OpenAI client
|
|
delattr(mock_client, "messages")
|
|
|
|
# This should work now - it detects chat.completions.create and uses from_openai
|
|
llm = llm_factory("test-model", provider=provider, client=mock_client)
|
|
|
|
assert llm.model == "test-model"
|
|
assert llm.is_async
|
|
|
|
|
|
def test_llm_factory_with_custom_mode(mock_sync_client, monkeypatch):
|
|
"""Test that llm_factory accepts and uses custom instructor mode."""
|
|
import instructor
|
|
|
|
captured_mode = None
|
|
|
|
def mock_from_openai(client, mode=None):
|
|
nonlocal captured_mode
|
|
captured_mode = mode
|
|
return MockInstructor(client)
|
|
|
|
monkeypatch.setattr("instructor.from_openai", mock_from_openai)
|
|
|
|
llm = llm_factory(
|
|
"gpt-4",
|
|
provider="openai",
|
|
client=mock_sync_client,
|
|
mode=instructor.Mode.MD_JSON,
|
|
)
|
|
|
|
assert llm.model == "gpt-4"
|
|
assert captured_mode == instructor.Mode.MD_JSON
|
|
|
|
|
|
def test_llm_factory_default_mode_is_json(mock_sync_client, monkeypatch):
|
|
"""Test that llm_factory defaults to Mode.JSON when no mode is specified."""
|
|
import instructor
|
|
|
|
captured_mode = None
|
|
|
|
def mock_from_openai(client, mode=None):
|
|
nonlocal captured_mode
|
|
captured_mode = mode
|
|
return MockInstructor(client)
|
|
|
|
monkeypatch.setattr("instructor.from_openai", mock_from_openai)
|
|
|
|
llm = llm_factory("gpt-4", provider="openai", client=mock_sync_client)
|
|
|
|
assert llm.model == "gpt-4"
|
|
assert captured_mode == instructor.Mode.JSON
|
|
|
|
|
|
def test_llm_factory_mode_with_generic_provider(monkeypatch):
|
|
"""Test that mode parameter works with generic providers via _patch_client_for_provider."""
|
|
import instructor
|
|
|
|
captured_mode = None
|
|
|
|
def mock_from_openai(client, mode=None):
|
|
nonlocal captured_mode
|
|
captured_mode = mode
|
|
return MockInstructor(client)
|
|
|
|
monkeypatch.setattr("instructor.from_openai", mock_from_openai)
|
|
|
|
mock_client = MockClient(is_async=False)
|
|
delattr(mock_client, "messages")
|
|
|
|
llm = llm_factory(
|
|
"custom-model",
|
|
provider="custom-provider",
|
|
client=mock_client,
|
|
mode=instructor.Mode.TOOLS,
|
|
)
|
|
|
|
assert llm.model == "custom-model"
|
|
assert captured_mode == instructor.Mode.TOOLS
|