This is an automated email from the ASF dual-hosted git repository.
imbajin pushed a commit to branch goal-scan
in repository https://gitbox.apache.org/repos/asf/hugegraph-ai.git
The following commit(s) were added to refs/heads/goal-scan by this push:
new 902fd6b5 fix(llm): address follow-up review comments
902fd6b5 is described below
commit 902fd6b540ae24837d4cf9db4406868593e6915a
Author: imbajin <[email protected]>
AuthorDate: Wed Jun 3 12:50:36 2026 +0800
fix(llm): address follow-up review comments
- remove request-wide graph config lock from RAG API
- roll back provider type settings when config apply fails
- let graph import apply typed defaults for missing properties
- clarify scan report wording for post-scan fixes
---
.../code-scan/reports/final-code-scan-report.md | 2 +-
hugegraph-llm/src/hugegraph_llm/api/rag_api.py | 114 ++++++++++++---------
.../operators/llm_op/property_graph_extract.py | 11 +-
hugegraph-llm/src/tests/api/test_rag_api.py | 62 +++++++++++
.../llm_op/test_property_graph_extract.py | 11 +-
5 files changed, 140 insertions(+), 60 deletions(-)
diff --git a/.workflow/code-scan/reports/final-code-scan-report.md
b/.workflow/code-scan/reports/final-code-scan-report.md
index 808be10c..635dc66a 100644
--- a/.workflow/code-scan/reports/final-code-scan-report.md
+++ b/.workflow/code-scan/reports/final-code-scan-report.md
@@ -7,7 +7,7 @@ Scope: `hugegraph-llm`, `hugegraph-python-client`
The scan completed all planned lanes. The highest-risk result is one P0
security issue in the `hugegraph-llm` admin log API. The broader pattern is
that core boundary failures are often hidden: client transport errors are
retyped, provider failures are returned as normal text, graph/vector dependency
failures can be converted to empty results, and several integration tests use
local stand-ins rather than production flows.
-No behavior-changing source fixes were made. Only scan documents and `FIXME:`
comments were added.
+During the scan phase, no behavior-changing source fixes were made. Only scan
documents and `FIXME:` comments were added. Follow-up fixes are listed later in
this report.
## Scope Covered
diff --git a/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
b/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
index 0e76ca50..1ac934ca 100644
--- a/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
+++ b/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
@@ -17,7 +17,6 @@
import json
from contextlib import contextmanager
-from threading import RLock
from fastapi import APIRouter, HTTPException, status
@@ -42,7 +41,18 @@ _GRAPH_CONFIG_FIELD_MAP = {
"pwd": "graph_pwd",
"gs": "graph_space",
}
-_GRAPH_CONFIG_LOCK = RLock()
+_LLM_TYPE_FIELDS = ("chat_llm_type", "extract_llm_type", "text2gql_llm_type")
+_EMBEDDING_TYPE_FIELDS = ("embedding_type",)
+_RERANKER_TYPE_FIELDS = ("reranker_type",)
+
+
+def _snapshot_settings(settings, fields):
+ return {field: getattr(settings, field) for field in fields}
+
+
+def _restore_settings(settings, values):
+ for field, value in values.items():
+ setattr(settings, field, value)
# pylint: disable=too-many-statements
@@ -58,21 +68,16 @@ def rag_http_api(
):
@contextmanager
def request_graph_config(req):
- with _GRAPH_CONFIG_LOCK:
- original_values = {
- settings_field: getattr(huge_settings, settings_field)
- for settings_field in _GRAPH_CONFIG_FIELD_MAP.values()
- }
- try:
- client_config = getattr(req, "client_config", None)
- if client_config is not None:
- for request_field, settings_field in
_GRAPH_CONFIG_FIELD_MAP.items():
- if request_field in client_config.model_fields_set:
- setattr(huge_settings, settings_field,
getattr(client_config, request_field))
- yield
- finally:
- for settings_field, original_value in original_values.items():
- setattr(huge_settings, settings_field, original_value)
+ original_values = _snapshot_settings(huge_settings,
_GRAPH_CONFIG_FIELD_MAP.values())
+ try:
+ client_config = getattr(req, "client_config", None)
+ if client_config is not None:
+ for request_field, settings_field in
_GRAPH_CONFIG_FIELD_MAP.items():
+ if request_field in client_config.model_fields_set:
+ setattr(huge_settings, settings_field,
getattr(client_config, request_field))
+ yield
+ finally:
+ _restore_settings(huge_settings, original_values)
@router.post("/rag", status_code=status.HTTP_200_OK)
def rag_answer_api(req: RAGRequest):
@@ -182,43 +187,58 @@ def rag_http_api(
# TODO: restructure the implement of llm to three types, like
"/config/chat_llm"
@router.post("/config/llm", status_code=status.HTTP_201_CREATED)
def llm_config_api(req: LLMConfigRequest):
- llm_settings.chat_llm_type = req.llm_type
- llm_settings.extract_llm_type = req.llm_type
- llm_settings.text2gql_llm_type = req.llm_type
-
- if req.llm_type in ("openai", "litellm"):
- res = apply_llm_conf(
- req.api_key,
- req.api_base,
- req.language_model,
- req.max_tokens,
- origin_call="http",
- )
- else:
- res = apply_llm_conf(req.host, req.port, req.language_model, None,
origin_call="http")
- return generate_response(RAGResponse(status_code=res, message="Missing
Value"))
+ original_values = _snapshot_settings(llm_settings, _LLM_TYPE_FIELDS)
+ try:
+ llm_settings.chat_llm_type = req.llm_type
+ llm_settings.extract_llm_type = req.llm_type
+ llm_settings.text2gql_llm_type = req.llm_type
+
+ if req.llm_type in ("openai", "litellm"):
+ res = apply_llm_conf(
+ req.api_key,
+ req.api_base,
+ req.language_model,
+ req.max_tokens,
+ origin_call="http",
+ )
+ else:
+ res = apply_llm_conf(req.host, req.port, req.language_model,
None, origin_call="http")
+ return generate_response(RAGResponse(status_code=res,
message="Missing Value"))
+ except Exception:
+ _restore_settings(llm_settings, original_values)
+ raise
@router.post("/config/embedding", status_code=status.HTTP_201_CREATED)
def embedding_config_api(req: LLMConfigRequest):
- llm_settings.embedding_type = req.llm_type
+ original_values = _snapshot_settings(llm_settings,
_EMBEDDING_TYPE_FIELDS)
+ try:
+ llm_settings.embedding_type = req.llm_type
- if req.llm_type in ("openai", "litellm"):
- res = apply_embedding_conf(req.api_key, req.api_base,
req.language_model, origin_call="http")
- else:
- res = apply_embedding_conf(req.host, req.port, req.language_model,
origin_call="http")
- return generate_response(RAGResponse(status_code=res, message="Missing
Value"))
+ if req.llm_type in ("openai", "litellm"):
+ res = apply_embedding_conf(req.api_key, req.api_base,
req.language_model, origin_call="http")
+ else:
+ res = apply_embedding_conf(req.host, req.port,
req.language_model, origin_call="http")
+ return generate_response(RAGResponse(status_code=res,
message="Missing Value"))
+ except Exception:
+ _restore_settings(llm_settings, original_values)
+ raise
@router.post("/config/rerank", status_code=status.HTTP_201_CREATED)
def rerank_config_api(req: RerankerConfigRequest):
- llm_settings.reranker_type = req.reranker_type
-
- if req.reranker_type == "cohere":
- res = apply_reranker_conf(req.api_key, req.reranker_model,
req.cohere_base_url, origin_call="http")
- elif req.reranker_type == "siliconflow":
- res = apply_reranker_conf(req.api_key, req.reranker_model, None,
origin_call="http")
- else:
- res = status.HTTP_501_NOT_IMPLEMENTED
- return generate_response(RAGResponse(status_code=res, message="Missing
Value"))
+ original_values = _snapshot_settings(llm_settings,
_RERANKER_TYPE_FIELDS)
+ try:
+ llm_settings.reranker_type = req.reranker_type
+
+ if req.reranker_type == "cohere":
+ res = apply_reranker_conf(req.api_key, req.reranker_model,
req.cohere_base_url, origin_call="http")
+ elif req.reranker_type == "siliconflow":
+ res = apply_reranker_conf(req.api_key, req.reranker_model,
None, origin_call="http")
+ else:
+ res = status.HTTP_501_NOT_IMPLEMENTED
+ return generate_response(RAGResponse(status_code=res,
message="Missing Value"))
+ except Exception:
+ _restore_settings(llm_settings, original_values)
+ raise
@router.post("/text2gremlin", status_code=status.HTTP_200_OK)
def text2gremlin_api(req: GremlinGenerateRequest):
diff --git
a/hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract.py
b/hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract.py
index dbe6f907..7d9f5c77 100644
--- a/hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract.py
+++ b/hugegraph-llm/src/hugegraph_llm/operators/llm_op/property_graph_extract.py
@@ -67,12 +67,11 @@ def filter_item(schema, items) -> List[Dict[str, Any]]:
item_type = item["type"]
if item_type == "vertex":
label = item["label"]
- non_nullable_keys =
set(properties_map[item_type][label]["properties"]).difference(
- set(properties_map[item_type][label]["nullable_keys"])
- )
- for key in non_nullable_keys:
- if key not in item["properties"]:
- item["properties"][key] = "NULL"
+ item["properties"] = {
+ key: value
+ for key, value in item["properties"].items()
+ if key in properties_map[item_type][label]["properties"]
+ }
filtered_items.append(item)
return filtered_items
diff --git a/hugegraph-llm/src/tests/api/test_rag_api.py
b/hugegraph-llm/src/tests/api/test_rag_api.py
index 7b05d70f..16de9c1c 100644
--- a/hugegraph-llm/src/tests/api/test_rag_api.py
+++ b/hugegraph-llm/src/tests/api/test_rag_api.py
@@ -144,6 +144,49 @@ def
test_embedding_config_api_passes_openai_fields_to_apply_embedding_conf():
)
+def test_llm_config_api_rolls_back_provider_type_on_apply_failure(monkeypatch):
+ monkeypatch.setattr(llm_settings, "chat_llm_type", "ollama/local")
+ monkeypatch.setattr(llm_settings, "extract_llm_type", "ollama/local")
+ monkeypatch.setattr(llm_settings, "text2gql_llm_type", "ollama/local")
+ client, callbacks =
_make_test_client(apply_llm_conf=Mock(return_value=status.HTTP_500_INTERNAL_SERVER_ERROR))
+
+ response = client.post(
+ "/config/llm",
+ json={
+ "llm_type": "openai",
+ "api_key": "sk-test",
+ "api_base": "https://api.example.test",
+ "language_model": "gpt-test",
+ "max_tokens": "1024",
+ },
+ )
+
+ assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
+ assert llm_settings.chat_llm_type == "ollama/local"
+ assert llm_settings.extract_llm_type == "ollama/local"
+ assert llm_settings.text2gql_llm_type == "ollama/local"
+ callbacks["apply_llm_conf"].assert_called_once()
+
+
+def
test_embedding_config_api_rolls_back_provider_type_on_apply_failure(monkeypatch):
+ monkeypatch.setattr(llm_settings, "embedding_type", "ollama/local")
+ client, callbacks =
_make_test_client(apply_embedding_conf=Mock(return_value=status.HTTP_500_INTERNAL_SERVER_ERROR))
+
+ response = client.post(
+ "/config/embedding",
+ json={
+ "llm_type": "openai",
+ "api_key": "sk-embedding",
+ "api_base": "https://embedding.example.test",
+ "language_model": "embedding-test",
+ },
+ )
+
+ assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
+ assert llm_settings.embedding_type == "ollama/local"
+ callbacks["apply_embedding_conf"].assert_called_once()
+
+
def test_rerank_config_api_passes_cohere_fields_to_apply_reranker_conf():
client, callbacks = _make_test_client()
@@ -257,6 +300,25 @@ def
test_rerank_config_rejects_unsupported_provider_without_mutating(monkeypatch
assert llm_settings.reranker_type == "cohere"
+def test_rerank_config_rolls_back_provider_type_on_apply_failure(monkeypatch):
+ monkeypatch.setattr(llm_settings, "reranker_type", "siliconflow")
+ client, callbacks =
_make_test_client(apply_reranker_conf=Mock(return_value=status.HTTP_500_INTERNAL_SERVER_ERROR))
+
+ response = client.post(
+ "/config/rerank",
+ json={
+ "reranker_type": "cohere",
+ "api_key": "cohere-key",
+ "reranker_model": "rerank-test",
+ "cohere_base_url": "https://cohere.example.test",
+ },
+ )
+
+ assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
+ assert llm_settings.reranker_type == "siliconflow"
+ callbacks["apply_reranker_conf"].assert_called_once()
+
+
def test_text2gremlin_callback_exception_returns_stable_response():
client, _ =
_make_test_client(gremlin_generate_selective_func=Mock(side_effect=RuntimeError("callback
failed")))
diff --git
a/hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py
b/hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py
index b6e24e44..93fdd3d4 100644
--- a/hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py
+++ b/hugegraph-llm/src/tests/operators/llm_op/test_property_graph_extract.py
@@ -174,22 +174,21 @@ class TestPropertyGraphExtract(unittest.TestCase):
"label": "movie",
"properties": {
# Missing 'title' which is non-nullable
- "year": 1994 # Non-string value
+ "year": 1994, # Non-string value
+ "ignored": "not in schema",
},
},
]
filtered_items = filter_item(self.schema, items)
- # Check that non-nullable keys are added with NULL value
- # Note: 'age' is nullable, so it won't be added automatically
+ # Missing properties stay absent so Commit2Graph can apply
schema-typed defaults.
self.assertNotIn("age", filtered_items[0]["properties"])
-
- # Check that title (non-nullable) was added with NULL value
- self.assertEqual(filtered_items[1]["properties"]["title"], "NULL")
+ self.assertNotIn("title", filtered_items[1]["properties"])
# Check that schema-typed values are preserved for Commit2Graph
self.assertEqual(filtered_items[1]["properties"]["year"], 1994)
+ self.assertNotIn("ignored", filtered_items[1]["properties"])
def test_extract_property_graph_by_llm(self):
"""Test the extract_property_graph_by_llm method."""