7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
356 lines
13 KiB
Python
356 lines
13 KiB
Python
"""
|
|
Test custom LangChain LLM integration with LDR.
|
|
|
|
This tests the integration of custom LangChain LLMs with Local Deep Research,
|
|
ensuring that users can provide their own LLM implementations.
|
|
"""
|
|
|
|
import os
|
|
import pytest
|
|
from unittest.mock import Mock, patch
|
|
from langchain_core.language_models.chat_models import BaseChatModel
|
|
from langchain_core.messages import AIMessage, BaseMessage
|
|
from langchain_core.outputs import ChatResult, ChatGeneration
|
|
from typing import Any, List, Optional
|
|
|
|
from local_deep_research.api.research_functions import (
|
|
quick_summary,
|
|
detailed_research,
|
|
)
|
|
|
|
|
|
class CustomTestLLM(BaseChatModel):
|
|
"""Custom Chat LLM for testing."""
|
|
|
|
@property
|
|
def _llm_type(self) -> str:
|
|
"""Return identifier of llm."""
|
|
return "custom_test_llm"
|
|
|
|
def _generate(
|
|
self,
|
|
messages: List[BaseMessage],
|
|
stop: Optional[List[str]] = None,
|
|
run_manager=None,
|
|
**kwargs: Any,
|
|
) -> ChatResult:
|
|
"""Generate a chat response based on message content."""
|
|
prompt = messages[-1].content if messages else ""
|
|
# Simple test response that varies based on prompt content
|
|
if "quantum" in prompt.lower():
|
|
response = "Quantum computing uses quantum bits (qubits) that can exist in superposition states."
|
|
elif "machine learning" in prompt.lower():
|
|
response = "Machine learning is a subset of AI that enables systems to learn from data."
|
|
elif "climate" in prompt.lower():
|
|
response = "Climate change is primarily driven by greenhouse gas emissions."
|
|
else:
|
|
response = f"This is a response from the custom LLM about: {prompt[:50]}..."
|
|
return ChatResult(
|
|
generations=[ChatGeneration(message=AIMessage(content=response))]
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CI") == "true"
|
|
or os.environ.get("GITHUB_ACTIONS") == "true",
|
|
reason="Langchain integration tests skipped in CI - testing advanced features",
|
|
)
|
|
class TestCustomLangChainLLM:
|
|
"""Test suite for custom LangChain LLM integration."""
|
|
|
|
@pytest.fixture
|
|
def mock_session(self):
|
|
"""Create a mock database session."""
|
|
session = Mock()
|
|
return session
|
|
|
|
@pytest.fixture
|
|
def settings_snapshot(self):
|
|
"""Create a settings snapshot for testing."""
|
|
return {
|
|
"llm.provider": {"value": "custom", "type": "str"},
|
|
"llm.model": {"value": "custom_test_llm", "type": "str"},
|
|
"llm.temperature": {"value": 0.7, "type": "float"},
|
|
"llm.custom.api_key": {"value": "test-key", "type": "str"},
|
|
"research.iterations": {"value": 2, "type": "int"},
|
|
"research.questions_per_iteration": {"value": 3, "type": "int"},
|
|
"research.search_engines": {"value": ["wikipedia"], "type": "list"},
|
|
"research.local_context": {"value": 2000, "type": "int"},
|
|
"research.web_context": {"value": 2000, "type": "int"},
|
|
"llm.context_window_unrestricted": {"value": False, "type": "bool"},
|
|
"llm.context_window_size": {"value": 8192, "type": "int"},
|
|
"llm.local_context_window_size": {"value": 4096, "type": "int"},
|
|
"llm.supports_max_tokens": {"value": True, "type": "bool"},
|
|
"llm.max_tokens": {"value": 4096, "type": "int"},
|
|
"rate_limiting.llm_enabled": {"value": False, "type": "bool"},
|
|
"search.tool": {"value": "wikipedia", "type": "str"},
|
|
"search.max_results": {"value": 10, "type": "int"},
|
|
"search.cross_engine_max_results": {"value": 100, "type": "int"},
|
|
"search.cross_engine_use_reddit": {"value": False, "type": "bool"},
|
|
"search.cross_engine_min_date": {"value": None, "type": "str"},
|
|
"search.region": {"value": "us", "type": "str"},
|
|
"search.time_period": {"value": "y", "type": "str"},
|
|
"search.safe_search": {"value": True, "type": "bool"},
|
|
"search.snippets_only": {"value": True, "type": "bool"},
|
|
"search.search_language": {"value": "English", "type": "str"},
|
|
"search.max_filtered_results": {"value": 20, "type": "int"},
|
|
}
|
|
|
|
def test_custom_llm_basic_usage(self, settings_snapshot):
|
|
"""Test basic usage of custom LLM with quick_summary."""
|
|
# Create custom LLM instance
|
|
custom_llm = CustomTestLLM()
|
|
|
|
# Mock the search results
|
|
with patch(
|
|
"local_deep_research.api.research_functions.get_search"
|
|
) as _mock_get_search:
|
|
mock_search_engine = Mock()
|
|
mock_search_engine.run.return_value = [
|
|
{
|
|
"url": "https://example.com/quantum",
|
|
"title": "Quantum Computing Basics",
|
|
"content": "Quantum computing is a revolutionary technology...",
|
|
"source": "wikipedia",
|
|
}
|
|
]
|
|
|
|
_mock_get_search.return_value = mock_search_engine
|
|
|
|
# Run quick summary with custom LLM
|
|
result = quick_summary(
|
|
query="What is quantum computing?",
|
|
research_id=12345,
|
|
llms={"custom": custom_llm},
|
|
settings_snapshot=settings_snapshot,
|
|
search_tool="wikipedia",
|
|
iterations=1,
|
|
questions_per_iteration=2,
|
|
)
|
|
|
|
# Verify results
|
|
assert result is not None
|
|
assert "research_id" in result
|
|
assert result["research_id"] == 12345
|
|
assert "summary" in result
|
|
assert "quantum" in result["summary"].lower()
|
|
assert "sources" in result
|
|
assert len(result["sources"]) > 0
|
|
|
|
def test_custom_llm_with_detailed_research(self, settings_snapshot):
|
|
"""Test custom LLM with detailed_research function."""
|
|
custom_llm = CustomTestLLM()
|
|
|
|
with patch(
|
|
"local_deep_research.api.research_functions.get_search"
|
|
) as mock_search:
|
|
# Mock multiple search results
|
|
mock_search_engine = Mock()
|
|
mock_search_engine.run.return_value = [
|
|
{
|
|
"url": "https://example.com/ml1",
|
|
"title": "Machine Learning Introduction",
|
|
"content": "Machine learning is transforming industries...",
|
|
"source": "wikipedia",
|
|
},
|
|
{
|
|
"url": "https://example.com/ml2",
|
|
"title": "ML Applications",
|
|
"content": "Applications of machine learning include...",
|
|
"source": "wikipedia",
|
|
},
|
|
]
|
|
|
|
mock_search.return_value = mock_search_engine
|
|
|
|
result = detailed_research(
|
|
query="Explain machine learning applications",
|
|
research_id="test-67890",
|
|
llms={"custom": custom_llm},
|
|
settings_snapshot=settings_snapshot,
|
|
search_tool="wikipedia",
|
|
iterations=2,
|
|
questions_per_iteration=3,
|
|
)
|
|
|
|
assert result is not None
|
|
assert result["research_id"] == "test-67890"
|
|
assert "machine learning" in result["summary"].lower()
|
|
assert len(result["sources"]) >= 2
|
|
assert "findings" in result
|
|
|
|
def test_custom_llm_with_custom_factory(self, settings_snapshot):
|
|
"""Test using a custom LLM factory function."""
|
|
|
|
def create_custom_llm(
|
|
model_name=None, temperature=None, settings_snapshot=None
|
|
):
|
|
"""Factory function for creating custom LLM."""
|
|
# Access settings from snapshot
|
|
api_key = settings_snapshot.get("llm.custom.api_key", {}).get(
|
|
"value"
|
|
)
|
|
assert api_key == "test-key" # Verify settings access
|
|
|
|
# Create and configure custom LLM
|
|
llm = CustomTestLLM()
|
|
# In real implementation, would use api_key and other settings
|
|
return llm
|
|
|
|
# Use factory to create LLM
|
|
custom_llm = create_custom_llm(
|
|
model_name="custom_test_llm",
|
|
temperature=0.7,
|
|
settings_snapshot=settings_snapshot,
|
|
)
|
|
|
|
with patch(
|
|
"local_deep_research.api.research_functions.get_search"
|
|
) as _mock_get_search:
|
|
mock_search_engine = Mock()
|
|
mock_search_engine.run.return_value = [
|
|
{
|
|
"url": "https://example.com/climate",
|
|
"title": "Climate Change Overview",
|
|
"content": "Climate change affects global weather patterns...",
|
|
"source": "wikipedia",
|
|
}
|
|
]
|
|
|
|
_mock_get_search.return_value = mock_search_engine
|
|
|
|
result = quick_summary(
|
|
query="Impact of climate change",
|
|
research_id="test-11111",
|
|
llms={"custom": custom_llm},
|
|
settings_snapshot=settings_snapshot,
|
|
search_tool="wikipedia",
|
|
iterations=1,
|
|
)
|
|
|
|
assert result["research_id"] == "test-11111"
|
|
assert "climate" in result["summary"].lower()
|
|
|
|
def test_custom_llm_error_handling(self, settings_snapshot):
|
|
"""Test error handling with custom LLM that raises errors."""
|
|
|
|
class FailingLLM(BaseChatModel):
|
|
"""LLM that raises errors for testing."""
|
|
|
|
@property
|
|
def _llm_type(self) -> str:
|
|
return "failing_llm"
|
|
|
|
def _generate(
|
|
self,
|
|
messages: List[BaseMessage],
|
|
stop=None,
|
|
run_manager=None,
|
|
**kwargs,
|
|
) -> ChatResult:
|
|
raise RuntimeError("LLM call failed")
|
|
|
|
failing_llm = FailingLLM()
|
|
|
|
# Override provider to match the "failing" LLM registration key
|
|
failing_settings = {
|
|
**settings_snapshot,
|
|
"llm.provider": {"value": "failing", "type": "str"},
|
|
}
|
|
|
|
with patch(
|
|
"local_deep_research.api.research_functions.get_search"
|
|
) as _mock_get_search:
|
|
mock_search_engine = Mock()
|
|
mock_search_engine.run.return_value = [
|
|
{
|
|
"url": "https://example.com/test",
|
|
"title": "Test Article",
|
|
"content": "Test content...",
|
|
"source": "wikipedia",
|
|
}
|
|
]
|
|
|
|
_mock_get_search.return_value = mock_search_engine
|
|
|
|
# The search strategy catches LLM errors gracefully,
|
|
# so the research completes but with degraded results
|
|
try:
|
|
result = quick_summary(
|
|
query="Test query",
|
|
llms={"failing": failing_llm},
|
|
settings_snapshot=failing_settings,
|
|
search_tool="wikipedia",
|
|
iterations=1,
|
|
)
|
|
# If it completes, verify we got some result back
|
|
assert result is not None
|
|
assert "summary" in result
|
|
except RuntimeError as e:
|
|
# If it does raise, verify it's the expected error
|
|
assert "LLM call failed" in str(e)
|
|
|
|
def test_custom_llm_streaming(self, settings_snapshot):
|
|
"""Test custom LLM with streaming support."""
|
|
|
|
class StreamingTestLLM(BaseChatModel):
|
|
"""Chat LLM with streaming support for testing."""
|
|
|
|
@property
|
|
def _llm_type(self) -> str:
|
|
return "streaming_test_llm"
|
|
|
|
def _generate(
|
|
self,
|
|
messages: List[BaseMessage],
|
|
stop=None,
|
|
run_manager=None,
|
|
**kwargs,
|
|
) -> ChatResult:
|
|
response = (
|
|
"This is a streaming response about quantum computing."
|
|
)
|
|
if run_manager:
|
|
for token in response.split():
|
|
run_manager.on_llm_new_token(token + " ")
|
|
return ChatResult(
|
|
generations=[
|
|
ChatGeneration(message=AIMessage(content=response))
|
|
]
|
|
)
|
|
|
|
streaming_llm = StreamingTestLLM()
|
|
|
|
# Override provider to match the "streaming" LLM registration key
|
|
streaming_settings = {
|
|
**settings_snapshot,
|
|
"llm.provider": {"value": "streaming", "type": "str"},
|
|
}
|
|
|
|
with patch(
|
|
"local_deep_research.api.research_functions.get_search"
|
|
) as _mock_get_search:
|
|
mock_search_engine = Mock()
|
|
mock_search_engine.run.return_value = [
|
|
{
|
|
"url": "https://example.com/quantum",
|
|
"title": "Quantum Info",
|
|
"content": "Quantum information...",
|
|
"source": "wikipedia",
|
|
}
|
|
]
|
|
|
|
_mock_get_search.return_value = mock_search_engine
|
|
|
|
result = quick_summary(
|
|
query="Quantum computing basics",
|
|
research_id="test-99999",
|
|
llms={"streaming": streaming_llm},
|
|
settings_snapshot=streaming_settings,
|
|
search_tool="wikipedia",
|
|
iterations=1,
|
|
)
|
|
|
|
assert result["research_id"] == "test-99999"
|
|
assert "streaming response" in result["summary"]
|