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
commit 11e5b407b428e86b63edc57c44564771cd33ddff Author: imbajin <[email protected]> AuthorDate: Sun May 31 11:54:47 2026 +0800 fix(llm): validate request-scoped config - update graph settings only for explicit client_config fields - reject unsupported provider types before global mutation - add API contract tests for CS-011 and CS-012 - update code-scan ledgers --- .workflow/code-scan/code-scan-state.json | 4 +- .../code-scan/reports/final-code-scan-report.md | 4 +- .workflow/code-scan/reports/issues.md | 12 ++-- .workflow/code-scan/reports/test-quality-ledger.md | 2 + .../src/hugegraph_llm/api/models/rag_requests.py | 7 ++- hugegraph-llm/src/hugegraph_llm/api/rag_api.py | 22 ++++--- hugegraph-llm/src/tests/api/test_rag_api.py | 68 +++++++++++++++++++++- 7 files changed, 100 insertions(+), 19 deletions(-) diff --git a/.workflow/code-scan/code-scan-state.json b/.workflow/code-scan/code-scan-state.json index 16845f25..704cd470 100644 --- a/.workflow/code-scan/code-scan-state.json +++ b/.workflow/code-scan/code-scan-state.json @@ -59,6 +59,8 @@ "CS-002", "CS-004", "CS-008", + "CS-011", + "CS-012", "CS-013" ], "files_touched": [ @@ -125,5 +127,5 @@ "uv run ruff check <edited-python-files>", "git status --short" ], - "next_recommended_action": "Fix global config mutation and provider validation: CS-011 and CS-012." + "next_recommended_action": "Replace fake integration tests with production-flow smoke coverage." } diff --git a/.workflow/code-scan/reports/final-code-scan-report.md b/.workflow/code-scan/reports/final-code-scan-report.md index d6dc2eed..2ca80e8e 100644 --- a/.workflow/code-scan/reports/final-code-scan-report.md +++ b/.workflow/code-scan/reports/final-code-scan-report.md @@ -79,7 +79,7 @@ None. 1. Fix `CS-001` immediately: lock down `/logs` path handling and default admin credentials. Status: fixed after the scan. 2. Fix the client error-contract cluster: `CS-002`, `CS-004`, `CS-008`. Status: fixed after the scan. 3. Fix graph import correctness: `CS-013`, then add the missing round-trip tests. Status: fixed after the scan. -4. Fix global config mutation and provider validation: `CS-011`, `CS-012`. +4. Fix global config mutation and provider validation: `CS-011`, `CS-012`. Status: fixed after the scan. 5. Replace fake integration tests with production-flow smoke coverage before broader refactors. ## Fix Progress After Scan @@ -89,3 +89,5 @@ None. - `CS-004`: fixed by raising `ResponseParseError` for malformed successful responses. - `CS-008`: fixed by preserving lower-level exceptions from Gremlin execution instead of wrapping every failure as `NotFoundError`. - `CS-013`: fixed by preserving extracted property value types through the full extraction-to-commit path. +- `CS-011`: fixed by updating global graph settings only for explicitly supplied `client_config` fields. +- `CS-012`: fixed by rejecting unsupported provider types at request validation before global settings are mutated. diff --git a/.workflow/code-scan/reports/issues.md b/.workflow/code-scan/reports/issues.md index 67e56604..63dbc69f 100644 --- a/.workflow/code-scan/reports/issues.md +++ b/.workflow/code-scan/reports/issues.md @@ -81,7 +81,7 @@ Issue IDs use `CS-NNN`. Priorities are P0 highest through P5 lowest. - Module: `hugegraph-python-client` - Layer: client-api - Paths: `hugegraph-python-client/src/pyhugegraph/api/graph.py:68`, `hugegraph-python-client/src/pyhugegraph/api/graph.py:73`, `hugegraph-python-client/src/pyhugegraph/api/graph.py:173`, `hugegraph-python-client/src/pyhugegraph/utils/huge_requests.py:144` -- Status: open +- Status: fixed - Evidence: `getVertexByPage()`, `getVertexByCondition()`, and `getEdgeByPage()` concatenate labels and JSON properties directly into the URL. - Impact: Property values containing `&`, `?`, `#`, `=`, or spaces can corrupt filters and query different data. - Recommendation: Use `params=` or `urllib.parse.urlencode()` for all query parameters. @@ -144,8 +144,8 @@ Issue IDs use `CS-NNN`. Priorities are P0 highest through P5 lowest. - Status: open - Evidence: `GraphConfigRequest` has concrete defaults and `set_graph_config()` writes every field to global `huge_settings` whenever `client_config` exists. - Impact: A request intended to override one field can clear credentials, graph name, or graphspace for the whole process. -- Recommendation: Make nested fields optional and update only explicitly provided values, or keep request config out of global settings. -- Test note: Added a `FIXME:` in `test_rag_api.py`. +- Fix: request-scoped graph config updates now use `model_fields_set`, so only explicitly provided nested fields mutate `huge_settings`. +- Test note: Added `/rag` contract coverage proving partial `client_config` updates only the supplied graph fields. ### CS-012: Unsupported provider types are written into global settings before validation @@ -153,11 +153,11 @@ Issue IDs use `CS-NNN`. Priorities are P0 highest through P5 lowest. - Module: `hugegraph-llm` - Layer: API / models / config - Paths: `hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py:106`, `hugegraph-llm/src/hugegraph_llm/api/rag_api.py:163`, `hugegraph-llm/src/hugegraph_llm/api/rag_api.py:188`, `hugegraph-llm/src/hugegraph_llm/config/llm_config.py:29` -- Status: open +- Status: fixed - Evidence: Request models use bare `str` for `llm_type` and `reranker_type`; handlers write values to global settings before branch validation. - Impact: One bad request can poison process-global provider state for subsequent requests. -- Recommendation: Validate provider types with `Literal`/Enum before mutating global config. -- Test note: Added a `FIXME:` in `test_rag_api.py`. +- Fix: LLM and reranker request models now validate provider types with `Literal`, causing unsupported providers to fail with `422` before handler-side global mutation. +- Test note: Added config API coverage proving unsupported provider requests do not call apply callbacks or mutate global provider settings. ### CS-013: Property graph extraction stringifies values that importer rejects diff --git a/.workflow/code-scan/reports/test-quality-ledger.md b/.workflow/code-scan/reports/test-quality-ledger.md index a909f039..4f41704a 100644 --- a/.workflow/code-scan/reports/test-quality-ledger.md +++ b/.workflow/code-scan/reports/test-quality-ledger.md @@ -58,3 +58,5 @@ Added specific `FIXME:` comments in these files: - `CS-004`: response-validation tests now prove malformed successful JSON raises `ResponseParseError`. - `CS-008`: Gremlin tests now prove lower-level auth exceptions keep their original type through `GremlinManager.exec()`. - `CS-013`: extraction-to-commit round-trip tests now prove schema-typed numeric values survive `PropertyGraphExtract.run()` and are accepted by `Commit2Graph`. +- `CS-011`: `/rag` contract tests now prove partial `client_config` only updates explicitly supplied graph settings. +- `CS-012`: config API tests now prove unsupported LLM/reranker providers fail validation before mutating global settings. diff --git a/hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py b/hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py index 433438ee..77155978 100644 --- a/hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py +++ b/hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py @@ -23,6 +23,9 @@ from pydantic import BaseModel, field_validator from hugegraph_llm.config import prompt +LLMProviderType = Literal["openai", "ollama/local", "litellm"] +RerankerProviderType = Literal["cohere", "siliconflow"] + class GraphConfigRequest(BaseModel): url: str = Query("127.0.0.1:8080", description="hugegraph client url.") @@ -104,7 +107,7 @@ class GraphRAGRequest(BaseModel): class LLMConfigRequest(BaseModel): - llm_type: str + llm_type: LLMProviderType # The common parameters shared by OpenAI, LiteLLM, # and OLLAMA platforms. api_key: str @@ -120,7 +123,7 @@ class LLMConfigRequest(BaseModel): class RerankerConfigRequest(BaseModel): reranker_model: str - reranker_type: str + reranker_type: RerankerProviderType api_key: str cohere_base_url: Optional[str] = None diff --git a/hugegraph-llm/src/hugegraph_llm/api/rag_api.py b/hugegraph-llm/src/hugegraph_llm/api/rag_api.py index bce7de0d..9b9c7d35 100644 --- a/hugegraph-llm/src/hugegraph_llm/api/rag_api.py +++ b/hugegraph-llm/src/hugegraph_llm/api/rag_api.py @@ -90,12 +90,18 @@ def rag_http_api( } def set_graph_config(req): - if req.client_config: - huge_settings.graph_url = req.client_config.url - huge_settings.graph_name = req.client_config.graph - huge_settings.graph_user = req.client_config.user - huge_settings.graph_pwd = req.client_config.pwd - huge_settings.graph_space = req.client_config.gs + if req.client_config is None: + return + field_map = { + "url": "graph_url", + "graph": "graph_name", + "user": "graph_user", + "pwd": "graph_pwd", + "gs": "graph_space", + } + for request_field, settings_field in field_map.items(): + if request_field in req.client_config.model_fields_set: + setattr(huge_settings, settings_field, getattr(req.client_config, request_field)) @router.post("/rag/graph", status_code=status.HTTP_200_OK) def graph_rag_recall_api(req: GraphRAGRequest): @@ -163,7 +169,7 @@ def rag_http_api( def llm_config_api(req: LLMConfigRequest): llm_settings.chat_llm_type = req.llm_type - if req.llm_type == "openai": + if req.llm_type in ("openai", "litellm"): res = apply_llm_conf( req.api_key, req.api_base, @@ -179,7 +185,7 @@ def rag_http_api( def embedding_config_api(req: LLMConfigRequest): llm_settings.embedding_type = req.llm_type - if req.llm_type == "openai": + 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") diff --git a/hugegraph-llm/src/tests/api/test_rag_api.py b/hugegraph-llm/src/tests/api/test_rag_api.py index c06a52ff..24e1a219 100644 --- a/hugegraph-llm/src/tests/api/test_rag_api.py +++ b/hugegraph-llm/src/tests/api/test_rag_api.py @@ -22,13 +22,14 @@ from fastapi import APIRouter, FastAPI, status from fastapi.testclient import TestClient from hugegraph_llm.api.rag_api import rag_http_api +from hugegraph_llm.config import huge_settings, llm_settings pytestmark = pytest.mark.contract # FIXME: add negative-path tests for each /config/* endpoint that assert # provider/auth/connection failures surface distinct error details. # FIXME: add happy-path HTTP contract tests for /rag and /rag/graph response -# shaping, output-flag filtering, and request-scoped graph config behavior. +# shaping and output-flag filtering. # FIXME: distinguish graph/provider dependency failures from programmer errors # instead of asserting one generic unexpected-error response for all failures. @@ -168,6 +169,71 @@ def test_rag_api_invalid_request_body_returns_validation_shape(): assert response.json()["detail"][0]["loc"][-1] == "query" +def test_rag_client_config_updates_only_explicit_graph_fields(monkeypatch): + monkeypatch.setattr(huge_settings, "graph_url", "http://original:8080") + monkeypatch.setattr(huge_settings, "graph_name", "original_graph") + monkeypatch.setattr(huge_settings, "graph_user", "original_user") + monkeypatch.setattr(huge_settings, "graph_pwd", "original_pwd") + monkeypatch.setattr(huge_settings, "graph_space", "original_space") + client, callbacks = _make_test_client() + + response = client.post( + "/rag", + json={ + "query": "find vertices", + "client_config": { + "url": "http://override:8080", + }, + }, + ) + + assert response.status_code == status.HTTP_200_OK + callbacks["rag_answer_func"].assert_called_once() + assert huge_settings.graph_url == "http://override:8080" + assert huge_settings.graph_name == "original_graph" + assert huge_settings.graph_user == "original_user" + assert huge_settings.graph_pwd == "original_pwd" + assert huge_settings.graph_space == "original_space" + + +def test_llm_config_rejects_unsupported_provider_without_mutating(monkeypatch): + monkeypatch.setattr(llm_settings, "chat_llm_type", "openai") + client, callbacks = _make_test_client() + + response = client.post( + "/config/llm", + json={ + "llm_type": "unsupported", + "api_key": "sk-test", + "api_base": "https://api.example.test", + "language_model": "gpt-test", + "max_tokens": "1024", + }, + ) + + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + callbacks["apply_llm_conf"].assert_not_called() + assert llm_settings.chat_llm_type == "openai" + + +def test_rerank_config_rejects_unsupported_provider_without_mutating(monkeypatch): + monkeypatch.setattr(llm_settings, "reranker_type", "cohere") + client, callbacks = _make_test_client() + + response = client.post( + "/config/rerank", + json={ + "reranker_type": "unsupported", + "api_key": "rerank-key", + "reranker_model": "rerank-test", + }, + ) + + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + callbacks["apply_reranker_conf"].assert_not_called() + assert llm_settings.reranker_type == "cohere" + + def test_text2gremlin_callback_exception_returns_stable_response(): client, _ = _make_test_client(gremlin_generate_selective_func=Mock(side_effect=RuntimeError("callback failed")))
