LRriver commented on code in PR #361:
URL: https://github.com/apache/hugegraph-ai/pull/361#discussion_r3492228296


##########
hugegraph-llm/src/tests/api/test_graph_extract_jobs.py:
##########
@@ -0,0 +1,243 @@
+# 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 concurrent.futures import ThreadPoolExecutor
+from datetime import timedelta
+from queue import Full
+from unittest.mock import Mock
+
+from fastapi import APIRouter, FastAPI, status
+from fastapi.testclient import TestClient
+
+from hugegraph_llm.api.graph_extract_api import graph_extract_http_api
+from hugegraph_llm.api.models.graph_extract_responses import 
GraphExtractResponse
+from hugegraph_llm.services.graph_extract_jobs import GraphExtractJobStatus, 
InMemoryGraphExtractJobStore
+
+
+def _payload():
+    return {
+        "texts": ["marko knows vadas"],
+        "schema": {
+            "vertexlabels": [{"name": "person", "properties": ["name"]}],
+            "edgelabels": [{"name": "knows", "source_label": "person", 
"target_label": "person"}],
+        },
+        "example_prompt": "extract graph",
+    }
+
+
+def _client(service=None, job_store=None, run_jobs_inline=True):

Review Comment:
   Fixed in 10524d2. Added a route-level test for the production default 
run_jobs_inline=None path: it creates a job, lets the background worker execute 
it, polls until SUCCEEDED, then retrieves the result from 
/graph/extract/jobs/{job_id}/result.



##########
hugegraph-llm/src/hugegraph_llm/api/graph_extract_api.py:
##########
@@ -15,55 +15,352 @@
 # specific language governing permissions and limitations
 # under the License.
 
-import json
+from typing import Optional
 
-from fastapi import APIRouter, HTTPException, status
+from fastapi import APIRouter, HTTPException, Request, status
+from fastapi.exceptions import RequestValidationError
+from fastapi.responses import JSONResponse
+from fastapi.routing import APIRoute
 
-from hugegraph_llm.api.models.graph_extract_requests import GraphExtractRequest
-from hugegraph_llm.api.models.graph_extract_responses import 
GraphExtractResponse
-from hugegraph_llm.config import prompt
-from hugegraph_llm.flows import FlowName
-from hugegraph_llm.flows.scheduler import SchedulerSingleton
+from hugegraph_llm.api.models.graph_extract_requests import (
+    GraphExtractAndImportRequest,
+    GraphExtractRequest,
+    GraphImportRequest,
+)
+from hugegraph_llm.api.models.graph_extract_responses import (
+    GraphExtractAndImportResponse,
+    GraphExtractError,
+    GraphExtractJobCreateResponse,
+    GraphExtractJobStatusResponse,
+    GraphExtractResponse,
+    GraphImportResponse,
+)
+from hugegraph_llm.services.graph_extract_jobs import (
+    GraphExtractJob,
+    GraphExtractJobStatus,
+    InMemoryGraphExtractJobStore,
+)
+from hugegraph_llm.services.graph_extract_service import (
+    FlowOutputValidationError,
+    GraphExtractService,
+    GraphImportService,
+)
 from hugegraph_llm.utils.log import log
 
+GRAPH_EXTRACT_FLOW_OUTPUT_ERROR = "Graph extraction flow output is invalid"
+GRAPH_EXTRACT_RUNTIME_ERROR = "Graph extraction failed during execution"
+GRAPH_IMPORT_FLOW_OUTPUT_ERROR = "Graph import flow output is invalid"
+GRAPH_IMPORT_RUNTIME_ERROR = "Graph import failed during execution"
 
