Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

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"]