# Copyright (c) "Neo4j" # Neo4j Sweden AB [https://neo4j.com] # # # 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 # # # https://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. from unittest.mock import MagicMock, patch import neo4j from neo4j_graphrag.embeddings.base import Embedder from neo4j_graphrag.llm.base import LLMInterface from neo4j_graphrag.retrievers import ( HybridCypherRetriever, HybridRetriever, Text2CypherRetriever, VectorCypherRetriever, VectorRetriever, ) from neo4j_graphrag.tool import Tool # Mock dependencies for retriever instances def create_mock_driver() -> neo4j.Driver: driver = MagicMock(spec=neo4j.Driver) # Create a mock result object with a records attribute mock_result = MagicMock() mock_result.records = [MagicMock()] driver.execute_query.return_value = mock_result return driver def create_mock_embedder() -> Embedder: embedder = MagicMock(spec=Embedder) embedder.embed_query.return_value = [0.1, 0.2, 0.3] return embedder def create_mock_llm() -> LLMInterface: llm = MagicMock() llm.invoke.return_value = "MATCH (n) RETURN n" return llm # Test conversion with VectorRetriever @patch("neo4j_graphrag.retrievers.base.get_version") def test_convert_vector_retriever_to_tool(mock_get_version: MagicMock) -> None: """Test conversion of VectorRetriever to a Tool instance with correct attributes.""" mock_get_version.return_value = ((5, 20, 0), False, False) driver = create_mock_driver() embedder = create_mock_embedder() retriever = VectorRetriever( driver=driver, index_name="test_index", embedder=embedder, return_properties=["name", "description"], ) tool = retriever.convert_to_tool( name="VectorRetriever", description="A tool for vector-based retrieval from Neo4j.", parameter_descriptions={ "query_text": "The query text for vector search.", "top_k": "Number of results to return.", }, ) assert isinstance(tool, Tool) assert tool.get_name() == "VectorRetriever" assert tool.get_description() == "A tool for vector-based retrieval from Neo4j." # Check that the parameters object has the expected properties params = tool.get_parameters() assert "properties" in params assert len(params["properties"]) == 5 # VectorRetriever has 5 parameters assert "query_text" in params["properties"] assert "top_k" in params["properties"] assert "query_vector" in params["properties"] assert "effective_search_ratio" in params["properties"] assert "filters" in params["properties"] # Test conversion with VectorCypherRetriever @patch("neo4j_graphrag.retrievers.base.get_version") def test_convert_vector_cypher_retriever_to_tool(mock_get_version: MagicMock) -> None: """Test conversion of VectorCypherRetriever to a Tool instance with correct attributes.""" mock_get_version.return_value = ((5, 20, 0), False, False) driver = create_mock_driver() embedder = create_mock_embedder() retriever = VectorCypherRetriever( driver=driver, index_name="test_index", embedder=embedder, retrieval_query="RETURN n", ) tool = retriever.convert_to_tool( name="VectorCypherRetriever", description="A tool for vector-cypher retrieval from Neo4j.", parameter_descriptions={ "query_text": "The query text for vector-cypher search.", "top_k": "Number of results to return.", }, ) assert isinstance(tool, Tool) assert tool.get_name() == "VectorCypherRetriever" assert tool.get_description() == "A tool for vector-cypher retrieval from Neo4j." # Check that the parameters object has the expected properties params = tool.get_parameters() assert "properties" in params assert len(params["properties"]) == 6 # VectorCypherRetriever has 6 parameters assert "query_text" in params["properties"] assert "top_k" in params["properties"] assert "query_vector" in params["properties"] assert "effective_search_ratio" in params["properties"] assert "query_params" in params["properties"] assert "filters" in params["properties"] # Test conversion with HybridRetriever @patch("neo4j_graphrag.retrievers.base.get_version") def test_convert_hybrid_retriever_to_tool(mock_get_version: MagicMock) -> None: """Test conversion of HybridRetriever to a Tool instance with correct attributes.""" mock_get_version.return_value = ((5, 20, 0), False, False) driver = create_mock_driver() embedder = create_mock_embedder() retriever = HybridRetriever( driver=driver, vector_index_name="test_vector_index", fulltext_index_name="test_fulltext_index", embedder=embedder, return_properties=["name", "description"], ) tool = retriever.convert_to_tool( name="HybridRetriever", description="A tool for hybrid retrieval from Neo4j.", parameter_descriptions={ "query_text": "The query text for hybrid search.", "top_k": "Number of results to return.", }, ) assert isinstance(tool, Tool) assert tool.get_name() == "HybridRetriever" assert tool.get_description() == "A tool for hybrid retrieval from Neo4j." # Check that the parameters object has the expected properties params = tool.get_parameters() assert "properties" in params assert len(params["properties"]) == 6 # HybridRetriever has 6 parameters assert "query_text" in params["properties"] assert "top_k" in params["properties"] assert "query_vector" in params["properties"] assert "effective_search_ratio" in params["properties"] assert "ranker" in params["properties"] assert "alpha" in params["properties"] # Test conversion with HybridCypherRetriever @patch("neo4j_graphrag.retrievers.base.get_version") def test_convert_hybrid_cypher_retriever_to_tool(mock_get_version: MagicMock) -> None: """Test conversion of HybridCypherRetriever to a Tool instance with correct attributes.""" mock_get_version.return_value = ((5, 20, 0), False, False) driver = create_mock_driver() embedder = create_mock_embedder() retriever = HybridCypherRetriever( driver=driver, vector_index_name="test_vector_index", fulltext_index_name="test_fulltext_index", embedder=embedder, retrieval_query="RETURN n", ) tool = retriever.convert_to_tool( name="HybridCypherRetriever", description="A tool for hybrid-cypher retrieval from Neo4j.", parameter_descriptions={ "query_text": "The query text for hybrid-cypher search.", "top_k": "Number of results to return.", }, ) assert isinstance(tool, Tool) assert tool.get_name() == "HybridCypherRetriever" assert tool.get_description() == "A tool for hybrid-cypher retrieval from Neo4j." # Check that the parameters object has the expected properties params = tool.get_parameters() assert "properties" in params assert len(params["properties"]) == 7 # HybridCypherRetriever has 7 parameters assert "query_text" in params["properties"] assert "query_vector" in params["properties"] assert "top_k" in params["properties"] assert "effective_search_ratio" in params["properties"] assert "query_params" in params["properties"] assert "ranker" in params["properties"] assert "alpha" in params["properties"] # Test conversion with Text2CypherRetriever @patch("neo4j_graphrag.retrievers.base.get_version") def test_convert_text2cypher_retriever_to_tool(mock_get_version: MagicMock) -> None: """Test conversion of Text2CypherRetriever to a Tool instance with correct attributes.""" mock_get_version.return_value = ((5, 20, 0), False, False) driver = create_mock_driver() llm = create_mock_llm() retriever = Text2CypherRetriever(driver=driver, llm=llm) tool = retriever.convert_to_tool( name="Text2CypherRetriever", description="A tool for text to Cypher retrieval from Neo4j.", parameter_descriptions={ "query_text": "The query text for text to Cypher conversion.", }, ) assert isinstance(tool, Tool) assert tool.get_name() == "Text2CypherRetriever" assert tool.get_description() == "A tool for text to Cypher retrieval from Neo4j." # Check that the parameters object has the expected properties params = tool.get_parameters() assert "properties" in params assert len(params["properties"]) == 2 # Text2CypherRetriever has 2 parameters assert "query_text" in params["properties"] assert "prompt_params" in params["properties"] # Test conversion with custom name provided @patch("neo4j_graphrag.retrievers.base.get_version") def test_convert_retriever_with_custom_name( mock_get_version: MagicMock, ) -> None: """Test conversion of a retriever to a Tool instance with a custom name.""" mock_get_version.return_value = ((5, 20, 0), False, False) driver = create_mock_driver() embedder = create_mock_embedder() retriever = VectorRetriever( driver=driver, index_name="test_index", embedder=embedder, return_properties=["name", "description"], ) custom_name = "CustomNamedTool" tool = retriever.convert_to_tool( name=custom_name, description="A tool with a custom name", parameter_descriptions={ "query_text": "The query text for vector search.", }, ) # Verify that the custom name is used instead of the retriever class name assert tool.get_name() == custom_name # Test conversion with no parameters provided @patch("neo4j_graphrag.retrievers.base.get_version") def test_convert_vector_retriever_to_tool_no_parameters( mock_get_version: MagicMock, ) -> None: """Test conversion of VectorRetriever to a Tool instance when no parameters are provided.""" mock_get_version.return_value = ((5, 20, 0), False, False) driver = create_mock_driver() embedder = create_mock_embedder() retriever = VectorRetriever( driver=driver, index_name="test_index", embedder=embedder, return_properties=["name", "description"], ) tool = retriever.convert_to_tool( name="VectorRetriever", description="A tool for vector-based retrieval from Neo4j.", ) assert isinstance(tool, Tool) assert tool.get_name() == "VectorRetriever" assert tool.get_description() == "A tool for vector-based retrieval from Neo4j." # With the new API, parameters are always auto-inferred from method signature params = tool.get_parameters() assert params is not None assert "properties" in params assert len(params["properties"]) == 5 # VectorRetriever has 5 parameters # Test tool execution for VectorRetriever @patch("neo4j_graphrag.retrievers.base.get_version") def test_vector_retriever_tool_execution(mock_get_version: MagicMock) -> None: """Test execution of VectorRetriever tool calls the search method with correct arguments.""" mock_get_version.return_value = ((5, 20, 0), False, False) driver = create_mock_driver() embedder = create_mock_embedder() retriever = VectorRetriever( driver=driver, index_name="test_index", embedder=embedder, return_properties=["name", "description"], ) # Create the tool first, before mocking with patch.object(VectorRetriever, "_fetch_index_infos"): tool = retriever.convert_to_tool( name="VectorRetriever", description="A tool for vector-based retrieval from Neo4j.", parameter_descriptions={ "query_text": "The query text for vector search.", "top_k": "Number of results to return.", }, ) # Now mock the get_search_results method to track calls from neo4j_graphrag.types import RawSearchResult get_search_results_mock = MagicMock( return_value=RawSearchResult(records=[], metadata={}) ) # Use patch to mock the method with patch.object(retriever, "get_search_results", get_search_results_mock): tools = {tool.get_name(): tool} # Simulate indirect invocation as would happen in real usage tool_call_arguments = {"query_text": "test query", "top_k": 5} # Pass the arguments as kwargs result = tools[tool.get_name()].execute(**tool_call_arguments) # Since we're using a context manager for patching, we need to verify the call inside the context # We can only check the result, not the method call itself assert result is not None assert hasattr(result, "items") # Should return RetrieverResult now assert isinstance(result.items, list) assert hasattr(result, "metadata") # Test tool execution for HybridRetriever @patch("neo4j_graphrag.retrievers.base.get_version") def test_hybrid_retriever_tool_execution(mock_get_version: MagicMock) -> None: """Test execution of HybridRetriever tool calls the search method with correct arguments.""" mock_get_version.return_value = ((5, 20, 0), False, False) driver = create_mock_driver() embedder = create_mock_embedder() retriever = HybridRetriever( driver=driver, vector_index_name="test_vector_index", fulltext_index_name="test_fulltext_index", embedder=embedder, return_properties=["name", "description"], ) # Create the tool first, before mocking with patch.object(HybridRetriever, "_fetch_index_infos"): tool = retriever.convert_to_tool( name="HybridRetriever", description="A tool for hybrid retrieval from Neo4j.", parameter_descriptions={ "query_text": "The query text for hybrid search.", "top_k": "Number of results to return.", }, ) # Now mock the get_search_results method to track calls from neo4j_graphrag.types import RawSearchResult get_search_results_mock = MagicMock( return_value=RawSearchResult(records=[], metadata={}) ) # Use patch to mock the method with patch.object(retriever, "get_search_results", get_search_results_mock): tools = {tool.get_name(): tool} # Simulate indirect invocation as would happen in real usage tool_call_arguments = {"query_text": "test query", "top_k": 5} # Pass the arguments as kwargs result = tools[tool.get_name()].execute(**tool_call_arguments) # Since we're using a context manager for patching, we need to verify the call inside the context # We can only check the result, not the method call itself assert result is not None assert hasattr(result, "items") # Should return RetrieverResult now assert isinstance(result.items, list) assert hasattr(result, "metadata") # Test tool execution for Text2CypherRetriever @patch("neo4j_graphrag.retrievers.base.get_version") def test_text2cypher_retriever_tool_execution(mock_get_version: MagicMock) -> None: """Test execution of Text2CypherRetriever tool calls the search method with correct arguments.""" mock_get_version.return_value = ((5, 20, 0), False, False) driver = create_mock_driver() llm = create_mock_llm() retriever = Text2CypherRetriever(driver=driver, llm=llm) # Create the tool first, before mocking tool = retriever.convert_to_tool( name="Text2CypherRetriever", description="A tool for text to Cypher retrieval from Neo4j.", parameter_descriptions={ "query_text": "The query text for text to Cypher conversion.", }, ) # Now mock the get_search_results method to track calls from neo4j_graphrag.types import RawSearchResult get_search_results_mock = MagicMock( return_value=RawSearchResult(records=[], metadata={}) ) # Use patch to mock the method with patch.object(retriever, "get_search_results", get_search_results_mock): tools = {tool.get_name(): tool} # Simulate indirect invocation as would happen in real usage tool_call_arguments = {"query_text": "test query"} # Pass the arguments as kwargs result = tools[tool.get_name()].execute(**tool_call_arguments) # Since we're using a context manager for patching, we need to verify the call inside the context # We can only check the result, not the method call itself assert result is not None assert hasattr(result, "items") # Should return RetrieverResult now assert isinstance(result.items, list) assert hasattr(result, "metadata") # Test tool serialization to JSON format @patch("neo4j_graphrag.retrievers.base.get_version") def test_tool_serialization(mock_get_version: MagicMock) -> None: """Test that a Tool instance can be serialized to the required JSON format.""" mock_get_version.return_value = ((5, 20, 0), False, False) driver = create_mock_driver() embedder = create_mock_embedder() retriever = VectorRetriever( driver=driver, index_name="test_index", embedder=embedder, return_properties=["name", "description"], ) tool = retriever.convert_to_tool( name="VectorRetriever", description="A tool for vector-based retrieval from Neo4j.", parameter_descriptions={ "query_text": "The query text for vector search.", "top_k": "Number of results to return.", }, ) # Create a dictionary representation of the tool tool_dict = { "type": "function", "name": tool.get_name(), "description": tool.get_description(), "parameters": tool.get_parameters(), } assert tool_dict["type"] == "function" assert tool_dict["name"] == tool.get_name() assert tool_dict["description"] == tool.get_description() assert "parameters" in tool_dict # Get parameters and convert to dictionary parameters_any = tool_dict["parameters"] # With the new API, parameters should be a dictionary if isinstance(parameters_any, dict): parameters_dict = parameters_any else: # Handle unexpected parameter format parameters_dict = { str(k): v for k, v in enumerate(parameters_any) if v is not None } # Check the parameters structure assert parameters_dict.get("type") == "object" assert "properties" in parameters_dict # Check that we have the expected parameter properties # VectorRetriever has all optional parameters (query_vector and query_text are both optional) expected_properties = { "query_vector", "query_text", "top_k", "effective_search_ratio", "filters", } actual_properties = set(parameters_dict.get("properties", {}).keys()) assert ( expected_properties == actual_properties ), f"Expected {expected_properties}, got {actual_properties}" # Check additionalProperties if it exists if "additionalProperties" in parameters_dict and not parameters_dict.get( "additionalProperties" ): pass # This line is just to satisfy the test, actual check is visual