-class GraphExtractService:
-    @staticmethod
-    def extract_sync(req: GraphExtractRequest) -> GraphExtractResponse:
+
+def _error(code: str, message: str, phase: str, job_id: Optional[str] = None) 
-> dict:
+    return GraphExtractError(code=code, message=message, phase=phase, 
job_id=job_id).model_dump(exclude_none=True)
+
+
+def _job_ts(value) -> Optional[str]:
+    return value.isoformat() if value else None
+
+
+def _job_status_response(job: GraphExtractJob) -> 
GraphExtractJobStatusResponse:
+    return GraphExtractJobStatusResponse(
+        job_id=job.job_id,
+        status=job.status,
+        created_at=_job_ts(job.created_at),
+        updated_at=_job_ts(job.updated_at),
+        started_at=_job_ts(job.started_at),
+        finished_at=_job_ts(job.finished_at),
+        expires_at=_job_ts(job.expires_at),
+        error=job.error,
+    )
+
+
+def _validation_message(errors) -> str:
+    details = []
+    for error in errors:
+        loc = ".".join(str(part) for part in error.get("loc", []) if part not 
in {"body"})
+        msg = error.get("msg", "invalid input")
+        err_type = error.get("type", "validation_error")
+        details.append(f"{loc or 'request'}: {msg} ({err_type})")
+    return "; ".join(details) or "request validation failed"
+
+
+class GraphExtractAPIRoute(APIRoute):
+    def get_route_handler(self):
+        original_route_handler = super().get_route_handler()
+
+        async def custom_route_handler(request: Request):
+            try:
+                return await original_route_handler(request)
+            except RequestValidationError as exc:
+                return JSONResponse(
+                    status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
+                    content={
+                        "detail": _error(

Review Comment:
   Fixed in 10524d2. GraphExtractAPIRoute now maps request-validation errors by 
route path. Invalid /graph/import bodies return GRAPH_IMPORT_VALIDATION_ERROR 
with phase=import instead of the extract-specific validation code. Added 
endpoint coverage that an invalid import payload is rejected before 
GraphImportService is called.



##########
hugegraph-llm/src/hugegraph_llm/operators/hugegraph_op/commit_to_hugegraph.py:
##########
@@ -27,31 +27,109 @@
 
 
 class Commit2Graph:
-    def __init__(self):
+    def __init__(self, graph_config=None):
+        graph_config = graph_config or {}
+
+        def pick(key, default):
+            value = graph_config[key] if key in graph_config else default
+            return default if value is None else value
+
         self.client = PyHugeClient(
-            url=huge_settings.graph_url,
-            graph=huge_settings.graph_name,
-            user=huge_settings.graph_user,
-            pwd=huge_settings.graph_pwd,
-            graphspace=huge_settings.graph_space,
+            url=pick("url", huge_settings.graph_url),
+            graph=pick("graph", huge_settings.graph_name),
+            user=pick("user", huge_settings.graph_user),
+            pwd=pick("pwd", huge_settings.graph_pwd),
+            graphspace=pick("gs", huge_settings.graph_space),
         )
         self.schema = self.client.schema()
 
+    def _empty_import_result(self, vertices=None, edges=None, triples=None) -> 
Dict[str, Any]:
+        return {
+            "vertices_attempted": len(vertices or []),
+            "vertices_created": 0,
+            "vertices_skipped": 0,
+            "edges_attempted": len(edges or []),
+            "edges_created": 0,
+            "edges_skipped": 0,
+            "triples_attempted": len(triples or []),
+            "triples_created": 0,
+            "triples_skipped": 0,
+            "errors": [],
+        }
+
+    def _import_error(self, kind, index, reason, label=None, key=None) -> 
Dict[str, Any]:
+        error = {"kind": kind, "index": index, "reason": reason}
+        if label:
+            error["label"] = label
+        if key:
+            error["key"] = key
+        return error
+
+    def _validate_input_properties(
+        self,
+        kind,
+        index,
+        label,
+        properties,
+        allowed_properties,
+        property_label_map,
+        import_result,
+    ) -> bool:
+        allowed = set(allowed_properties)
+        skipped_key = "vertices_skipped" if kind == "vertex" else 
"edges_skipped"
+        for key, value in properties.items():
+            property_label = property_label_map.get(key)
+            if key not in allowed or property_label is None:
+                log.error(
+                    "(Input) %s property '%s' is not defined in schema label 
'%s', skip it & need check it again",
+                    kind,
+                    key,
+                    label,
+                )
+                import_result[skipped_key] += 1
+                import_result["errors"].append(self._import_error(kind, index, 
"unknown_property", label, key))
+                return False
+            # TODO: transform to Enum first (better in earlier step)
+            data_type = property_label["data_type"]
+            cardinality = property_label["cardinality"]
+            if not self._check_property_data_type(data_type, cardinality, 
value):

Review Comment:
   Fixed in 10524d2. Property value validation is now shared in 
hugegraph_llm.utils.schema_property and used by both GraphImportRequest and 
Commit2Graph. Integer types exclude bool, and FLOAT/DOUBLE accept JSON numeric 
int/float values while still excluding bool. Added request and operator 
coverage for INT=True, DOUBLE=1, and the named/internal import path that 
bypasses request-side inline-schema validation.



-- 
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