* Update styleUtils.ts * update to prompt component * update to template * update to mcp component * update to smart function * [autofix.ci] apply automated fixes * update to templates * fix sidebar * change name * update import * update import * update import * [autofix.ci] apply automated fixes * fix import * fix ollama * fix ruff * refactor(agent): standardize memory handling and update chat history logic (#8715) * update chat history * update to agents * Update Simple Agent.json * update to templates * ruff errors * Update agent.py * Update test_agent_component.py * [autofix.ci] apply automated fixes * update templates * test fix --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Mike Fortman <michael.fortman@datastax.com> * fix prompt change * feat(message): support sequencing of multiple streamable models (#8434) * feat: update OpenAI model parameters handling for reasoning models * feat: extend input_value type in LCModelComponent to support AsyncIterator and Iterator * refactor: remove assert_streaming_sequence method and related checks from Graph class * feat: add consume_iterator method to Message class for handling iterators * test: add unit tests for OpenAIModelComponent functionality and integration * feat: update OpenAIModelComponent to include temperature and seed parameters in build_model method * feat: rename consume_iterator method to consume_iterator_in_text and update its implementation for handling text * feat: add is_connected_to_chat_output method to Component class for improved message handling * feat: refactor LCModelComponent methods to support asynchronous message handling and improve chat output integration * refactor: remove consume_iterator_in_text method from Message class and clean up LCModelComponent input handling * fix: update import paths for input components in multiple starter project JSON files * fix: enhance error message formatting in ErrorMessage class to handle additional exception attributes * refactor: remove validate_stream calls from generate_flow_events and Graph class to streamline flow processing * fix: handle asyncio.CancelledError in aadd_messagetables to ensure proper session rollback and retry logic * refactor: streamline message handling in LCModelComponent by replacing async invocation with synchronous calls and updating message text handling * refactor: enhance message handling in LCModelComponent by introducing lf_message for improved return value management and updating properties for consistency * feat: add _build_source method to Component class for enhanced source handling and flexibility in source object management * feat: enhance LCModelComponent by adding _handle_stream method for improved streaming response handling and refactoring chat output integration * feat: update MemoryComponent to enhance message retrieval and storage functionality, including new sender type handling and output options for text and dataframe formats * test: refactor LanguageModelComponent tests to use ComponentTestBaseWithoutClient and add tests for Google model creation and error handling * test: add fixtures for API keys and implement live API tests for OpenAI, Anthropic, and Google models * fix: reorder JSON properties for consistency in starter projects * Updated JSON files for various starter projects to ensure consistent ordering of properties, specifically moving "type" to follow "selected_output" for better readability and maintainability. * Affected files: Basic Prompt Chaining.json, Blog Writer.json, Financial Report Parser.json, Hybrid Search RAG.json, SEO Keyword Generator.json. * refactor: simplify input_value type in LCModelComponent * Updated the input_value parameter in LCModelComponent to remove AsyncIterator and Iterator types, streamlining the input options to only str and Message for improved clarity and maintainability. * This change enhances the documentation and understanding of the expected input types for the component. * fix: clarify comment for handling source in Component class * refactor: remove unnecessary mocking in OpenAI model integration tests * auto update * update * [autofix.ci] apply automated fixes * fix openai import * revert template changes * test fixes * update templates * [autofix.ci] apply automated fixes * fix tests * fix order * fix prompts import * fix frontend tests * fix frontend * [autofix.ci] apply automated fixes * add charmander * [autofix.ci] apply automated fixes * fix prompt frontend * fix frontend * test fix * [autofix.ci] apply automated fixes * change pokedex * remove pokedex extra * update template * name fix * update template * mcp test fix --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: cristhianzl <cristhian.lousa@gmail.com> Co-authored-by: Yuqi Tang <yuqi.tang@datastax.com> Co-authored-by: Mike Fortman <michael.fortman@datastax.com> Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
183 lines
7.1 KiB
Python
183 lines
7.1 KiB
Python
import asyncio
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from langflow.components.agents.mcp_component import MCPSseClient, MCPStdioClient, MCPToolsComponent
|
|
|
|
from tests.base import ComponentTestBaseWithoutClient, VersionComponentMapping
|
|
|
|
# TODO: This test suite is incomplete and is in need of an update to handle the latest MCP component changes.
|
|
pytestmark = pytest.mark.skip(reason="Skipping entire file")
|
|
|
|
|
|
class TestMCPToolsComponent(ComponentTestBaseWithoutClient):
|
|
@pytest.fixture
|
|
def component_class(self):
|
|
"""Return the component class to test."""
|
|
return MCPToolsComponent
|
|
|
|
@pytest.fixture
|
|
def default_kwargs(self):
|
|
"""Return the default kwargs for the component."""
|
|
return {
|
|
"mode": "Stdio",
|
|
"command": "uvx mcp-server-fetch",
|
|
"sse_url": "http://localhost:7860/api/v1/mcp/sse",
|
|
"tool": "",
|
|
}
|
|
|
|
@pytest.fixture
|
|
def file_names_mapping(self) -> list[VersionComponentMapping]:
|
|
"""Return the file names mapping for different versions."""
|
|
return []
|
|
|
|
@pytest.fixture
|
|
def mock_tool(self):
|
|
"""Create a mock MCP tool."""
|
|
tool = MagicMock()
|
|
tool.name = "test_tool"
|
|
tool.description = "Test tool description"
|
|
tool.inputSchema = {
|
|
"type": "object",
|
|
"properties": {"test_param": {"type": "string", "description": "Test parameter"}},
|
|
}
|
|
return tool
|
|
|
|
@pytest.fixture
|
|
def mock_stdio_client(self, mock_tool):
|
|
"""Create a mock stdio client."""
|
|
stdio_client = AsyncMock()
|
|
stdio_client.connect_to_server = AsyncMock(return_value=[mock_tool])
|
|
stdio_client.session = AsyncMock()
|
|
return stdio_client
|
|
|
|
@pytest.fixture
|
|
def mock_sse_client(self, mock_tool):
|
|
"""Create a mock SSE client."""
|
|
sse_client = AsyncMock()
|
|
sse_client.connect_to_server = AsyncMock(return_value=[mock_tool])
|
|
sse_client.session = AsyncMock()
|
|
return sse_client
|
|
|
|
|
|
class TestMCPStdioClient:
|
|
@pytest.fixture
|
|
def stdio_client(self):
|
|
return MCPStdioClient()
|
|
|
|
async def test_connect_to_server(self, stdio_client):
|
|
"""Test connecting to server via Stdio."""
|
|
# Create mock for stdio transport
|
|
mock_stdio = AsyncMock()
|
|
mock_write = AsyncMock()
|
|
mock_stdio_transport = (mock_stdio, mock_write)
|
|
mock_stdio_cm = AsyncMock()
|
|
mock_stdio_cm.__aenter__.return_value = mock_stdio_transport
|
|
|
|
# Mock the stdio_client function to return our mock context manager
|
|
with patch("mcp.client.stdio.stdio_client", return_value=mock_stdio_cm):
|
|
# Mock ClientSession
|
|
mock_session = AsyncMock()
|
|
mock_session.initialize = AsyncMock()
|
|
mock_session.list_tools.return_value.tools = [MagicMock()]
|
|
|
|
# Mock the AsyncExitStack
|
|
mock_exit_stack = AsyncMock()
|
|
mock_exit_stack.enter_async_context = AsyncMock()
|
|
mock_exit_stack.enter_async_context.side_effect = [
|
|
mock_stdio_transport, # For stdio_client
|
|
mock_session, # For ClientSession
|
|
]
|
|
stdio_client.exit_stack = mock_exit_stack
|
|
|
|
tools = await stdio_client.connect_to_server("test_command")
|
|
|
|
assert len(tools) == 1
|
|
assert stdio_client.session is not None
|
|
# Verify the exit stack was used correctly
|
|
assert mock_exit_stack.enter_async_context.call_count == 2
|
|
# Verify the stdio transport was properly set
|
|
assert stdio_client.stdio == mock_stdio
|
|
assert stdio_client.write == mock_write
|
|
|
|
|
|
class TestMCPSseClient:
|
|
@pytest.fixture
|
|
def sse_client(self):
|
|
return MCPSseClient()
|
|
|
|
async def test_pre_check_redirect(self, sse_client):
|
|
"""Test pre-checking URL for redirects."""
|
|
test_url = "http://test.url"
|
|
redirect_url = "http://redirect.url"
|
|
|
|
with patch("httpx.AsyncClient") as mock_client:
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 307
|
|
mock_response.headers.get.return_value = redirect_url
|
|
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
|
|
|
result = await sse_client.pre_check_redirect(test_url)
|
|
assert result == redirect_url
|
|
|
|
async def test_connect_to_server(self, sse_client):
|
|
"""Test connecting to server via SSE."""
|
|
# Mock the pre_check_redirect first
|
|
with (
|
|
patch.object(sse_client, "pre_check_redirect", return_value="http://test.url"),
|
|
patch.object(sse_client, "validate_url", return_value=(True, "")),
|
|
):
|
|
# Create mock for sse_client context manager
|
|
mock_sse = AsyncMock()
|
|
mock_write = AsyncMock()
|
|
mock_sse_transport = (mock_sse, mock_write)
|
|
mock_sse_cm = AsyncMock()
|
|
mock_sse_cm.__aenter__.return_value = mock_sse_transport
|
|
|
|
# Mock the sse_client function to return our mock context manager
|
|
with patch("mcp.client.sse.sse_client", return_value=mock_sse_cm):
|
|
# Mock ClientSession
|
|
mock_session = AsyncMock()
|
|
mock_session.initialize = AsyncMock()
|
|
mock_session.list_tools.return_value.tools = [MagicMock()]
|
|
|
|
# Mock the AsyncExitStack
|
|
mock_exit_stack = AsyncMock()
|
|
mock_exit_stack.enter_async_context = AsyncMock()
|
|
mock_exit_stack.enter_async_context.side_effect = [
|
|
mock_sse_transport, # For sse_client
|
|
mock_session, # For ClientSession
|
|
]
|
|
sse_client.exit_stack = mock_exit_stack
|
|
|
|
tools = await sse_client.connect_to_server("http://test.url", {})
|
|
|
|
assert len(tools) == 1
|
|
assert sse_client.session is not None
|
|
# Verify the exit stack was used correctly
|
|
assert mock_exit_stack.enter_async_context.call_count == 2
|
|
# Verify the SSE transport was properly set
|
|
assert sse_client.sse == mock_sse
|
|
assert sse_client.write == mock_write
|
|
|
|
async def test_connect_timeout(self, sse_client):
|
|
"""Test connection timeout handling."""
|
|
# Set max_retries to 1 to avoid multiple retry attempts
|
|
sse_client.max_retries = 1
|
|
|
|
with (
|
|
patch.object(sse_client, "pre_check_redirect", return_value="http://test.url"),
|
|
patch.object(sse_client, "validate_url", return_value=(True, "")), # Mock URL validation
|
|
patch.object(sse_client, "_connect_with_timeout") as mock_connect,
|
|
):
|
|
mock_connect.side_effect = asyncio.TimeoutError()
|
|
|
|
# Expect ConnectionError instead of TimeoutError
|
|
with pytest.raises(
|
|
ConnectionError,
|
|
match=(
|
|
"Failed to connect after 1 attempts. "
|
|
"Last error: Connection to http://test.url timed out after 1 seconds"
|
|
),
|
|
):
|
|
await sse_client.connect_to_server("http://test.url", {}, timeout_seconds=1)
|