imbajin commented on code in PR #358:
URL: https://github.com/apache/hugegraph-ai/pull/358#discussion_r3346728623


##########
hugegraph-python-client/src/tests/api/test_gremlin.py:
##########
@@ -139,8 +143,35 @@ def test_set_up_class_skips_when_env_var_set(self):
 
 
 def test_gremlin_error_surface_is_explicit(client_utils):
-    with pytest.raises(NotFoundError) as exc_info:
+    with pytest.raises(ServerError) as exc_info:

Review Comment:
   SKIP_GREMLIN_TESTS=true does not skip this module-level Gremlin test. The 
TestGremlin class is skipped, but this function still requests the client_utils 
fixture, connects to the local HugeGraph service, and fails during graph 
cleanup when the service is unavailable or has different credentials. Please 
apply the same explicit skip gate to this test or move the skip into the shared 
hugegraph/client fixture so the opt-out is reliable.



##########
hugegraph-llm/src/hugegraph_llm/api/admin_api.py:
##########
@@ -15,29 +15,56 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import ntpath
 import os
 
-from fastapi import APIRouter, status
+from fastapi import APIRouter, HTTPException, status
 from fastapi.responses import StreamingResponse
 
 from hugegraph_llm.api.exceptions.rag_exceptions import generate_response
 from hugegraph_llm.api.models.rag_requests import LogStreamRequest
 from hugegraph_llm.api.models.rag_response import RAGResponse
 from hugegraph_llm.config import admin_settings
 
+LOG_DIR = "logs"
+INSECURE_ADMIN_TOKENS = {"", "xxxx"}
+
+
+def _is_configured_admin_token(admin_token: str | None) -> bool:
+    return admin_token is not None and admin_token not in INSECURE_ADMIN_TOKENS
+
+
+def _resolve_log_path(log_file: str | None) -> str:
+    if not log_file:
+        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, 
detail="Invalid log file name.")
+    if (
+        os.path.isabs(log_file)
+        or ntpath.isabs(log_file)
+        or ntpath.splitdrive(log_file)[0]
+        or "/" in log_file
+        or "\\" in log_file
+        or os.path.normpath(log_file) in {"", ".", ".."}
+    ):
+        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, 
detail="Invalid log file name.")
+    return os.path.join(LOG_DIR, log_file)
+
 
-# FIXME: line 31: E0702: Raising dict while only classes or instances are 
allowed (raising-bad-type)
 def admin_http_api(router: APIRouter, log_stream):
     @router.post("/logs", status_code=status.HTTP_200_OK)
     async def log_stream_api(req: LogStreamRequest):
+        if not _is_configured_admin_token(admin_settings.admin_token):

Review Comment:
   This hardening only covers the FastAPI /logs route. The Gradio Admin Tools 
path still authenticates via admin_block.check_password() with a direct 
password == admin_token comparison and then reads logs through 
read_llm_server_log(), so deployments that keep the default/empty admin token 
can still expose logs through the UI even though POST /logs is now rejected. 
Please share the same configured-token check with the UI path, or disable Admin 
Tools log access when the admin token is insecure.



##########
hugegraph-llm/src/hugegraph_llm/api/rag_api.py:
##########
@@ -161,68 +189,82 @@ def graph_config_api(req: GraphConfigRequest):
     # 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 == "openai":
-            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)

Review Comment:
   The rollback here only restores the provider-type fields. The production 
callbacks mutate concrete provider settings before returning a non-2xx status, 
so a failed /config/llm, /config/embedding, or /config/rerank request can leave 
API keys, base URLs, model names, or token limits half-applied even though the 
endpoint returns an error. Please snapshot and restore the concrete provider 
config fields as well, not just *_type, and extend the rollback tests to cover 
those fields.



##########
hugegraph-llm/src/tests/integration/test_graph_rag_pipeline.py:
##########
@@ -15,275 +15,27 @@
 # specific language governing permissions and limitations
 # under the License.
 
-
-import shutil
-import tempfile
-import unittest
-from unittest.mock import MagicMock
-
 import pytest
 
