This is an automated email from the ASF dual-hosted git repository.
imbajin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hugegraph-ai.git
The following commit(s) were added to refs/heads/main by this push:
new b63cc69b fix(llm): handle empty gremlin examples when building index
(#323)
b63cc69b is described below
commit b63cc69b957267c4d1238ba9634c68b7cffbc987
Author: Gardenia-zx <[email protected]>
AuthorDate: Sat Jun 20 22:58:54 2026 +0800
fix(llm): handle empty gremlin examples when building index (#323)
## Purpose
This PR handles the empty examples case in `BuildGremlinExampleIndex`.
Previously, when `examples` was empty, `run()` could access
`examples_embedding[0]` and raise an `IndexError`.
---------
Co-authored-by: imbajin <[email protected]>
---
.../index_op/build_gremlin_example_index.py | 17 ++++--
.../src/tests/api/test_graph_extract_api.py | 2 +-
.../test_build_gremlin_example_index_node.py | 37 +++++++++++++
.../index_op/test_build_gremlin_example_index.py | 60 ++++++++++++++++++----
pyproject.toml | 1 +
5 files changed, 100 insertions(+), 17 deletions(-)
diff --git
a/hugegraph-llm/src/hugegraph_llm/operators/index_op/build_gremlin_example_index.py
b/hugegraph-llm/src/hugegraph_llm/operators/index_op/build_gremlin_example_index.py
index fb385a59..8e39e295 100644
---
a/hugegraph-llm/src/hugegraph_llm/operators/index_op/build_gremlin_example_index.py
+++
b/hugegraph-llm/src/hugegraph_llm/operators/index_op/build_gremlin_example_index.py
@@ -38,14 +38,21 @@ class BuildGremlinExampleIndex:
self.vector_index = vector_index
def run(self, context: Dict[str, Any]) -> Dict[str, Any]:
- # !: We have assumed that self.example is not empty
+ if not self.examples:
+ context["embed_dim"] = 0
+ return context
+
queries = [example["query"] for example in self.examples]
# TODO: refactor function chain async to avoid blocking
examples_embedding =
asyncio.run(get_embeddings_parallel(self.embedding, queries))
+
+ if not examples_embedding:
+ raise ValueError(f"Embedding service returned empty result for
{len(self.examples)} gremlin examples")
+
embed_dim = len(examples_embedding[0])
- if len(self.examples) > 0:
- vector_index = self.vector_index.from_name(embed_dim,
self.vector_index_name)
- vector_index.add(examples_embedding, self.examples)
- vector_index.save_index_by_name(self.vector_index_name)
+ vector_index = self.vector_index.from_name(embed_dim,
self.vector_index_name)
+ vector_index.add(examples_embedding, self.examples)
+ vector_index.save_index_by_name(self.vector_index_name)
+
context["embed_dim"] = embed_dim
return context
diff --git a/hugegraph-llm/src/tests/api/test_graph_extract_api.py
b/hugegraph-llm/src/tests/api/test_graph_extract_api.py
index 29a3a827..2f7cdc3f 100644
--- a/hugegraph-llm/src/tests/api/test_graph_extract_api.py
+++ b/hugegraph-llm/src/tests/api/test_graph_extract_api.py
@@ -389,7 +389,7 @@ def test_existing_routes_still_register():
app = FastAPI()
app.include_router(router)
- paths = {route.path for route in app.routes if hasattr(route, "path")}
+ paths = set(app.openapi()["paths"])
assert "/rag" in paths
assert "/text2gremlin" in paths
assert "/config/graph" in paths
diff --git
a/hugegraph-llm/src/tests/nodes/index_node/test_build_gremlin_example_index_node.py
b/hugegraph-llm/src/tests/nodes/index_node/test_build_gremlin_example_index_node.py
new file mode 100644
index 00000000..34cf8342
--- /dev/null
+++
b/hugegraph-llm/src/tests/nodes/index_node/test_build_gremlin_example_index_node.py
@@ -0,0 +1,37 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+from unittest.mock import patch
+
+from hugegraph_llm.nodes.index_node.build_gremlin_example_index import
BuildGremlinExampleIndexNode
+from hugegraph_llm.state.ai_state import WkFlowInput, WkFlowState
+
+
+@patch("hugegraph_llm.nodes.index_node.build_gremlin_example_index.Embeddings")
+@patch("hugegraph_llm.utils.vector_index_utils.get_vector_index_class")
+def test_node_init_rejects_empty_examples(mock_get_vector_index_class,
mock_embeddings):
+ node = BuildGremlinExampleIndexNode()
+ node.wk_input = WkFlowInput()
+ node.wk_input.examples = []
+ node.context = WkFlowState()
+
+ status = node.node_init()
+
+ assert status.isErr()
+ mock_get_vector_index_class.assert_not_called()
+ mock_embeddings.assert_not_called()
+ assert not hasattr(node, "build_gremlin_example_index_op")
diff --git
a/hugegraph-llm/src/tests/operators/index_op/test_build_gremlin_example_index.py
b/hugegraph-llm/src/tests/operators/index_op/test_build_gremlin_example_index.py
index 239e377a..b1f855b6 100644
---
a/hugegraph-llm/src/tests/operators/index_op/test_build_gremlin_example_index.py
+++
b/hugegraph-llm/src/tests/operators/index_op/test_build_gremlin_example_index.py
@@ -54,7 +54,10 @@ class TestBuildGremlinExampleIndex(unittest.TestCase):
self.assertEqual(self.index_builder.vector_index_name,
"gremlin_examples")
@patch('asyncio.run')
- @patch('hugegraph_llm.utils.embedding_utils.get_embeddings_parallel')
+ @patch(
+
'hugegraph_llm.operators.index_op.build_gremlin_example_index.get_embeddings_parallel',
+ new_callable=MagicMock,
+ )
def test_run_with_examples(self, mock_get_embeddings_parallel,
mock_asyncio_run):
"""Test run method with examples"""
# Setup mocks
@@ -78,7 +81,10 @@ class TestBuildGremlinExampleIndex(unittest.TestCase):
self.assertEqual(context["embed_dim"], 3)
@patch('asyncio.run')
- @patch('hugegraph_llm.utils.embedding_utils.get_embeddings_parallel')
+ @patch(
+
'hugegraph_llm.operators.index_op.build_gremlin_example_index.get_embeddings_parallel',
+ new_callable=MagicMock,
+ )
def test_run_with_empty_examples(self, mock_get_embeddings_parallel,
mock_asyncio_run):
"""Test run method with empty examples"""
# Create new mocks for this test
@@ -91,19 +97,48 @@ class TestBuildGremlinExampleIndex(unittest.TestCase):
embedding=self.mock_embedding, examples=[],
vector_index=mock_vector_store_class
)
- # Setup mocks - empty embeddings
- test_embeddings = []
- mock_asyncio_run.return_value = test_embeddings
-
# Run the method
context = {}
- # This should raise an IndexError when trying to access
examples_embedding[0]
- with self.assertRaises(IndexError):
- empty_index_builder.run(context)
+ # Empty examples should be handled gracefully without building vector
index.
+ result = empty_index_builder.run(context)
+
+ mock_asyncio_run.assert_not_called()
+ mock_get_embeddings_parallel.assert_not_called()
+ mock_vector_store_class.clean.assert_not_called()
+ mock_vector_store_class.from_name.assert_not_called()
+ mock_vector_store_instance.add.assert_not_called()
+ mock_vector_store_instance.save_index_by_name.assert_not_called()
+
+ self.assertEqual(result["embed_dim"], 0)
+ self.assertEqual(context["embed_dim"], 0)
+
+ @patch('asyncio.run')
+ @patch(
+
'hugegraph_llm.operators.index_op.build_gremlin_example_index.get_embeddings_parallel',
+ new_callable=MagicMock,
+ )
+ def test_run_with_empty_embeddings(self, mock_get_embeddings_parallel,
mock_asyncio_run):
+ """Test run method when embedding returns no vectors"""
+ mock_asyncio_run.return_value = []
+
+ context = {}
+ with self.assertRaisesRegex(ValueError, "Embedding service returned
empty result"):
+ self.index_builder.run(context)
+
+ mock_asyncio_run.assert_called_once()
+ mock_get_embeddings_parallel.assert_called_once()
+ self.mock_vector_store_class.clean.assert_not_called()
+ self.mock_vector_store_class.from_name.assert_not_called()
+ self.mock_vector_store_instance.add.assert_not_called()
+ self.mock_vector_store_instance.save_index_by_name.assert_not_called()
+ self.assertNotIn("embed_dim", context)
@patch('asyncio.run')
- @patch('hugegraph_llm.utils.embedding_utils.get_embeddings_parallel')
+ @patch(
+
'hugegraph_llm.operators.index_op.build_gremlin_example_index.get_embeddings_parallel',
+ new_callable=MagicMock,
+ )
def test_run_single_example(self, mock_get_embeddings_parallel,
mock_asyncio_run):
"""Test run method with single example"""
# Create new mocks for this test
@@ -134,7 +169,10 @@ class TestBuildGremlinExampleIndex(unittest.TestCase):
self.assertEqual(result["embed_dim"], 4)
@patch('asyncio.run')
- @patch('hugegraph_llm.utils.embedding_utils.get_embeddings_parallel')
+ @patch(
+
'hugegraph_llm.operators.index_op.build_gremlin_example_index.get_embeddings_parallel',
+ new_callable=MagicMock,
+ )
def test_run_preserves_existing_context(self,
mock_get_embeddings_parallel, mock_asyncio_run):
"""Test that run method preserves existing context data"""
# Setup mocks
diff --git a/pyproject.toml b/pyproject.toml
index c2feda54..bb63371f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -42,6 +42,7 @@ vermeer = ["vermeer-python-client"]
dev = [
"pytest~=8.0.0",
"pytest-cov~=5.0.0",
+ "coverage[toml]==7.10.4",
"pylint~=3.0.0",
"ruff>=0.11.0",
"mypy>=1.16.1",