LRriver commented on code in PR #361:
URL: https://github.com/apache/hugegraph-ai/pull/361#discussion_r3492226779
##########
hugegraph-llm/src/tests/operators/hugegraph_op/test_commit_to_hugegraph.py:
##########
@@ -303,6 +321,293 @@ def test_init_schema_if_need(self,
mock_handle_graph_creation, mock_create_prope
# Verify that edgeLabel was called for each edge label
self.assertEqual(schema_mocks["edge_label"].call_count, 1) # 1 edge
label
+
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._check_property_data_type")
+
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
+ def test_load_into_graph(self, mock_handle_graph_creation,
mock_check_property_data_type):
+ """Test load_into_graph method."""
+ # Setup mocks
+ mock_handle_graph_creation.return_value = MagicMock(id="vertex_id")
+ mock_check_property_data_type.return_value = True
+
+ # Create vertices with proper data types according to schema
+ vertices = [
+ {"label": "person", "properties": {"name": "Tom Hanks", "age":
67}},
+ {"label": "movie", "properties": {"title": "Forrest Gump", "year":
1994}},
+ ]
+
+ edges = [
+ {
+ "label": "acted_in",
+ "properties": {"role": "Forrest Gump"},
+ "outV": "person:Tom Hanks", # Use the format expected by the
implementation
+ "inV": "movie:Forrest Gump", # Use the format expected by the
implementation
+ }
+ ]
+
+ # Call the method
+ self.commit2graph.load_into_graph(vertices, edges, self.schema)
+
+ # Verify that _handle_graph_creation was called for each vertex and
edge
+ self.assertEqual(mock_handle_graph_creation.call_count, 3) # 2
vertices + 1 edge
+
+
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._check_property_data_type")
+
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
+ def test_load_into_graph_returns_actual_created_and_skipped_counts(
+ self, mock_handle_graph_creation, mock_check_property_data_type
+ ):
+ """Test import stats reflect actual created and skipped graph
elements."""
+ mock_handle_graph_creation.side_effect = [
+ MagicMock(id="person:Tom Hanks"),
+ None,
+ ]
+ mock_check_property_data_type.return_value = True
+ vertices = [
+ {"label": "person", "properties": {"name": "Tom Hanks", "age":
67}},
+ {"label": "unknown", "properties": {"name": "Ignored"}},
+ ]
+ edges = [
+ {
+ "label": "acted_in",
+ "properties": {"role": "Forrest Gump"},
+ "outV": "person:Tom Hanks",
+ "inV": "movie:Forrest Gump",
+ }
+ ]
+
+ result = self.commit2graph.load_into_graph(vertices, edges,
self.schema)
+
+ self.assertEqual(result["vertices_attempted"], 2)
+ self.assertEqual(result["vertices_created"], 1)
+ self.assertEqual(result["vertices_skipped"], 1)
+ self.assertEqual(result["edges_attempted"], 1)
+ self.assertEqual(result["edges_created"], 0)
+ self.assertEqual(result["edges_skipped"], 1)
+ self.assertIn(
+ {"kind": "vertex", "index": 1, "reason": "vertex_label_not_found",
"label": "unknown"},
+ result["errors"],
+ )
+
+
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
+ def test_load_into_graph_with_data_type_validation_success(self,
mock_handle_graph_creation):
+ """Test load_into_graph method with successful data type validation."""
+ # Setup mocks
+ mock_handle_graph_creation.return_value = MagicMock(id="vertex_id")
+
+ # Create vertices with correct data types matching schema expectations
+ vertices = [
+ {"label": "person", "properties": {"name": "Tom Hanks", "age":
67}}, # age: INT -> int
+ {"label": "movie", "properties": {"title": "Forrest Gump", "year":
1994}}, # year: INT -> int
+ ]
+
+ edges = [
+ {
+ "label": "acted_in",
+ "properties": {"role": "Forrest Gump"}, # role: TEXT -> str
+ "outV": "person:Tom Hanks",
+ "inV": "movie:Forrest Gump",
+ }
+ ]
+
+ # Call the method - should succeed with correct data types
+ self.commit2graph.load_into_graph(vertices, edges, self.schema)
+
+ # Verify that _handle_graph_creation was called for each vertex and
edge
+ self.assertEqual(mock_handle_graph_creation.call_count, 3) # 2
vertices + 1 edge
+
+
@patch("hugegraph_llm.operators.hugegraph_op.commit_to_hugegraph.Commit2Graph._handle_graph_creation")
+ def test_load_into_graph_maps_llm_vertex_ids_to_created_vertex_ids(self,
mock_handle_graph_creation):
+ """Test edges use server-created vertex ids when LLM ids differ."""
+ mock_handle_graph_creation.side_effect = [
+ MagicMock(id="1:Tom Hanks"),
+ MagicMock(id="2:Forrest Gump"),
+ MagicMock(id="edge_id"),
+ ]
+
+ vertices = [
+ {
+ "id": "person:Tom Hanks",
+ "label": "person",
+ "properties": {"name": "Tom Hanks", "age": 67},
+ },
Review Comment:
Fixed in 10524d2. init_schema_if_need() now branches on id_strategy and uses
useCustomizeStringId() for CUSTOMIZE_STRING vertex labels instead of creating a
primary-key schema first. Added a Commit2Graph.run() regression test so the
production init_schema_if_need() path is covered before load_into_graph().
##########
hugegraph-llm/src/hugegraph_llm/services/graph_extract_service.py:
##########
@@ -0,0 +1,376 @@
+# 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.
+
+import json
+import time
+from typing import Any, Dict, List, Optional
+
+from hugegraph_llm.api.models.graph_extract_requests import (
+ GraphExtractAndImportRequest,
+ GraphExtractRequest,
+ GraphImportRequest,
+ SchemaInput,
+ _validate_schema_value,
+)
+from hugegraph_llm.api.models.graph_extract_responses import (
+ GraphExtractResponse,
+ GraphImportResponse,
+)
+from hugegraph_llm.config import prompt
+from hugegraph_llm.flows import FlowName
+from hugegraph_llm.flows.scheduler import SchedulerSingleton
+from hugegraph_llm.utils.log import log
+
+SENSITIVE_CLIENT_CONFIG_KEYS = {"pwd", "password", "token", "api_key",
"secret"}
+SAFE_IMPORT_ERROR_KEYS = {"kind", "index", "label", "key", "reason"}
+
+
+class FlowOutputValidationError(ValueError):
+ """Raised when a workflow returns malformed output."""
+
+
+def normalize_schema(schema: SchemaInput) -> str:
+ schema = _validate_schema_value(schema)
+ if isinstance(schema, dict):
+ return json.dumps(schema, ensure_ascii=False)
+
+ schema_text = str(schema).strip()
+ if schema_text.startswith("{"):
+ try:
+ parsed_schema = json.loads(schema_text)
+ except json.JSONDecodeError as exc:
+ raise ValueError(f"schema must be valid JSON: {exc.msg}") from exc
+ return json.dumps(parsed_schema, ensure_ascii=False)
+ return schema_text
+
+
+def _redact_client_config(client_config) -> Dict[str, Any]:
+ if client_config is None:
+ return {}
+ config = client_config if isinstance(client_config, dict) else
client_config.model_dump(exclude_none=True)
+ return {key: ("***" if key in SENSITIVE_CLIENT_CONFIG_KEYS and value else
value) for key, value in config.items()}
+
+
+def _schema_graph_name(schema: str) -> Optional[str]:
+ schema_text = str(schema).strip()
+ return None if schema_text.startswith("{") else schema_text
+
+
+def apply_client_config(
+ client_config,
+ schema: Optional[str] = None,
+ align_graph_with_schema: bool = False,
+) -> Optional[Dict[str, Any]]:
+ if client_config is None:
+ config = {}
+ elif isinstance(client_config, dict):
+ config = {key: value for key, value in client_config.items() if value
is not None}
+ else:
+ config = client_config.model_dump(exclude_none=True)
+ schema_graph = _schema_graph_name(schema) if schema else None
+ if schema_graph:
+ target_graph = config.get("graph")
+ if target_graph and target_graph != schema_graph:
+ raise ValueError("schema graph name must match
client_config.graph")
+ if align_graph_with_schema:
+ config["graph"] = schema_graph
+ return config or None
+
+
+def _parse_flow_json(raw_result: Any, error_message: str) -> Dict[str, Any]:
+ if isinstance(raw_result, dict):
+ return raw_result
+ if not isinstance(raw_result, str):
+ raise FlowOutputValidationError(error_message)
+ try:
+ parsed = json.loads(raw_result)
+ except json.JSONDecodeError as exc:
+ raise FlowOutputValidationError(error_message) from exc
+ if not isinstance(parsed, dict):
+ raise FlowOutputValidationError(error_message)
+ return parsed
+
+
+def _pop_warnings(result: Dict[str, Any]) -> List[str]:
+ warnings = []
+ warning = result.pop("warning", None)
+ if warning:
+ warnings.append(str(warning))
+ extra_warnings = result.pop("warnings", None)
+ if isinstance(extra_warnings, list):
+ warnings.extend(str(item) for item in extra_warnings)
+ elif extra_warnings:
+ warnings.append(str(extra_warnings))
+ return warnings
+
+
+def _count_items(result: Dict[str, Any], key: str) -> int:
+ value = result.get(key)
+ return len(value) if isinstance(value, list) else 0
+
+
+def _validate_property_graph_result(result: Dict[str, Any]) -> None:
+ vertices = result.get("vertices", [])
+ edges = result.get("edges", [])
+ if not isinstance(vertices, list) or not isinstance(edges, list):
+ raise FlowOutputValidationError("property graph result must contain
list vertices and edges")
+ for vertex in vertices:
+ if not isinstance(vertex, dict) or "label" not in vertex or
"properties" not in vertex:
Review Comment:
Fixed in 10524d2. GraphExtractService now validates workflow property-graph
output against the import contract: vertex label must be a non-empty string,
vertex properties must be an object, edge label/outV/outVLabel/inV/inVLabel
must be non-empty strings, and edge properties must be an object. Added
endpoint/service regression coverage where the scheduler returns
properties=null and /graph/extract responds with
GRAPH_EXTRACT_INVALID_FLOW_OUTPUT.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]