From 85d575e2d570f94553d4ac3a3453604ae7607bad Mon Sep 17 00:00:00 2001 From: David Gardner Date: Wed, 2 Oct 2024 09:07:13 -0700 Subject: [PATCH 01/10] WIP --- python/morpheus_llm/morpheus_llm/error.py | 18 ++++++ .../llm/nodes/langchain_agent_node.py | 13 +++- .../llm/services/nemo_llm_service.py | 7 +-- tests/benchmarks/conftest.py | 9 ++- .../test_bench_agents_simple_pipeline.py | 16 +++-- tests/conftest.py | 62 +++++++++++++------ tests/llm/conftest.py | 49 +++++++++++++++ tests/llm/nodes/test_langchain_agent_node.py | 57 +++++++++++++---- tests/llm/services/conftest.py | 8 +-- tests/llm/test_agents_simple_pipe.py | 13 ++-- 10 files changed, 196 insertions(+), 56 deletions(-) create mode 100644 python/morpheus_llm/morpheus_llm/error.py diff --git a/python/morpheus_llm/morpheus_llm/error.py b/python/morpheus_llm/morpheus_llm/error.py new file mode 100644 index 0000000000..2505d987dd --- /dev/null +++ b/python/morpheus_llm/morpheus_llm/error.py @@ -0,0 +1,18 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +# +# 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_ERROR_MESSAGE = ( + "{package} not found. Install it and other additional dependencies by running the following command:\n" + "`conda env update --solver=libmamba -n morpheus " + "--file conda/environments/examples_cuda-121_arch-x86_64.yaml`") diff --git a/python/morpheus_llm/morpheus_llm/llm/nodes/langchain_agent_node.py b/python/morpheus_llm/morpheus_llm/llm/nodes/langchain_agent_node.py index e63b1d351c..617a5e2892 100644 --- a/python/morpheus_llm/morpheus_llm/llm/nodes/langchain_agent_node.py +++ b/python/morpheus_llm/morpheus_llm/llm/nodes/langchain_agent_node.py @@ -16,13 +16,19 @@ import logging import typing -from langchain_core.exceptions import OutputParserException - +from morpheus_llm.error import IMPORT_ERROR_MESSAGE from morpheus_llm.llm import LLMContext from morpheus_llm.llm import LLMNodeBase logger = logging.getLogger(__name__) +IMPORT_EXCEPTION = None + +try: + from langchain_core.exceptions import OutputParserException +except ImportError as e: + IMPORT_EXCEPTION = e + if typing.TYPE_CHECKING: from langchain.agents import AgentExecutor @@ -47,6 +53,9 @@ def __init__(self, agent_executor: "AgentExecutor", replace_exceptions: bool = False, replace_exceptions_value: typing.Optional[str] = None): + if IMPORT_EXCEPTION is not None: + raise ImportError(IMPORT_ERROR_MESSAGE.format('langchain_core')) from IMPORT_EXCEPTION + super().__init__() self._agent_executor = agent_executor diff --git a/python/morpheus_llm/morpheus_llm/llm/services/nemo_llm_service.py b/python/morpheus_llm/morpheus_llm/llm/services/nemo_llm_service.py index ef80814929..293dff12a1 100644 --- a/python/morpheus_llm/morpheus_llm/llm/services/nemo_llm_service.py +++ b/python/morpheus_llm/morpheus_llm/llm/services/nemo_llm_service.py @@ -18,16 +18,13 @@ import warnings from morpheus.utils.env_config_value import EnvConfigValue +from morpheus_llm.error import IMPORT_ERROR_MESSAGE from morpheus_llm.llm.services.llm_service import LLMClient from morpheus_llm.llm.services.llm_service import LLMService logger = logging.getLogger(__name__) IMPORT_EXCEPTION = None -IMPORT_ERROR_MESSAGE = ( - "NemoLLM not found. Install it and other additional dependencies by running the following command:\n" - "`conda env update --solver=libmamba -n morpheus " - "--file conda/environments/examples_cuda-121_arch-x86_64.yaml --prune`") try: import nemollm @@ -53,7 +50,7 @@ class NeMoLLMClient(LLMClient): def __init__(self, parent: "NeMoLLMService", *, model_name: str, **model_kwargs) -> None: if IMPORT_EXCEPTION is not None: - raise ImportError(IMPORT_ERROR_MESSAGE) from IMPORT_EXCEPTION + raise ImportError(IMPORT_ERROR_MESSAGE.format(package='nemollm')) from IMPORT_EXCEPTION super().__init__() diff --git a/tests/benchmarks/conftest.py b/tests/benchmarks/conftest.py index 1e21affaa8..607febf434 100644 --- a/tests/benchmarks/conftest.py +++ b/tests/benchmarks/conftest.py @@ -20,8 +20,13 @@ from unittest import mock import pytest -from pynvml.smi import NVSMI_QUERY_GPU -from pynvml.smi import nvidia_smi + +try: + from pynvml.smi import NVSMI_QUERY_GPU + from pynvml.smi import nvidia_smi +except ImportError: + print("pynvml is not installed") + from test_bench_e2e_pipelines import E2E_TEST_CONFIGS diff --git a/tests/benchmarks/test_bench_agents_simple_pipeline.py b/tests/benchmarks/test_bench_agents_simple_pipeline.py index 85202fdc02..cbd83e3cae 100644 --- a/tests/benchmarks/test_bench_agents_simple_pipeline.py +++ b/tests/benchmarks/test_bench_agents_simple_pipeline.py @@ -19,13 +19,17 @@ import typing from unittest import mock -import langchain import pytest -from langchain.agents import AgentType -from langchain.agents import initialize_agent -from langchain.agents import load_tools -from langchain.agents.tools import Tool -from langchain.utilities import serpapi + +try: + import langchain + from langchain.agents import AgentType + from langchain.agents import initialize_agent + from langchain.agents import load_tools + from langchain.agents.tools import Tool + from langchain.utilities import serpapi +except ImportError: + print("langchain is not installed") import cudf diff --git a/tests/conftest.py b/tests/conftest.py index 952142f249..84f894f707 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,6 +47,10 @@ from _utils.kafka import kafka_server # noqa: F401 pylint:disable=unused-import from _utils.kafka import zookeeper_proc # noqa: F401 pylint:disable=unused-import +OPT_DEP_SKIP_REASON = ( + "This test requires the {package} package to be installed, to install this run:\n" + "`conda env update --solver=libmamba -n morpheus --file conda/environments/examples_cuda-121_arch-x86_64.yaml`") + def pytest_addoption(parser: pytest.Parser): """ @@ -1064,33 +1068,53 @@ def nemollm_fixture(fail_missing: bool): """ Fixture to ensure nemollm is installed """ - skip_reason = ("Tests for the NeMoLLMService require the nemollm package to be installed, to install this run:\n" - "`conda env update --solver=libmamba -n morpheus " - "--file conda/environments/all_cuda-121_arch-x86_64.yaml --prune`") - yield import_or_skip("nemollm", reason=skip_reason, fail_missing=fail_missing) + yield import_or_skip("nemollm", reason=OPT_DEP_SKIP_REASON.format(package="nemollm"), fail_missing=fail_missing) -@pytest.fixture(name="nvfoundationllm", scope='session') -def nvfoundationllm_fixture(fail_missing: bool): +@pytest.fixture(name="openai", scope='session') +def openai_fixture(fail_missing: bool): """ - Fixture to ensure nvfoundationllm is installed + Fixture to ensure openai is installed """ - skip_reason = ( - "Tests for NVFoundation require the langchain-nvidia-ai-endpoints package to be installed, to install this " - "run:\n `conda env update --solver=libmamba -n morpheus " - "--file conda/environments/all_cuda-121_arch-x86_64.yaml --prune`") - yield import_or_skip("langchain_nvidia_ai_endpoints", reason=skip_reason, fail_missing=fail_missing) + yield import_or_skip("openai", reason=OPT_DEP_SKIP_REASON.format(package="openai"), fail_missing=fail_missing) -@pytest.fixture(name="openai", scope='session') -def openai_fixture(fail_missing: bool): +@pytest.fixture(name="langchain", scope='session') +def langchain_fixture(fail_missing: bool): """ - Fixture to ensure openai is installed + Fixture to ensure langchain is installed + """ + yield import_or_skip("langchain", reason=OPT_DEP_SKIP_REASON.format(package="langchain"), fail_missing=fail_missing) + + +@pytest.fixture(name="langchain_core", scope='session') +def langchain_core_fixture(fail_missing: bool): + """ + Fixture to ensure langchain_core is installed + """ + yield import_or_skip("langchain_core", + reason=OPT_DEP_SKIP_REASON.format(package="langchain_core"), + fail_missing=fail_missing) + + +@pytest.fixture(name="langchain_community", scope='session') +def langchain_community_fixture(fail_missing: bool): + """ + Fixture to ensure langchain_community is installed + """ + yield import_or_skip("langchain_community", + reason=OPT_DEP_SKIP_REASON.format(package="langchain_community"), + fail_missing=fail_missing) + + +@pytest.fixture(name="langchain_nvidia_ai_endpoints", scope='session') +def langchain_nvidia_ai_endpoints_fixture(fail_missing: bool): + """ + Fixture to ensure langchain_nvidia_ai_endpoints is installed """ - skip_reason = ("Tests for the OpenAIChatService require the openai package to be installed, to install this run:\n" - "`conda env update --solver=libmamba -n morpheus " - "--file conda/environments/all_cuda-121_arch-x86_64.yaml --prune`") - yield import_or_skip("openai", reason=skip_reason, fail_missing=fail_missing) + yield import_or_skip("langchain_nvidia_ai_endpoints", + reason=OPT_DEP_SKIP_REASON.format(package="langchain_nvidia_ai_endpoints"), + fail_missing=fail_missing) @pytest.mark.usefixtures("openai") diff --git a/tests/llm/conftest.py b/tests/llm/conftest.py index 3519166635..25dc206eed 100644 --- a/tests/llm/conftest.py +++ b/tests/llm/conftest.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import types from unittest import mock import pytest @@ -20,6 +21,54 @@ from _utils import require_env_variable +@pytest.fixture(name="nemollm", scope='session', autouse=True) +def nemollm_fixture(nemollm: types.ModuleType, fail_missing: bool): + """ + Fixture to ensure nemollm is installed + """ + yield nemollm + + +@pytest.fixture(name="openai", scope='session', autouse=True) +def openai_fixture(openai: types.ModuleType, fail_missing: bool): + """ + Fixture to ensure openai is installed + """ + yield openai + + +@pytest.fixture(name="langchain", scope='session', autouse=True) +def langchain_fixture(langchain: types.ModuleType, fail_missing: bool): + """ + Fixture to ensure langchain is installed + """ + yield langchain + + +@pytest.fixture(name="langchain_core", scope='session', autouse=True) +def langchain_core_fixture(langchain_core: types.ModuleType, fail_missing: bool): + """ + Fixture to ensure langchain_core is installed + """ + yield langchain_core + + +@pytest.fixture(name="langchain_community", scope='session', autouse=True) +def langchain_community_fixture(langchain_community: types.ModuleType, fail_missing: bool): + """ + Fixture to ensure langchain_community is installed + """ + yield langchain_community + + +@pytest.fixture(name="langchain_nvidia_ai_endpoints", scope='session', autouse=True) +def langchain_nvidia_ai_endpoints_fixture(langchain_nvidia_ai_endpoints: types.ModuleType, fail_missing: bool): + """ + Fixture to ensure langchain_nvidia_ai_endpoints is installed + """ + yield langchain_nvidia_ai_endpoints + + @pytest.fixture(name="countries") def countries_fixture(): yield [ diff --git a/tests/llm/nodes/test_langchain_agent_node.py b/tests/llm/nodes/test_langchain_agent_node.py index 033e978402..a8c41110cc 100644 --- a/tests/llm/nodes/test_langchain_agent_node.py +++ b/tests/llm/nodes/test_langchain_agent_node.py @@ -19,14 +19,6 @@ from unittest import mock import pytest -from langchain.agents import AgentType -from langchain.agents import Tool -from langchain.agents import initialize_agent -from langchain.callbacks.manager import AsyncCallbackManagerForToolRun -from langchain.callbacks.manager import CallbackManagerForToolRun -from langchain_community.chat_models.openai import ChatOpenAI -from langchain_core.exceptions import OutputParserException -from langchain_core.tools import BaseTool from _utils.llm import execute_node from _utils.llm import mk_mock_langchain_tool @@ -34,6 +26,19 @@ from morpheus_llm.llm import LLMNodeBase from morpheus_llm.llm.nodes.langchain_agent_node import LangChainAgentNode +if typing.TYPE_CHECKING: + from langchain.callbacks.manager import AsyncCallbackManagerForToolRun + from langchain.callbacks.manager import CallbackManagerForToolRun + + +class OutputParserExceptionStandin(Exception): + """ + Stand-in for the OutputParserException class to avoid importing the actual class from the langchain_core.exceptions. + There is a need to have OutputParserException objects appear in test parameters, but we don't want to import + langchain_core at the top of the test as it is an optional dependency. + """ + pass + def test_constructor(mock_agent_executor: mock.MagicMock): node = LangChainAgentNode(agent_executor=mock_agent_executor) @@ -76,6 +81,11 @@ def test_execute( def test_execute_tools(mock_chat_completion: tuple[mock.MagicMock, mock.MagicMock]): + from langchain.agents import AgentType + from langchain.agents import Tool + from langchain.agents import initialize_agent + from langchain_community.chat_models.openai import ChatOpenAI + # Tests the execute method of the LangChainAgentNode with a a mocked tools and chat completion (_, mock_async_client) = mock_chat_completion chat_responses = [ @@ -118,6 +128,11 @@ def test_execute_tools(mock_chat_completion: tuple[mock.MagicMock, mock.MagicMoc def test_execute_error(mock_chat_completion: tuple[mock.MagicMock, mock.MagicMock]): + from langchain.agents import AgentType + from langchain.agents import Tool + from langchain.agents import initialize_agent + from langchain_community.chat_models.openai import ChatOpenAI + # Tests the execute method of the LangChainAgentNode with a a mocked tools and chat completion (_, mock_async_client) = mock_chat_completion chat_responses = [ @@ -167,14 +182,14 @@ class MetadataSaverTool(BaseTool): def _run( self, query: str, - run_manager: typing.Optional[CallbackManagerForToolRun] = None, + run_manager: typing.Optional["CallbackManagerForToolRun"] = None, ) -> str: raise NotImplementedError("This tool only supports async") async def _arun( self, query: str, - run_manager: typing.Optional[AsyncCallbackManagerForToolRun] = None, + run_manager: typing.Optional["AsyncCallbackManagerForToolRun"] = None, ) -> str: assert query is not None # avoiding unused-argument assert run_manager is not None @@ -192,6 +207,10 @@ async def _arun( }], ids=["single-metadata", "single-metadata-list", "multiple-metadata-list"]) def test_metadata(mock_chat_completion: tuple[mock.MagicMock, mock.MagicMock], metadata: dict): + from langchain.agents import AgentType + from langchain.agents import initialize_agent + from langchain_community.chat_models.openai import ChatOpenAI + if isinstance(metadata['morpheus'], list): num_meta = len(metadata['morpheus']) input_data = [f"input_{i}" for i in range(num_meta)] @@ -271,7 +290,7 @@ def mock_llm_chat(*_, messages, **__): "arun_return,replace_value,expected_output", [ ( - [[OutputParserException("Parsing Error"), "A valid result."]], + [[OutputParserExceptionStandin("Parsing Error"), "A valid result."]], "Default error message.", [["Default error message.", "A valid result."]], ), @@ -282,7 +301,7 @@ def mock_llm_chat(*_, messages, **__): ), ( [ - ["A valid result.", OutputParserException("Parsing Error")], + ["A valid result.", OutputParserExceptionStandin("Parsing Error")], [Exception("General error"), "Another valid result."], ], None, @@ -297,6 +316,20 @@ def test_execute_replaces_exceptions( replace_value: str, expected_output: list, ): + from langchain_core.exceptions import OutputParserException + + arun_return_tmp = [] + for values in arun_return: + values_tmp = [] + for value in values: + if isinstance(value, OutputParserExceptionStandin): + values_tmp.append(OutputParserException(*value.args)) + else: + values_tmp.append(value) + arun_return_tmp.append(values_tmp) + + arun_return = arun_return_tmp + placeholder_input_values = {"foo": "bar"} # a non-empty placeholder input for the context mock_agent_executor.arun.return_value = arun_return diff --git a/tests/llm/services/conftest.py b/tests/llm/services/conftest.py index a802c6ec84..88f30e76ba 100644 --- a/tests/llm/services/conftest.py +++ b/tests/llm/services/conftest.py @@ -36,12 +36,12 @@ def openai_fixture(openai): yield openai -@pytest.fixture(name="nvfoundationllm", autouse=True, scope='session') -def nvfoundationllm_fixture(nvfoundationllm): +@pytest.fixture(name="langchain_nvidia_ai_endpoints", autouse=True, scope='session') +def langchain_nvidia_ai_endpoints_fixture(langchain_nvidia_ai_endpoints): """ - All of the tests in this subdir require nvfoundationllm + All of the tests in this subdir require langchain_nvidia_ai_endpoints """ - yield nvfoundationllm + yield langchain_nvidia_ai_endpoints @pytest.fixture(name="mock_chat_completion", autouse=True) diff --git a/tests/llm/test_agents_simple_pipe.py b/tests/llm/test_agents_simple_pipe.py index 61fa7f8d84..923588ecce 100644 --- a/tests/llm/test_agents_simple_pipe.py +++ b/tests/llm/test_agents_simple_pipe.py @@ -18,12 +18,6 @@ from unittest import mock import pytest -from langchain.agents import AgentType -from langchain.agents import initialize_agent -from langchain.agents import load_tools -from langchain.agents.tools import Tool -from langchain_community.llms import OpenAI # pylint: disable=no-name-in-module -from langchain_community.utilities import serpapi import cudf @@ -48,6 +42,12 @@ def questions_fixture(): def _build_agent_executor(model_name: str): + from langchain.agents import AgentType + from langchain.agents import initialize_agent + from langchain.agents import load_tools + from langchain.agents.tools import Tool + from langchain_community.llms import OpenAI # pylint: disable=no-name-in-module + from langchain_community.utilities import serpapi llm = OpenAI(model=model_name, temperature=0, cache=False) @@ -134,6 +134,7 @@ def test_agents_simple_pipe(mock_openai_agenerate: mock.AsyncMock, from langchain.schema import Generation from langchain.schema import LLMResult + from langchain_community.utilities import serpapi assert serpapi.SerpAPIWrapper().aresults is mock_serpapi_aresults From 742419afac69d7f36f13187c585f47af782bbb36 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Wed, 2 Oct 2024 09:17:32 -0700 Subject: [PATCH 02/10] WIP --- tests/llm/nodes/test_langchain_agent_node.py | 54 ++++++++++---------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/tests/llm/nodes/test_langchain_agent_node.py b/tests/llm/nodes/test_langchain_agent_node.py index a8c41110cc..f9e59a012c 100644 --- a/tests/llm/nodes/test_langchain_agent_node.py +++ b/tests/llm/nodes/test_langchain_agent_node.py @@ -171,32 +171,6 @@ def test_execute_error(mock_chat_completion: tuple[mock.MagicMock, mock.MagicMoc assert isinstance(execute_node(node, input="input1"), RuntimeError) -class MetadataSaverTool(BaseTool): - # The base class defines *args and **kwargs in the signature for _run and _arun requiring the arguments-differ - # pylint: disable=arguments-differ - name: str = "MetadataSaverTool" - description: str = "useful for when you need to know the name of a reptile" - - saved_metadata: list[dict] = [] - - def _run( - self, - query: str, - run_manager: typing.Optional["CallbackManagerForToolRun"] = None, - ) -> str: - raise NotImplementedError("This tool only supports async") - - async def _arun( - self, - query: str, - run_manager: typing.Optional["AsyncCallbackManagerForToolRun"] = None, - ) -> str: - assert query is not None # avoiding unused-argument - assert run_manager is not None - self.saved_metadata.append(run_manager.metadata.copy()) - return "frog" - - @pytest.mark.parametrize("metadata", [{ "morpheus": "unittest" @@ -210,6 +184,32 @@ def test_metadata(mock_chat_completion: tuple[mock.MagicMock, mock.MagicMock], m from langchain.agents import AgentType from langchain.agents import initialize_agent from langchain_community.chat_models.openai import ChatOpenAI + from langchain_core.tools import BaseTool + + class MetadataSaverTool(BaseTool): + # The base class defines *args and **kwargs in the signature for _run and _arun requiring the arguments-differ + # pylint: disable=arguments-differ + name: str = "MetadataSaverTool" + description: str = "useful for when you need to know the name of a reptile" + + saved_metadata: list[dict] = [] + + def _run( + self, + query: str, + run_manager: typing.Optional["CallbackManagerForToolRun"] = None, + ) -> str: + raise NotImplementedError("This tool only supports async") + + async def _arun( + self, + query: str, + run_manager: typing.Optional["AsyncCallbackManagerForToolRun"] = None, + ) -> str: + assert query is not None # avoiding unused-argument + assert run_manager is not None + self.saved_metadata.append(run_manager.metadata.copy()) + return "frog" if isinstance(metadata['morpheus'], list): num_meta = len(metadata['morpheus']) @@ -316,6 +316,8 @@ def test_execute_replaces_exceptions( replace_value: str, expected_output: list, ): + # We couldn't import OutputParserException at the module level, so we need to replace instances of + # OutputParserExceptionStandin with OutputParserException from langchain_core.exceptions import OutputParserException arun_return_tmp = [] From c6dec8d93a8575e7ef5e2ec4685e4424364b9770 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Wed, 2 Oct 2024 09:34:45 -0700 Subject: [PATCH 03/10] Mark the shared process pool tests as slow --- tests/utils/test_shared_process_pool.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/utils/test_shared_process_pool.py b/tests/utils/test_shared_process_pool.py index e1d605f4bb..4ec0b5e24a 100644 --- a/tests/utils/test_shared_process_pool.py +++ b/tests/utils/test_shared_process_pool.py @@ -93,6 +93,7 @@ def test_singleton(): assert pool_1 is pool_2 +@pytest.mark.slow def test_pool_status(shared_process_pool): pool = shared_process_pool @@ -125,6 +126,7 @@ def test_pool_status(shared_process_pool): assert not pool._task_queues +@pytest.mark.slow @pytest.mark.parametrize( "a, b, expected", [ @@ -157,6 +159,7 @@ def test_submit_single_task(shared_process_pool, a, b, expected): pool.submit_task("test_stage", _add_task, 10, 20) +@pytest.mark.slow def test_submit_task_with_invalid_stage(shared_process_pool): pool = shared_process_pool @@ -165,6 +168,7 @@ def test_submit_task_with_invalid_stage(shared_process_pool): pool.submit_task("stage_does_not_exist", _add_task, 10, 20) +@pytest.mark.slow def test_submit_task_raises_exception(shared_process_pool): pool = shared_process_pool @@ -175,6 +179,7 @@ def test_submit_task_raises_exception(shared_process_pool): task.result() +@pytest.mark.slow def test_submit_task_with_unserializable_result(shared_process_pool): pool = shared_process_pool @@ -185,6 +190,7 @@ def test_submit_task_with_unserializable_result(shared_process_pool): task.result() +@pytest.mark.slow def test_submit_task_with_unserializable_arg(shared_process_pool): pool = shared_process_pool @@ -195,6 +201,7 @@ def test_submit_task_with_unserializable_arg(shared_process_pool): pool.submit_task("test_stage", _arbitrary_function, threading.Lock()) +@pytest.mark.slow @pytest.mark.parametrize( "a, b, expected", [ @@ -220,6 +227,7 @@ def test_submit_multiple_tasks(shared_process_pool, a, b, expected): assert future.result() == expected +@pytest.mark.slow def test_set_usage(shared_process_pool): pool = shared_process_pool @@ -256,6 +264,7 @@ def test_set_usage(shared_process_pool): assert pool._total_usage == 0.9 +@pytest.mark.slow def test_task_completion_with_early_stop(shared_process_pool): pool = shared_process_pool @@ -292,6 +301,7 @@ def test_task_completion_with_early_stop(shared_process_pool): assert task.done() +@pytest.mark.slow def test_terminate_running_tasks(shared_process_pool): pool = shared_process_pool From c24b493b11925df0fdb8f1687e4818fa014e5a35 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Wed, 2 Oct 2024 09:35:13 -0700 Subject: [PATCH 04/10] Import optional deps in a try block --- tests/llm/services/test_nvfoundation_llm_service.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/llm/services/test_nvfoundation_llm_service.py b/tests/llm/services/test_nvfoundation_llm_service.py index f139ddacde..35a6a66f2b 100644 --- a/tests/llm/services/test_nvfoundation_llm_service.py +++ b/tests/llm/services/test_nvfoundation_llm_service.py @@ -17,13 +17,17 @@ from unittest import mock import pytest -from langchain_core.messages import ChatMessage -from langchain_core.outputs import ChatGeneration -from langchain_core.outputs import LLMResult from morpheus_llm.llm.services.nvfoundation_llm_service import NVFoundationLLMClient from morpheus_llm.llm.services.nvfoundation_llm_service import NVFoundationLLMService +try: + from langchain_core.messages import ChatMessage + from langchain_core.outputs import ChatGeneration + from langchain_core.outputs import LLMResult +except ImportError: + pass + @pytest.fixture(name="set_default_nvidia_api_key", autouse=True, scope="function") def set_default_nvidia_api_key_fixture(): @@ -34,7 +38,7 @@ def set_default_nvidia_api_key_fixture(): @pytest.mark.parametrize("api_key", ["nvapi-12345", None]) @pytest.mark.parametrize("base_url", ["http://test.nvidia.com/v1", None]) -def test_constructor(api_key: str, base_url: bool): +def test_constructor(api_key: str | None, base_url: bool | None): service = NVFoundationLLMService(api_key=api_key, base_url=base_url) From 92c4b84ce992f241b0551043374ded21b50732d1 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Wed, 2 Oct 2024 09:50:28 -0700 Subject: [PATCH 05/10] Move imports to a try-block --- tests/llm/nodes/test_langchain_agent_node.py | 24 ++++++++------------ tests/llm/test_agents_simple_pipe.py | 21 ++++++++--------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/tests/llm/nodes/test_langchain_agent_node.py b/tests/llm/nodes/test_langchain_agent_node.py index f9e59a012c..ab1a9babe7 100644 --- a/tests/llm/nodes/test_langchain_agent_node.py +++ b/tests/llm/nodes/test_langchain_agent_node.py @@ -26,6 +26,16 @@ from morpheus_llm.llm import LLMNodeBase from morpheus_llm.llm.nodes.langchain_agent_node import LangChainAgentNode +try: + from langchain.agents import AgentType + from langchain.agents import Tool + from langchain.agents import initialize_agent + from langchain.callbacks.manager import AsyncCallbackManagerForToolRun + from langchain.callbacks.manager import CallbackManagerForToolRun + from langchain_community.chat_models.openai import ChatOpenAI +except ImportError: + pass + if typing.TYPE_CHECKING: from langchain.callbacks.manager import AsyncCallbackManagerForToolRun from langchain.callbacks.manager import CallbackManagerForToolRun @@ -81,11 +91,6 @@ def test_execute( def test_execute_tools(mock_chat_completion: tuple[mock.MagicMock, mock.MagicMock]): - from langchain.agents import AgentType - from langchain.agents import Tool - from langchain.agents import initialize_agent - from langchain_community.chat_models.openai import ChatOpenAI - # Tests the execute method of the LangChainAgentNode with a a mocked tools and chat completion (_, mock_async_client) = mock_chat_completion chat_responses = [ @@ -128,11 +133,6 @@ def test_execute_tools(mock_chat_completion: tuple[mock.MagicMock, mock.MagicMoc def test_execute_error(mock_chat_completion: tuple[mock.MagicMock, mock.MagicMock]): - from langchain.agents import AgentType - from langchain.agents import Tool - from langchain.agents import initialize_agent - from langchain_community.chat_models.openai import ChatOpenAI - # Tests the execute method of the LangChainAgentNode with a a mocked tools and chat completion (_, mock_async_client) = mock_chat_completion chat_responses = [ @@ -181,10 +181,6 @@ def test_execute_error(mock_chat_completion: tuple[mock.MagicMock, mock.MagicMoc }], ids=["single-metadata", "single-metadata-list", "multiple-metadata-list"]) def test_metadata(mock_chat_completion: tuple[mock.MagicMock, mock.MagicMock], metadata: dict): - from langchain.agents import AgentType - from langchain.agents import initialize_agent - from langchain_community.chat_models.openai import ChatOpenAI - from langchain_core.tools import BaseTool class MetadataSaverTool(BaseTool): # The base class defines *args and **kwargs in the signature for _run and _arun requiring the arguments-differ diff --git a/tests/llm/test_agents_simple_pipe.py b/tests/llm/test_agents_simple_pipe.py index 923588ecce..b44734a987 100644 --- a/tests/llm/test_agents_simple_pipe.py +++ b/tests/llm/test_agents_simple_pipe.py @@ -35,20 +35,23 @@ from morpheus_llm.llm.task_handlers.simple_task_handler import SimpleTaskHandler from morpheus_llm.stages.llm.llm_engine_stage import LLMEngineStage - -@pytest.fixture(name="questions") -def questions_fixture(): - return ["Who is Leo DiCaprio's girlfriend? What is her current age raised to the 0.43 power?"] - - -def _build_agent_executor(model_name: str): +try: from langchain.agents import AgentType from langchain.agents import initialize_agent from langchain.agents import load_tools from langchain.agents.tools import Tool from langchain_community.llms import OpenAI # pylint: disable=no-name-in-module from langchain_community.utilities import serpapi +except ImportError: + pass + +@pytest.fixture(name="questions") +def questions_fixture(): + return ["Who is Leo DiCaprio's girlfriend? What is her current age raised to the 0.43 power?"] + + +def _build_agent_executor(model_name: str): llm = OpenAI(model=model_name, temperature=0, cache=False) # Explicitly construct the serpapi tool, loading it via load_tools makes it too difficult to mock @@ -132,10 +135,6 @@ def test_agents_simple_pipe(mock_openai_agenerate: mock.AsyncMock, questions: list[str]): os.environ.update({'OPENAI_API_KEY': 'test_api_key', 'SERPAPI_API_KEY': 'test_api_key'}) - from langchain.schema import Generation - from langchain.schema import LLMResult - from langchain_community.utilities import serpapi - assert serpapi.SerpAPIWrapper().aresults is mock_serpapi_aresults model_name = "test_model" From d7efd722848a4c9aaf599f7d86e81d73a0a15713 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Wed, 2 Oct 2024 10:31:21 -0700 Subject: [PATCH 06/10] Cleanup import error messages --- .../morpheus_llm/llm/services/nemo_llm_service.py | 2 +- .../llm/services/nvfoundation_llm_service.py | 12 +++++------- .../morpheus_llm/llm/services/openai_chat_service.py | 9 +++------ .../morpheus_llm/service/vdb/faiss_vdb_service.py | 5 ++--- .../service/vdb/milvus_vector_db_service.py | 4 ++-- 5 files changed, 13 insertions(+), 19 deletions(-) diff --git a/python/morpheus_llm/morpheus_llm/llm/services/nemo_llm_service.py b/python/morpheus_llm/morpheus_llm/llm/services/nemo_llm_service.py index 293dff12a1..30cda8e02c 100644 --- a/python/morpheus_llm/morpheus_llm/llm/services/nemo_llm_service.py +++ b/python/morpheus_llm/morpheus_llm/llm/services/nemo_llm_service.py @@ -228,7 +228,7 @@ def __init__(self, """ if IMPORT_EXCEPTION is not None: - raise ImportError(IMPORT_ERROR_MESSAGE) from IMPORT_EXCEPTION + raise ImportError(IMPORT_ERROR_MESSAGE.format(package='nemollm')) from IMPORT_EXCEPTION super().__init__() diff --git a/python/morpheus_llm/morpheus_llm/llm/services/nvfoundation_llm_service.py b/python/morpheus_llm/morpheus_llm/llm/services/nvfoundation_llm_service.py index d1e706b7c2..709f394712 100644 --- a/python/morpheus_llm/morpheus_llm/llm/services/nvfoundation_llm_service.py +++ b/python/morpheus_llm/morpheus_llm/llm/services/nvfoundation_llm_service.py @@ -16,17 +16,13 @@ import typing from morpheus.utils.env_config_value import EnvConfigValue +from morpheus_llm.error import IMPORT_ERROR_MESSAGE from morpheus_llm.llm.services.llm_service import LLMClient from morpheus_llm.llm.services.llm_service import LLMService logger = logging.getLogger(__name__) IMPORT_EXCEPTION = None -IMPORT_ERROR_MESSAGE = ( - "The `langchain-nvidia-ai-endpoints` package was not found. Install it and other additional dependencies by " - "running the following command:" - "`conda env update --solver=libmamba -n morpheus " - "--file conda/environments/examples_cuda-121_arch-x86_64.yaml`") try: from langchain_core.prompt_values import StringPromptValue @@ -52,7 +48,8 @@ class NVFoundationLLMClient(LLMClient): def __init__(self, parent: "NVFoundationLLMService", *, model_name: str, **model_kwargs) -> None: if IMPORT_EXCEPTION is not None: - raise ImportError(IMPORT_ERROR_MESSAGE) from IMPORT_EXCEPTION + raise ImportError( + IMPORT_ERROR_MESSAGE.format(package='langchain-nvidia-ai-endpoints')) from IMPORT_EXCEPTION super().__init__() @@ -218,7 +215,8 @@ class BaseURL(EnvConfigValue): def __init__(self, *, api_key: APIKey | str = None, base_url: BaseURL | str = None, **model_kwargs) -> None: if IMPORT_EXCEPTION is not None: - raise ImportError(IMPORT_ERROR_MESSAGE) from IMPORT_EXCEPTION + raise ImportError( + IMPORT_ERROR_MESSAGE.format(package='langchain-nvidia-ai-endpoints')) from IMPORT_EXCEPTION super().__init__() diff --git a/python/morpheus_llm/morpheus_llm/llm/services/openai_chat_service.py b/python/morpheus_llm/morpheus_llm/llm/services/openai_chat_service.py index d4eaac4503..2df6048d5a 100644 --- a/python/morpheus_llm/morpheus_llm/llm/services/openai_chat_service.py +++ b/python/morpheus_llm/morpheus_llm/llm/services/openai_chat_service.py @@ -23,16 +23,13 @@ import appdirs from morpheus.utils.env_config_value import EnvConfigValue +from morpheus_llm.error import IMPORT_ERROR_MESSAGE from morpheus_llm.llm.services.llm_service import LLMClient from morpheus_llm.llm.services.llm_service import LLMService logger = logging.getLogger(__name__) IMPORT_EXCEPTION = None -IMPORT_ERROR_MESSAGE = ("OpenAIChatService & OpenAIChatClient require the openai package to be installed. " - "Install it by running the following command:\n" - "`conda env update --solver=libmamba -n morpheus " - "--file conda/environments/examples_cuda-121_arch-x86_64.yaml --prune`") try: import openai @@ -107,7 +104,7 @@ def __init__(self, json=False, **model_kwargs) -> None: if IMPORT_EXCEPTION is not None: - raise ImportError(IMPORT_ERROR_MESSAGE) from IMPORT_EXCEPTION + raise ImportError(IMPORT_ERROR_MESSAGE.format(package='openai')) from IMPORT_EXCEPTION super().__init__() @@ -400,7 +397,7 @@ def __init__(self, default_model_kwargs: dict = None) -> None: if IMPORT_EXCEPTION is not None: - raise ImportError(IMPORT_ERROR_MESSAGE) from IMPORT_EXCEPTION + raise ImportError(IMPORT_ERROR_MESSAGE.format(package='openai')) from IMPORT_EXCEPTION super().__init__() diff --git a/python/morpheus_llm/morpheus_llm/service/vdb/faiss_vdb_service.py b/python/morpheus_llm/morpheus_llm/service/vdb/faiss_vdb_service.py index 82e6c146d2..2e50dd804f 100644 --- a/python/morpheus_llm/morpheus_llm/service/vdb/faiss_vdb_service.py +++ b/python/morpheus_llm/morpheus_llm/service/vdb/faiss_vdb_service.py @@ -27,7 +27,6 @@ logger = logging.getLogger(__name__) IMPORT_EXCEPTION = None -IMPORT_ERROR_MESSAGE = "FaissDBResourceService requires the FAISS library to be installed." try: from langchain.embeddings.base import Embeddings @@ -50,7 +49,7 @@ class FaissVectorDBResourceService(VectorDBResourceService): def __init__(self, parent: "FaissVectorDBService", *, name: str) -> None: if IMPORT_EXCEPTION is not None: - raise ImportError(IMPORT_ERROR_MESSAGE) from IMPORT_EXCEPTION + raise ImportError(IMPORT_ERROR_MESSAGE.format(package='langchain and faiss-gpu')) from IMPORT_EXCEPTION super().__init__() @@ -285,7 +284,7 @@ class FaissVectorDBService(VectorDBService): def __init__(self, local_dir: str, embeddings: "Embeddings"): if IMPORT_EXCEPTION is not None: - raise ImportError(IMPORT_ERROR_MESSAGE) from IMPORT_EXCEPTION + raise ImportError(IMPORT_ERROR_MESSAGE.format(package='langchain and faiss-gpu')) from IMPORT_EXCEPTION self._local_dir = local_dir self._embeddings = embeddings diff --git a/python/morpheus_llm/morpheus_llm/service/vdb/milvus_vector_db_service.py b/python/morpheus_llm/morpheus_llm/service/vdb/milvus_vector_db_service.py index f43fbbf79c..71df614b23 100644 --- a/python/morpheus_llm/morpheus_llm/service/vdb/milvus_vector_db_service.py +++ b/python/morpheus_llm/morpheus_llm/service/vdb/milvus_vector_db_service.py @@ -25,13 +25,13 @@ from morpheus.io.utils import cudf_string_cols_exceed_max_bytes from morpheus.io.utils import truncate_string_cols_by_bytes from morpheus.utils.type_aliases import DataFrameType +from morpheus_llm.error import IMPORT_ERROR_MESSAGE from morpheus_llm.service.vdb.vector_db_service import VectorDBResourceService from morpheus_llm.service.vdb.vector_db_service import VectorDBService logger = logging.getLogger(__name__) IMPORT_EXCEPTION = None -IMPORT_ERROR_MESSAGE = "MilvusVectorDBResourceService requires the milvus and pymilvus packages to be installed." # Milvus has a max string length in bytes of 65,535. Multi-byte characters like "ñ" will have a string length of 1, the # byte length encoded as UTF-8 will be 2 @@ -234,7 +234,7 @@ class MilvusVectorDBResourceService(VectorDBResourceService): def __init__(self, name: str, client: "MilvusClient", truncate_long_strings: bool = False) -> None: if IMPORT_EXCEPTION is not None: - raise ImportError(IMPORT_ERROR_MESSAGE) from IMPORT_EXCEPTION + raise ImportError(IMPORT_ERROR_MESSAGE.format(package='pymilvus')) from IMPORT_EXCEPTION super().__init__() From c687d6a6a6b4bbf23780162bec599c1057002efa Mon Sep 17 00:00:00 2001 From: David Gardner Date: Wed, 2 Oct 2024 10:42:24 -0700 Subject: [PATCH 07/10] Remove unused fail_missing fixture --- .../morpheus_llm/llm/nodes/langchain_agent_node.py | 4 ++-- .../morpheus_llm/service/vdb/faiss_vdb_service.py | 1 + tests/llm/conftest.py | 12 ++++++------ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/python/morpheus_llm/morpheus_llm/llm/nodes/langchain_agent_node.py b/python/morpheus_llm/morpheus_llm/llm/nodes/langchain_agent_node.py index 617a5e2892..0e96c600fd 100644 --- a/python/morpheus_llm/morpheus_llm/llm/nodes/langchain_agent_node.py +++ b/python/morpheus_llm/morpheus_llm/llm/nodes/langchain_agent_node.py @@ -26,8 +26,8 @@ try: from langchain_core.exceptions import OutputParserException -except ImportError as e: - IMPORT_EXCEPTION = e +except ImportError as import_exc: + IMPORT_EXCEPTION = import_exc if typing.TYPE_CHECKING: from langchain.agents import AgentExecutor diff --git a/python/morpheus_llm/morpheus_llm/service/vdb/faiss_vdb_service.py b/python/morpheus_llm/morpheus_llm/service/vdb/faiss_vdb_service.py index 2e50dd804f..0197f3071d 100644 --- a/python/morpheus_llm/morpheus_llm/service/vdb/faiss_vdb_service.py +++ b/python/morpheus_llm/morpheus_llm/service/vdb/faiss_vdb_service.py @@ -21,6 +21,7 @@ import cudf +from morpheus_llm.error import IMPORT_ERROR_MESSAGE from morpheus_llm.service.vdb.vector_db_service import VectorDBResourceService from morpheus_llm.service.vdb.vector_db_service import VectorDBService diff --git a/tests/llm/conftest.py b/tests/llm/conftest.py index 25dc206eed..94658863c5 100644 --- a/tests/llm/conftest.py +++ b/tests/llm/conftest.py @@ -22,7 +22,7 @@ @pytest.fixture(name="nemollm", scope='session', autouse=True) -def nemollm_fixture(nemollm: types.ModuleType, fail_missing: bool): +def nemollm_fixture(nemollm: types.ModuleType): """ Fixture to ensure nemollm is installed """ @@ -30,7 +30,7 @@ def nemollm_fixture(nemollm: types.ModuleType, fail_missing: bool): @pytest.fixture(name="openai", scope='session', autouse=True) -def openai_fixture(openai: types.ModuleType, fail_missing: bool): +def openai_fixture(openai: types.ModuleType): """ Fixture to ensure openai is installed """ @@ -38,7 +38,7 @@ def openai_fixture(openai: types.ModuleType, fail_missing: bool): @pytest.fixture(name="langchain", scope='session', autouse=True) -def langchain_fixture(langchain: types.ModuleType, fail_missing: bool): +def langchain_fixture(langchain: types.ModuleType): """ Fixture to ensure langchain is installed """ @@ -46,7 +46,7 @@ def langchain_fixture(langchain: types.ModuleType, fail_missing: bool): @pytest.fixture(name="langchain_core", scope='session', autouse=True) -def langchain_core_fixture(langchain_core: types.ModuleType, fail_missing: bool): +def langchain_core_fixture(langchain_core: types.ModuleType): """ Fixture to ensure langchain_core is installed """ @@ -54,7 +54,7 @@ def langchain_core_fixture(langchain_core: types.ModuleType, fail_missing: bool) @pytest.fixture(name="langchain_community", scope='session', autouse=True) -def langchain_community_fixture(langchain_community: types.ModuleType, fail_missing: bool): +def langchain_community_fixture(langchain_community: types.ModuleType): """ Fixture to ensure langchain_community is installed """ @@ -62,7 +62,7 @@ def langchain_community_fixture(langchain_community: types.ModuleType, fail_miss @pytest.fixture(name="langchain_nvidia_ai_endpoints", scope='session', autouse=True) -def langchain_nvidia_ai_endpoints_fixture(langchain_nvidia_ai_endpoints: types.ModuleType, fail_missing: bool): +def langchain_nvidia_ai_endpoints_fixture(langchain_nvidia_ai_endpoints: types.ModuleType): """ Fixture to ensure langchain_nvidia_ai_endpoints is installed """ From a9c4315d66b903538626e5ce369a6d5703eaf7f2 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Wed, 2 Oct 2024 10:48:54 -0700 Subject: [PATCH 08/10] pylint fixes --- tests/llm/nodes/test_langchain_agent_node.py | 1 + tests/llm/test_agents_simple_pipe.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/tests/llm/nodes/test_langchain_agent_node.py b/tests/llm/nodes/test_langchain_agent_node.py index ab1a9babe7..aa843bc930 100644 --- a/tests/llm/nodes/test_langchain_agent_node.py +++ b/tests/llm/nodes/test_langchain_agent_node.py @@ -33,6 +33,7 @@ from langchain.callbacks.manager import AsyncCallbackManagerForToolRun from langchain.callbacks.manager import CallbackManagerForToolRun from langchain_community.chat_models.openai import ChatOpenAI + from langchain_core.tools import BaseTool except ImportError: pass diff --git a/tests/llm/test_agents_simple_pipe.py b/tests/llm/test_agents_simple_pipe.py index b44734a987..5d33dacb03 100644 --- a/tests/llm/test_agents_simple_pipe.py +++ b/tests/llm/test_agents_simple_pipe.py @@ -40,6 +40,8 @@ from langchain.agents import initialize_agent from langchain.agents import load_tools from langchain.agents.tools import Tool + from langchain.schema import Generation + from langchain.schema import LLMResult from langchain_community.llms import OpenAI # pylint: disable=no-name-in-module from langchain_community.utilities import serpapi except ImportError: From 7ba73249408efe3bf5dfa8bb1e558f93e5e60164 Mon Sep 17 00:00:00 2001 From: David Gardner Date: Wed, 2 Oct 2024 10:51:30 -0700 Subject: [PATCH 09/10] Remove redundant imports --- tests/llm/nodes/test_langchain_agent_node.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/llm/nodes/test_langchain_agent_node.py b/tests/llm/nodes/test_langchain_agent_node.py index aa843bc930..e6698866a2 100644 --- a/tests/llm/nodes/test_langchain_agent_node.py +++ b/tests/llm/nodes/test_langchain_agent_node.py @@ -30,8 +30,6 @@ from langchain.agents import AgentType from langchain.agents import Tool from langchain.agents import initialize_agent - from langchain.callbacks.manager import AsyncCallbackManagerForToolRun - from langchain.callbacks.manager import CallbackManagerForToolRun from langchain_community.chat_models.openai import ChatOpenAI from langchain_core.tools import BaseTool except ImportError: From 1b53b32fc424497462f1104c0f0750f23faabb3c Mon Sep 17 00:00:00 2001 From: David Gardner Date: Wed, 2 Oct 2024 11:27:33 -0700 Subject: [PATCH 10/10] CallbackManagerForToolRun and AsyncCallbackManagerForToolRun have to explicitly appear in the type-hint not as a lazy string --- tests/llm/nodes/test_langchain_agent_node.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/llm/nodes/test_langchain_agent_node.py b/tests/llm/nodes/test_langchain_agent_node.py index e6698866a2..0779b11604 100644 --- a/tests/llm/nodes/test_langchain_agent_node.py +++ b/tests/llm/nodes/test_langchain_agent_node.py @@ -30,15 +30,13 @@ from langchain.agents import AgentType from langchain.agents import Tool from langchain.agents import initialize_agent + from langchain.callbacks.manager import AsyncCallbackManagerForToolRun + from langchain.callbacks.manager import CallbackManagerForToolRun from langchain_community.chat_models.openai import ChatOpenAI from langchain_core.tools import BaseTool except ImportError: pass -if typing.TYPE_CHECKING: - from langchain.callbacks.manager import AsyncCallbackManagerForToolRun - from langchain.callbacks.manager import CallbackManagerForToolRun - class OutputParserExceptionStandin(Exception): """ @@ -192,14 +190,14 @@ class MetadataSaverTool(BaseTool): def _run( self, query: str, - run_manager: typing.Optional["CallbackManagerForToolRun"] = None, + run_manager: typing.Optional[CallbackManagerForToolRun] = None, ) -> str: raise NotImplementedError("This tool only supports async") async def _arun( self, query: str, - run_manager: typing.Optional["AsyncCallbackManagerForToolRun"] = None, + run_manager: typing.Optional[AsyncCallbackManagerForToolRun] = None, ) -> str: assert query is not None # avoiding unused-argument assert run_manager is not None