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]

Reply via email to