Repository: neo4j/neo4j-graphrag-python

Issue:
Matching between neo4j and qdrant ids fails
Hello, 

in the QdrantNeo4jRetriever class (qdrant.py file) in line 214, it tries to append:

`[f"{point.payload[self.id_property_external]}", point.score]`

That means, even if the id_property_external from qdrant is its "id", it will look for `point.payload("id")`. However, in the case of 'id', it is a separate attribute of a qdrant point object and not contained in the payload itself ({"id": ..., "payload": {"page_content": ..., "vector": ..., "metadata": ..., ...}). So the correct way to retrieve it would be `point.id` instead (for all other cases the current way should work).

Error message:
```
File "C:Users\...\Lib\site-packages\neo4j_graphrag\retrievers\external\qdrant\qdrant.py", line 214, in get_search_results
    [f"{point.payload[self.id_property_external]}", point.score]
        ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
KeyError: 'id'
```

Hints from issue comments:
@stellasia: Hi @keichho ,

Thank you for raising this, we'll investigate. 

(@Anush008, tagging you only because you added the Qdrant support, maybe you have relevant comment for this issue)@Anush008: #303 should resolve this.

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8123110..09d264a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -309,3 +309,4 @@
 
 -   Updated documentation to include new custom exceptions.
 -   Improved the use of Pydantic for input data validation for retriever objects.
+-   Fixed config loading after module reload (usage in jupyter notebooks)
diff --git a/src/neo4j_graphrag/experimental/pipeline/component.py b/src/neo4j_graphrag/experimental/pipeline/component.py
index 84cd5bc..9087773 100644
--- a/src/neo4j_graphrag/experimental/pipeline/component.py
+++ b/src/neo4j_graphrag/experimental/pipeline/component.py
@@ -21,6 +21,7 @@ from typing import Any, get_type_hints
 from pydantic import BaseModel
 
 from neo4j_graphrag.experimental.pipeline.exceptions import PipelineDefinitionError
+from neo4j_graphrag.utils.validation import issubclass_safe
 
 
 class DataModel(BaseModel):
@@ -52,7 +53,7 @@ class ComponentMeta(abc.ABCMeta):
                     f"The run method return type must be annotated in {name}"
                 )
             # the type hint must be a subclass of DataModel
-            if not issubclass(return_model, DataModel):
+            if not issubclass_safe(return_model, DataModel):
                 raise PipelineDefinitionError(
                     f"The run method must return a subclass of DataModel in {name}"
                 )
diff --git a/src/neo4j_graphrag/experimental/pipeline/config/object_config.py b/src/neo4j_graphrag/experimental/pipeline/config/object_config.py
index deeee20..95d6988 100644
--- a/src/neo4j_graphrag/experimental/pipeline/config/object_config.py
+++ b/src/neo4j_graphrag/experimental/pipeline/config/object_config.py
@@ -56,6 +56,8 @@ from neo4j_graphrag.experimental.pipeline.config.param_resolver import (
     ParamConfig,
 )
 from neo4j_graphrag.llm import LLMInterface
+from neo4j_graphrag.utils.validation import issubclass_safe
+
 
 logger = logging.getLogger(__name__)
 
@@ -131,9 +133,9 @@ class ObjectConfig(AbstractConfig, Generic[T]):
         self._global_data = resolved_data or {}
         logger.debug(f"OBJECT_CONFIG: parsing {self} using {resolved_data}")
         if self.class_ is None:
-            raise ValueError(f"`class_` is not required to parse object {self}")
+            raise ValueError(f"`class_` is required to parse object {self}")
         klass = self._get_class(self.class_, self.get_module())
-        if not issubclass(klass, self.get_interface()):
+        if not issubclass_safe(klass, self.get_interface()):
             raise ValueError(
                 f"Invalid class '{klass}'. Expected a subclass of '{self.get_interface()}'"
             )
diff --git a/src/neo4j_graphrag/retrievers/external/qdrant/qdrant.py b/src/neo4j_graphrag/retrievers/external/qdrant/qdrant.py
index f0e366a..ecd3dd2 100644
--- a/src/neo4j_graphrag/retrievers/external/qdrant/qdrant.py
+++ b/src/neo4j_graphrag/retrievers/external/qdrant/qdrant.py
@@ -211,7 +211,7 @@ class QdrantNeo4jRetriever(ExternalRetriever):
         for point in points:
             assert point.payload is not None
             result_tuples.append(
-                [f"{point.payload[self.id_property_external]}", point.score]
+                [point.payload.get(self.id_property_external, point.id), point.score]
             )
 
         search_query = get_match_query(
diff --git a/src/neo4j_graphrag/utils/validation.py b/src/neo4j_graphrag/utils/validation.py
index e86f758..f1cd466 100644
--- a/src/neo4j_graphrag/utils/validation.py
+++ b/src/neo4j_graphrag/utils/validation.py
@@ -14,7 +14,8 @@
 #  limitations under the License.
 from __future__ import annotations
 
-from typing import Optional
+import importlib
+from typing import Optional, Tuple, Union, cast, Type
 
 
 def validate_search_query_input(
@@ -22,3 +23,23 @@ def validate_search_query_input(
 ) -> None:
     if not (bool(query_vector) ^ bool(query_text)):
         raise ValueError("You must provide exactly one of query_vector or query_text.")
+
+
+def issubclass_safe(
+    cls: Type[object], class_or_tuple: Union[Type[object], Tuple[Type[object]]]
+) -> bool:
+    if isinstance(class_or_tuple, tuple):
+        return any(issubclass_safe(cls, base) for base in class_or_tuple)
+
+    if issubclass(cls, class_or_tuple):
+        return True
+
+    # Handle case where module was reloaded
+    cls_module = importlib.import_module(cls.__module__)
+    # Get the latest version of the base class from the module
+    latest_base = getattr(cls_module, class_or_tuple.__name__, None)
+    latest_base = cast(Union[tuple[Type[object], ...], Type[object]], latest_base)
+    if issubclass(cls, latest_base):
+        return True
+
+    return False


Test patch:
diff --git a/tests/unit/experimental/pipeline/config/test_object_config.py b/tests/unit/experimental/pipeline/config/test_object_config.py
index baa3c76..c39dcb8 100644
--- a/tests/unit/experimental/pipeline/config/test_object_config.py
+++ b/tests/unit/experimental/pipeline/config/test_object_config.py
@@ -12,6 +12,10 @@
 #  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 importlib
+import sys
+from abc import ABC
+from typing import ClassVar
 from unittest.mock import patch
 
 import neo4j
@@ -58,6 +62,30 @@ def test_get_class_wrong_path() -> None:
         c._get_class("MyClass")
 
 
+class _MyClass:
+    def __init__(self, param: str) -> None:
+        self.param = param
+
+
+class _MyInterface(ABC): ...
+
+
+def test_parse_after_module_reload() -> None:
+    class MyClassConfig(ObjectConfig[_MyClass]):
+        DEFAULT_MODULE: ClassVar[str] = __name__
+        INTERFACE: ClassVar[type] = _MyClass
+
+    param_value = "value"
+    config = MyClassConfig.model_validate(
+        {"class_": f"{__name__}.{_MyClass.__name__}", "params_": {"param": param_value}}
+    )
+    importlib.reload(sys.modules[__name__])
+
+    my_obj = config.parse()
+    assert isinstance(my_obj, _MyClass)
+    assert my_obj.param == param_value
+
+
 def test_neo4j_driver_config() -> None:
     config = Neo4jDriverConfig.model_validate(
         {