-from tests.utils.mock import MockEmbedding
-
-pytestmark = [pytest.mark.external, pytest.mark.slow]
-
-
-class BaseLLM:
-    def generate(self, prompt, **kwargs):
-        pass
-
-    async def async_generate(self, prompt, **kwargs):
-        pass
-
-    def get_llm_type(self):
-        pass
-
-
-# 模拟RAGPipeline类
-class RAGPipeline:
-    def __init__(self, llm=None, embedding=None):
-        self.llm = llm
-        self.embedding = embedding
-        self.operators = {}
-
-    def extract_word(self, text=None, language="english"):
-        if "word_extract" in self.operators:
-            return self.operators["word_extract"]({"query": text})
-        return {"words": []}
-
-    def extract_keywords(self, text=None, max_keywords=5, language="english", 
extract_template=None):
-        if "keyword_extract" in self.operators:
-            return self.operators["keyword_extract"]({"query": text})
-        return {"keywords": []}
-
-    def keywords_to_vid(self, by="keywords", topk_per_keyword=5, 
topk_per_query=10):
-        if "semantic_id_query" in self.operators:
-            return self.operators["semantic_id_query"]({"keywords": []})
-        return {"match_vids": []}
-
-    def query_graphdb(
-        self,
-        max_deep=2,
-        max_graph_items=10,
-        max_v_prop_len=2048,
-        max_e_prop_len=256,
-        prop_to_match=None,
-        num_gremlin_generate_example=1,
-        gremlin_prompt=None,
-    ):
-        if "graph_rag_query" in self.operators:
-            return self.operators["graph_rag_query"]({"match_vids": []})
-        return {"graph_result": []}
-
-    def query_vector_index(self, max_items=3):
-        if "vector_index_query" in self.operators:
-            return self.operators["vector_index_query"]({"query": ""})
-        return {"vector_result": []}
-
-    def merge_dedup_rerank(
-        self, graph_ratio=0.5, rerank_method="bleu", 
near_neighbor_first=False, custom_related_information=""
-    ):
-        if "merge_dedup_rerank" in self.operators:
-            return self.operators["merge_dedup_rerank"]({"graph_result": [], 
"vector_result": []})
-        return {"merged_result": []}
-
-    def synthesize_answer(
-        self,
-        raw_answer=False,
-        vector_only_answer=True,
-        graph_only_answer=False,
-        graph_vector_answer=False,
-        answer_prompt=None,
-    ):
-        if "answer_synthesize" in self.operators:
-            return self.operators["answer_synthesize"]({"merged_result": []})
-        return {"answer": ""}
-
-    def run(self, **kwargs):
-        context = {"query": kwargs.get("query", "")}
-
-        # 执行各个步骤
-        if not kwargs.get("skip_extract_word", False):
-            context.update(self.extract_word(text=context["query"]))
-
-        if not kwargs.get("skip_extract_keywords", False):
-            context.update(self.extract_keywords(text=context["query"]))
-
-        if not kwargs.get("skip_keywords_to_vid", False):
-            context.update(self.keywords_to_vid())
-
-        if not kwargs.get("skip_query_graphdb", False):
-            context.update(self.query_graphdb())
-
-        if not kwargs.get("skip_query_vector_index", False):
-            context.update(self.query_vector_index())
-
-        if not kwargs.get("skip_merge_dedup_rerank", False):
-            context.update(self.merge_dedup_rerank())
-
-        if not kwargs.get("skip_synthesize_answer", False):
-            context.update(
-                self.synthesize_answer(
-                    vector_only_answer=kwargs.get("vector_only_answer", False),
-                    graph_only_answer=kwargs.get("graph_only_answer", False),
-                    graph_vector_answer=kwargs.get("graph_vector_answer", 
False),
-                )
-            )
-
-        return context
-
-
-class MockLLM(BaseLLM):
-    """Mock LLM class for testing"""
-
-    def __init__(self):
-        self.model = "mock_llm"
-
-    def generate(self, prompt, **kwargs):
-        # Return a simple mock response based on the prompt
-        if "person" in prompt.lower():
-            return "This is information about a person."
-        if "movie" in prompt.lower():
-            return "This is information about a movie."
-        return "I don't have specific information about that."
-
-    async def async_generate(self, prompt, **kwargs):
-        # Async version returns the same as the sync version
-        return self.generate(prompt, **kwargs)
-
-    def get_llm_type(self):
-        return "mock"
-
-
-class TestGraphRAGPipeline(unittest.TestCase):
-    def setUp(self):
-        # Create a temporary directory for testing
-        self.test_dir = tempfile.mkdtemp()
-
-        # Create mock models
-        self.embedding = MockEmbedding()
-        self.llm = MockLLM()
-
-        # Create mock operators
-        self.mock_word_extract = MagicMock()
-        self.mock_word_extract.return_value = {"words": ["person", "movie"]}
-
-        self.mock_keyword_extract = MagicMock()
-        self.mock_keyword_extract.return_value = {"keywords": ["person", 
"movie"]}
-
-        self.mock_semantic_id_query = MagicMock()
-        self.mock_semantic_id_query.return_value = {"match_vids": ["1:person", 
"2:movie"]}
-
-        self.mock_graph_rag_query = MagicMock()
-        self.mock_graph_rag_query.return_value = {
-            "graph_result": ["Person: John Doe, Age: 30", "Movie: The Matrix, 
Year: 1999"]
-        }
-
-        self.mock_vector_index_query = MagicMock()
-        self.mock_vector_index_query.return_value = {
-            "vector_result": ["John Doe is a software engineer.", "The Matrix 
is a science fiction movie."]
-        }
-
-        self.mock_merge_dedup_rerank = MagicMock()
-        self.mock_merge_dedup_rerank.return_value = {
-            "merged_result": [
-                "Person: John Doe, Age: 30",
-                "Movie: The Matrix, Year: 1999",
-                "John Doe is a software engineer.",
-                "The Matrix is a science fiction movie.",
-            ]
-        }
-
-        self.mock_answer_synthesize = MagicMock()
-        self.mock_answer_synthesize.return_value = {
-            "answer": (
-                "John Doe is a 30-year-old software engineer. The Matrix is a 
science fiction movie released in 1999."
-            )
-        }
-
-        # 创建RAGPipeline实例
-        self.pipeline = RAGPipeline(llm=self.llm, embedding=self.embedding)
-        self.pipeline.operators = {
-            "word_extract": self.mock_word_extract,
-            "keyword_extract": self.mock_keyword_extract,
-            "semantic_id_query": self.mock_semantic_id_query,
-            "graph_rag_query": self.mock_graph_rag_query,
-            "vector_index_query": self.mock_vector_index_query,
-            "merge_dedup_rerank": self.mock_merge_dedup_rerank,
-            "answer_synthesize": self.mock_answer_synthesize,
-        }
-
-    def tearDown(self):
-        # Clean up the temporary directory
-        shutil.rmtree(self.test_dir)
-
-    def test_rag_pipeline_end_to_end(self):
-        # Run the pipeline with a query
-        query = "Tell me about John Doe and The Matrix movie"
-        result = self.pipeline.run(query=query)
-
-        # Verify the result
-        self.assertIn("answer", result)
-        self.assertEqual(
-            result["answer"],
-            "John Doe is a 30-year-old software engineer. The Matrix is a 
science fiction movie released in 1999.",
-        )
-
-        # Verify that all operators were called
-        self.mock_word_extract.assert_called_once()
-        self.mock_keyword_extract.assert_called_once()
-        self.mock_semantic_id_query.assert_called_once()
-        self.mock_graph_rag_query.assert_called_once()
-        self.mock_vector_index_query.assert_called_once()
-        self.mock_merge_dedup_rerank.assert_called_once()
-        self.mock_answer_synthesize.assert_called_once()
-
-    def test_rag_pipeline_vector_only(self):
-        # Run the pipeline with a query, skipping graph-related steps
-        query = "Tell me about John Doe and The Matrix movie"
-        result = self.pipeline.run(
-            query=query,
-            skip_keywords_to_vid=True,
-            skip_query_graphdb=True,
-            skip_merge_dedup_rerank=True,
-            vector_only_answer=True,
-        )
-
-        # Verify the result
-        self.assertIn("answer", result)
-        self.assertEqual(
-            result["answer"],
-            "John Doe is a 30-year-old software engineer. The Matrix is a 
science fiction movie released in 1999.",
-        )
+pytestmark = [pytest.mark.smoke, pytest.mark.integration]
 
-        # Verify that only vector-related operators were called
-        self.mock_word_extract.assert_called_once()
-        self.mock_keyword_extract.assert_called_once()
-        self.mock_semantic_id_query.assert_not_called()
-        self.mock_graph_rag_query.assert_not_called()
-        self.mock_vector_index_query.assert_called_once()
-        self.mock_merge_dedup_rerank.assert_not_called()
-        self.mock_answer_synthesize.assert_called_once()
 
-    def test_rag_pipeline_graph_only(self):
-        # Run the pipeline with a query, skipping vector-related steps
-        query = "Tell me about John Doe and The Matrix movie"
-        result = self.pipeline.run(
-            query=query, skip_query_vector_index=True, 
skip_merge_dedup_rerank=True, graph_only_answer=True
-        )
+def test_vector_only_rag_flow_builds_production_pipeline():

Review Comment:
   These integration tests now use more production classes, but most of the 
coverage stops at build-time wiring or isolated operators. They do not execute 
the flow-level contract or assert post_deal/final response behavior, so 
regressions in node ordering, state propagation, or output shaping can still 
pass. Could we keep at least one deterministic end-to-end smoke per production 
flow instead of only build-smoke coverage?



##########
hugegraph-python-client/src/pyhugegraph/api/gremlin.py:
##########
@@ -45,10 +46,10 @@ def exec(self, gremlin):
                 "g": f"__g_{self._sess.cfg.graph_name}",
             }
 
-        try:
-            if response := self._invoke_request(data=gremlin_data.to_json()):
-                return ResponseData(response).result
-            log.error("Gremlin can't get results: %s", str(response))
-            return None
-        except Exception as e:
-            raise NotFoundError(f"Gremlin can't get results: {e}") from e
+        response = self._invoke_request(data=gremlin_data.to_json())
+        if response is not None:
+            if not isinstance(response, dict) or not 
_REQUIRED_RESPONSE_FIELDS.issubset(response):
+                raise ValueError(f"Invalid Gremlin response payload: 
{response}")

Review Comment:
   This malformed-success path still raises a raw ValueError, while 
ResponseValidation now standardizes malformed successful HTTP bodies as typed 
client exceptions. That leaves Gremlin callers with a special-case parse/error 
surface. Could we normalize this to the same typed exception contract and 
update the regression test to assert that public contract?



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