imbajin commented on code in PR #372:
URL: https://github.com/apache/hugegraph-ai/pull/372#discussion_r4281190394
##########
hugegraph-llm/src/hugegraph_llm/nodes/llm_node/extract_info.py:
##########
@@ -47,8 +53,41 @@ def node_init(self):
return super().node_init()
def operator_schedule(self, data_json):
+ if self.wk_input.review_options is not None:
+ return self._extract_with_review(data_json)
if self.extract_type == "triples":
return self.info_extract.run(data_json)
if self.extract_type == "property_graph":
return self.property_graph_extract.run(data_json)
raise ValueError("Unsupport extract type")
+
+ def _extract_with_review(self, data_json):
+ chunks = [ChunkInput(index=index, text=text) for index, text in
enumerate(data_json.get("chunks", []))]
+ if not chunks:
+ raise ValueError("No text chunks available for graph extraction")
+ schema = data_json["schema"]
+ example_prompt = self.wk_input.example_prompt
+
+ def prepare(_chunk):
+ # Clients and bundles belong to one chunk; workers never mutate
workflow state.
+ return PropertyGraphBundle(
+ llm=get_chat_llm(llm_settings, limit_output=False,
raise_on_truncation=True),
+ schema=schema,
+ example_prompt=example_prompt,
+ )
+
+ results = run_chunks_v1(chunks=chunks, prepare=prepare,
options=self.wk_input.review_options)
Review Comment:
⚠️ Important. Blocking: no. Summary: Chunks in one request may be processed
with different LLM configurations. Evidence: this batch creates a fresh client
in each worker by rereading mutable `llm_settings`; `/config/llm` mutates
provider/model/key fields under `runtime_config_lock`, but this path takes no
snapshot or lock. If settings change while a multi-chunk request is running,
later workers may use a different provider or credentials. Please capture the
config once under the existing lock before dispatch and use that snapshot for
every chunk.
##########
hugegraph-llm/src/hugegraph_llm/extraction_runtime/property_graph.py:
##########
@@ -0,0 +1,298 @@
+# 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.
+
+"""Property-graph extraction and review using the configured model client."""
+
+import json
+import math
+import re
+from copy import deepcopy
+from dataclasses import asdict
+from datetime import datetime
+
+from hugegraph_llm.config import prompt
+from hugegraph_llm.extraction_runtime.v1.contracts import ChunkInput, Issue,
ReviewResult
+from hugegraph_llm.models.llms.base import BaseLLM
+from hugegraph_llm.operators.llm_op.property_graph_extract import (
+ PropertyGraphExtract,
+ generate_extract_property_graph_prompt,
+)
+
+
+def _invalid_constant(value: str):
+ raise ValueError(f"Invalid JSON constant: {value}")
+
+
+def _parse_float(value: str) -> float:
+ number = float(value)
+ if not math.isfinite(number):
+ raise ValueError(f"Non-finite JSON number: {value}")
+ return number
+
+
+def _parse_json(response: str):
+ text = response.strip()
+ fenced = re.fullmatch(r"```(?:json)?\s*\n?(.*?)\s*```", text, re.DOTALL |
re.IGNORECASE)
+ if fenced:
+ text = fenced.group(1)
+ return json.loads(text, parse_constant=_invalid_constant,
parse_float=_parse_float)
+
+
+def _parse_graph(response: str) -> dict:
+ graph = _parse_json(response)
+ if isinstance(graph, list):
+ grouped = {"vertices": [], "edges": []}
+ for item in graph:
+ if not isinstance(item, dict) or item.get("type") not in
("vertex", "edge"):
+ raise ValueError("Flat graph entries must declare type vertex
or edge")
+ grouped["vertices" if item["type"] == "vertex" else
"edges"].append(item)
+ graph = grouped
+ if not isinstance(graph, dict) or any(not isinstance(graph.get(key), list)
for key in ("vertices", "edges")):
+ raise ValueError("Graph response must contain vertices and edges
arrays")
+ for key, kind in (("vertices", "vertex"), ("edges", "edge")):
+ for item in graph[key]:
+ if isinstance(item, dict):
+ item.setdefault("type", kind)
+ return graph
+
+
+def _valid_id(value) -> bool:
+ return (isinstance(value, str) and bool(value)) or (isinstance(value, int)
and not isinstance(value, bool))
+
+
+def _valid_scalar(value, data_type: str) -> bool:
+ if data_type in ("TEXT", "UUID"):
Review Comment:
⚠️ Important. Blocking: yes. Summary: The schema validator can mark
malformed UUIDs as valid and return them in a `final` graph. Evidence:
`_valid_scalar` handles `UUID` exactly like `TEXT` and only checks for a
string; the vertex ID checks below cover `CUSTOMIZE_STRING` and
`CUSTOMIZE_NUMBER` but omit `CUSTOMIZE_UUID`. HugeGraph parses UUID properties
through `DataType.valueToUUID` and `StringEncoding.uuid`, and custom UUID IDs
through `IdGenerator.of(..., true)`, which reject invalid strings. Please
validate both UUID properties and `CUSTOMIZE_UUID` IDs with
HugeGraph-compatible parsing and add invalid-value cases.
##########
hugegraph-llm/src/hugegraph_llm/nodes/llm_node/extract_info.py:
##########
@@ -47,8 +53,41 @@ def node_init(self):
return super().node_init()
def operator_schedule(self, data_json):
+ if self.wk_input.review_options is not None:
+ return self._extract_with_review(data_json)
if self.extract_type == "triples":
return self.info_extract.run(data_json)
if self.extract_type == "property_graph":
return self.property_graph_extract.run(data_json)
raise ValueError("Unsupport extract type")
+
+ def _extract_with_review(self, data_json):
+ chunks = [ChunkInput(index=index, text=text) for index, text in
enumerate(data_json.get("chunks", []))]
+ if not chunks:
+ raise ValueError("No text chunks available for graph extraction")
+ schema = data_json["schema"]
+ example_prompt = self.wk_input.example_prompt
+
+ def prepare(_chunk):
+ # Clients and bundles belong to one chunk; workers never mutate
workflow state.
+ return PropertyGraphBundle(
+ llm=get_chat_llm(llm_settings, limit_output=False,
raise_on_truncation=True),
Review Comment:
⚠️ Important. Blocking: yes. Summary: Reviewed extraction can mark truncated
Ollama output as `final`. Evidence: `raise_on_truncation=True` is forwarded by
the factory only for OpenAI and LiteLLM; `ollama/local` returns `OllamaClient`,
whose `generate()` drops `response["done_reason"]` and returns only content.
Ollama exposes `done_reason=length`, so a truncated output that remains
parseable can pass this flow. Either handle that reason in Ollama or reject
Ollama for reviewed extraction with a clear error and document the provider
restriction.
--
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]