Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions hugegraph-llm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ graph systems and large language models.

7. After running the web demo, the config file `.env` will be automatically generated at the path `hugegraph-llm/.env`. Additionally, a prompt-related configuration file `config_prompt.yaml` will also be generated at the path `hugegraph-llm/src/hugegraph_llm/resources/demo/config_prompt.yaml`.
You can modify the content on the web page, and it will be automatically saved to the configuration file after the corresponding feature is triggered. You can also modify the file directly without restarting the web application; refresh the page to load your latest changes.
The `.env` file is used for setting various environment variables. For a detailed list of configurable environment variables and their descriptions, please see the "Environment Variables for Configuration" section below.
(Optional)To regenerate the config file, you can use `config.generate` with `-u` or `--update`.
```bash
python -m hugegraph_llm.config.generate --update
Expand All @@ -74,6 +75,19 @@ graph systems and large language models.
> [!TIP]
> You can also refer to our [quick-start](https://github.com/apache/incubator-hugegraph-ai/blob/main/hugegraph-llm/quick_start.md) doc to understand how to use it & the basic query logic 🚧

## Environment Variables for Configuration

The application can be configured using environment variables, typically set in the `.env` file located in the `hugegraph-llm` directory. This file is automatically generated after running the web demo for the first time.

Below is a list of variables that can be configured:

- **`RAG_QUERY_MAX_LENGTH`**:
- **Description**: Sets the maximum character length for user queries submitted to the RAG (Retrieval Augmented Generation) functionalities, including the interactive demo and APIs. Queries exceeding this length will be rejected by the system.
- **Default Value**: `50`
- **Example**: If set to `100`, queries longer than 100 characters will be rejected.

Many other configuration options related to LLM providers (OpenAI, LiteLLM, Ollama, Qianfan), embedding models, and rerankers are also managed via environment variables in the `.env` file. These are typically set through the web UI and saved to the `.env` file, or can be manually edited. Refer to the `LLMConfig` class in `hugegraph_llm.config.llm_config.py` for a comprehensive list of all possible environment variables.

## 4 Examples

### 4.1 Build a knowledge graph in HugeGraph through LLM
Expand Down
20 changes: 18 additions & 2 deletions hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
from typing import Optional, Literal

from fastapi import Query
from pydantic import BaseModel
from pydantic import BaseModel, validator

from hugegraph_llm.config import prompt
from hugegraph_llm.config import prompt, LLMConfig


class GraphConfigRequest(BaseModel):
Expand Down Expand Up @@ -62,6 +62,14 @@ class RAGRequest(BaseModel):
description="Prompt for the Text2Gremlin query.",
)

@validator('query')

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [Medium] Duplicated validation logic violates DRY principle

The same query length validation is duplicated here and in rag_requests.py lines 99-105. Consider extracting to a shared validation utility function to improve maintainability.

Suggested approach: Create a validate_query_length() utility that can be reused across all validation points.

def check_query_length(cls, value):
llm_config = LLMConfig()
max_len = int(llm_config.rag_query_max_length)
if len(value) > max_len:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 [Minor] Missing edge case validation

No validation for negative or zero values. Consider adding:

if not value or len(value.strip()) == 0:
    raise ValueError('Query cannot be empty')
if max_len <= 0:
    raise ValueError('Invalid max length configuration')

raise ValueError(f"Query exceeds maximum allowed length of {max_len} characters.")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [Medium] Duplicated validation logic between RAGRequest and GraphRAGRequest

Both classes have identical check_query_length validators. Consider extracting to a base class or mixin to reduce duplication and ensure consistency:

class QueryValidationMixin:
    @validator('query')
    def check_query_length(cls, value):
        llm_config = LLMConfig()
        max_len = int(llm_config.rag_query_max_length)
        if len(value) > max_len:
            raise ValueError(f"Query exceeds maximum allowed length of {max_len} characters.")
        return value

class RAGRequest(BaseModel, QueryValidationMixin):
    # ... existing fields

class GraphRAGRequest(BaseModel, QueryValidationMixin):
    # ... existing fields

This would make future maintenance easier and ensure both validators stay in sync.

return value


# TODO: import the default value of prompt.* dynamically
class GraphRAGRequest(BaseModel):
Expand All @@ -88,6 +96,14 @@ class GraphRAGRequest(BaseModel):
description="Prompt for the Text2Gremlin query.",
)

@validator('query')
def check_query_length(cls, value):
llm_config = LLMConfig()
max_len = int(llm_config.rag_query_max_length)
if len(value) > max_len:
raise ValueError(f"Query exceeds maximum allowed length of {max_len} characters.")
return value
Comment on lines +99 to +105

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

代码重复:与 RAGRequest 中的验证器相同

此验证器与 RAGRequest 中的验证器完全相同,存在代码重复问题。

建议将验证逻辑提取为共享函数:

+def validate_query_length(value: str) -> str:
+    """共享的查询长度验证函数"""
+    llm_config = LLMConfig()
+    max_len = llm_config.rag_query_max_length
+    if len(value) > max_len:
+        raise ValueError(f"Query exceeds maximum allowed length of {max_len} characters.")
+    return value
+
 @validator('query')
 def check_query_length(cls, value):
-    llm_config = LLMConfig()
-    max_len = int(llm_config.rag_query_max_length)
-    if len(value) > max_len:
-        raise ValueError(f"Query exceeds maximum allowed length of {max_len} characters.")
-    return value
+    return validate_query_length(value)

然后在 RAGRequest 中使用相同的共享函数。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@validator('query')
def check_query_length(cls, value):
llm_config = LLMConfig()
max_len = int(llm_config.rag_query_max_length)
if len(value) > max_len:
raise ValueError(f"Query exceeds maximum allowed length of {max_len} characters.")
return value
# Shared helper for query‐length validation
def validate_query_length(value: str) -> str:
"""共享的查询长度验证函数"""
llm_config = LLMConfig()
max_len = llm_config.rag_query_max_length
if len(value) > max_len:
raise ValueError(f"Query exceeds maximum allowed length of {max_len} characters.")
return value
# In your Pydantic model:
@validator('query')
def check_query_length(cls, value):
return validate_query_length(value)
🤖 Prompt for AI Agents
In hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py around lines 99 to
105, the query length validator duplicates the same logic found in RAGRequest,
causing code repetition. Extract the query length validation logic into a shared
helper function and then call this function from both the current validator and
the one in RAGRequest to eliminate duplication.



class LLMConfigRequest(BaseModel):
llm_type: str
Expand Down
1 change: 1 addition & 0 deletions hugegraph-llm/src/hugegraph_llm/config/llm_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class LLMConfig(BaseConfig):
text2gql_llm_type: Literal["openai", "litellm", "ollama/local", "qianfan_wenxin"] = "openai"
embedding_type: Optional[Literal["openai", "litellm", "ollama/local", "qianfan_wenxin"]] = "openai"
reranker_type: Optional[Literal["cohere", "siliconflow"]] = None
rag_query_max_length: int = os.environ.get("RAG_QUERY_MAX_LENGTH", 50)
Comment thread
imbajin marked this conversation as resolved.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 [Critical] Missing proper type conversion for environment variable

The environment variable is retrieved without proper conversion to int. When RAG_QUERY_MAX_LENGTH is set in the environment, it will be a string, but the default value is an integer (50), causing type inconsistency.

Suggested change
rag_query_max_length: int = os.environ.get("RAG_QUERY_MAX_LENGTH", 50)
rag_query_max_length: int = int(os.environ.get("RAG_QUERY_MAX_LENGTH", "50"))

This ensures consistent integer type regardless of whether the value comes from environment or default.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 [Critical] Type conversion error in llm_config.py line 33

os.environ.get() returns string but assigned to int field without conversion. Will cause TypeError at runtime.

Fix: rag_query_max_length: int = int(os.environ.get('RAG_QUERY_MAX_LENGTH', '50'))

# 1. OpenAI settings
openai_chat_api_base: Optional[str] = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
openai_chat_api_key: Optional[str] = os.environ.get("OPENAI_API_KEY")
Expand Down
23 changes: 22 additions & 1 deletion hugegraph-llm/src/hugegraph_llm/demo/rag_demo/rag_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import pandas as pd
from gradio.utils import NamedString

from hugegraph_llm.config import resource_path, prompt, huge_settings, llm_settings
from hugegraph_llm.config import resource_path, prompt, huge_settings, llm_settings, LLMConfig
from hugegraph_llm.operators.graph_rag_task import RAGPipeline
from hugegraph_llm.utils.decorators import with_task_id
from hugegraph_llm.operators.llm_op.answer_synthesize import AnswerSynthesize
Expand All @@ -49,6 +49,16 @@ def rag_answer(
vector_dis_threshold=0.9,
topk_per_keyword=1,
) -> Tuple:
llm_config = LLMConfig()
max_len = int(llm_config.rag_query_max_length)
query_text = text
if len(query_text) > max_len:
log.warning(
f"Input query exceeds maximum length of {max_len} characters. Query: '{query_text[:100]}...'"
)
gr.Warning(f"Query is too long! Maximum allowed length is {max_len} characters.")
return "", "", "", ""

"""
Generate an answer using the RAG (Retrieval-Augmented Generation) pipeline.
1. Initialize the RAGPipeline.
Expand Down Expand Up @@ -160,6 +170,17 @@ async def rag_answer_streaming(
gremlin_tmpl_num: Optional[int] = -1,
gremlin_prompt: Optional[str] = None,
) -> AsyncGenerator[Tuple[str, str, str, str], None]:
llm_config = LLMConfig()
max_len = int(llm_config.rag_query_max_length)
query_text = text
if len(query_text) > max_len:
log.warning(
f"Input query exceeds maximum length of {max_len} characters. Query: '{query_text[:100]}...'"
)
gr.Warning(f"Query is too long! Maximum allowed length is {max_len} characters.")
yield "", "", "", ""
return

"""
Generate an answer using the RAG (Retrieval-Augmented Generation) pipeline.
1. Initialize the RAGPipeline.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import gradio as gr
import pandas as pd

from hugegraph_llm.config import prompt, resource_path, huge_settings
from hugegraph_llm.config import prompt, resource_path, huge_settings, LLMConfig
from hugegraph_llm.models.embeddings.init_embedding import Embeddings
from hugegraph_llm.models.llms.init_llm import LLMs
from hugegraph_llm.operators.graph_rag_task import RAGPipeline
Expand Down Expand Up @@ -194,6 +194,14 @@ def graph_rag_recall(
topk_per_keyword: int,
get_vertex_only: bool = False,
) -> dict:
llm_config = LLMConfig()
max_len = int(llm_config.rag_query_max_length)
if len(query) > max_len:
log.warning(
f"Input query for graph_rag_recall exceeds maximum length of {max_len} characters. Query: '{query[:100]}...'"
)
return {"error": f"Query is too long! Maximum allowed length is {max_len} characters.", "graph_result": []}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [Medium] Inconsistent error handling breaks API contract

This function returns a dict with error key instead of raising an exception like other validation points. This inconsistency makes error handling unpredictable for API consumers.

Should either raise a proper exception or document this different behavior clearly.


store_schema(prompt.text2gql_graph_schema, query, gremlin_prompt)
rag = RAGPipeline()
rag.extract_keywords().keywords_to_vid(
Expand Down
167 changes: 167 additions & 0 deletions hugegraph-llm/src/tests/api/test_rag_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import unittest
Comment thread
imbajin marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
from unittest.mock import patch, MagicMock

from fastapi import APIRouter, HTTPException, status

from hugegraph_llm.config.llm_config import LLMConfig
from hugegraph_llm.api.rag_api import rag_http_api
from hugegraph_llm.api.models.rag_requests import RAGRequest, GraphRAGRequest


class TestRagApiQueryValidation(unittest.TestCase):

def setUp(self):
self.router = APIRouter()
self.mock_rag_answer_func = MagicMock()
self.mock_graph_rag_recall_func = MagicMock()
self.mock_apply_graph_conf = MagicMock()
self.mock_apply_llm_conf = MagicMock()
self.mock_apply_embedding_conf = MagicMock()
self.mock_apply_reranker_conf = MagicMock()

rag_http_api(
self.router,
self.mock_rag_answer_func,
self.mock_graph_rag_recall_func,
self.mock_apply_graph_conf,
self.mock_apply_llm_conf,
self.mock_apply_embedding_conf,
self.mock_apply_reranker_conf,
)

def get_endpoint_function(self, path: str):
for route in self.router.routes:
if route.path == path:
return route.endpoint
raise ValueError(f"Route {path} not found")

@patch('hugegraph_llm.api.models.rag_requests.LLMConfig')
def test_rag_answer_api_query_too_long(self, mock_llm_config_pydantic):
rag_answer_api_endpoint = self.get_endpoint_function("/rag")

mock_llm_config_instance = mock_llm_config_pydantic.return_value
mock_llm_config_instance.rag_query_max_length = 10

# This will be validated by Pydantic before the endpoint logic is hit
with self.assertRaises(HTTPException) as cm:
# Directly instantiating RAGRequest with invalid data won't raise HTTPException here,
# FastAPI does this when processing the request.
# To simulate FastAPI's behavior, we assume the endpoint is called with data
# that *would* cause Pydantic to fail during request body parsing.
# The actual RAGRequest instantiation happens inside FastAPI's request handling.
# For a unit test, we are directly calling the endpoint function.
# Pydantic validation for path/query/body parameters is typically handled by
# FastAPI's request parsing layer *before* the endpoint function is called.
# However, if the endpoint function itself receives the raw request model and
# Pydantic validation happens upon model instantiation *within* the endpoint,
# then the test structure is fine. Given the current structure of FastAPI,
# the validation for RAGRequest happens *before* rag_answer_api is called.
# This test simulates the state *after* FastAPI has parsed and validated,
# and if validation failed, it would have raised HTTPException(422).
# Since we are calling the function directly, we must ensure the Pydantic model
# itself raises an error that FastAPI would catch and convert to 422.
# Let's assume the endpoint *receives* an already validated model or the validation
# is part of the endpoint for this test to make sense as written.
# The instructions imply Pydantic validation in the model will lead to a 422.
# This means FastAPI's handling of Pydantic's ValueError.
# We will construct the request, and the endpoint call will internally trigger validation
# if the model is instantiated there, or FastAPI handles it if passed as type hint.
# For this test to be accurate to FastAPI behavior for request body validation:
# We should not expect to catch HTTPException directly from RAGRequest instantiation here.
# Instead, the endpoint call should be the one raising it due to FastAPI's processing.
# The current test structure where `rag_answer_api_endpoint(req)` is called is correct
# if we assume FastAPI passes a validated model or the model instantiation happens inside.
# Given Pydantic validator raises ValueError, FastAPI converts this to HTTP 422.

# Simulate calling the endpoint which would trigger Pydantic validation via FastAPI

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [Medium] Test doesn't properly validate FastAPI behavior

This test manually creates HTTPException to simulate FastAPI's validation, but doesn't actually test the real request flow. Should use FastAPI's TestClient for proper integration testing to ensure validation works correctly in production.

Example:

from fastapi.testclient import TestClient
client = TestClient(app)
response = client.post('/rag', json={'query': long_query})
assert response.status_code == 422

# For the purpose of this unit test, we'll assume the Pydantic model validation
# error (ValueError) is caught by FastAPI and results in an HTTPException(422).
# Since we call the endpoint function directly, we need to simulate this.
# The most direct way to test the Pydantic validator itself is to instantiate the model.
try:
RAGRequest(query="This is a very long query that exceeds the limit.")
except ValueError as e: # Pydantic validator raises ValueError
# FastAPI would catch this and convert it to HTTPException 422
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))

Comment on lines +46 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

简化测试逻辑并改进异常处理

当前的测试逻辑过于复杂,试图模拟 FastAPI 的内部行为。同时,异常处理可以改进。

简化测试逻辑:

 with self.assertRaises(HTTPException) as cm:
-    # 移除复杂的注释和说明
-    try:
-        RAGRequest(query="This is a very long query that exceeds the limit.")
-    except ValueError as e: # Pydantic validator raises ValueError
-         # FastAPI would catch this and convert it to HTTPException 422
-        raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))
+    try:
+        RAGRequest(query="This is a very long query that exceeds the limit.")
+    except ValueError as e:
+        raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e)) from e

类似地修复第二个测试中的异常处理:

     try:
         GraphRAGRequest(query="This is a very long query for graph recall that exceeds limit.")
     except ValueError as e:
-        raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))
+        raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e)) from e

Also applies to: 129-135

🧰 Tools
🪛 Ruff (0.11.9)

85-85: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
In hugegraph-llm/src/tests/api/test_rag_api.py around lines 46 to 86 and also
lines 129 to 135, the test logic is overly complex trying to simulate FastAPI's
internal validation and exception handling. Simplify the test by directly
instantiating the RAGRequest model with invalid data and asserting that it
raises a ValueError, which is the actual Pydantic validation error. Then
separately test that the endpoint raises HTTPException with status 422 when
given invalid input, avoiding nested try-except blocks and unnecessary comments
about FastAPI internals. This will make the tests clearer and more maintainable.


self.assertEqual(cm.exception.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
error_detail_str = str(cm.exception.detail)
self.assertIn("Query exceeds maximum allowed length", error_detail_str)
# Ensure the actual function was not called
self.mock_rag_answer_func.assert_not_called()

@patch('hugegraph_llm.api.models.rag_requests.LLMConfig')
def test_rag_answer_api_query_within_limit(self, mock_llm_config_pydantic):
rag_answer_api_endpoint = self.get_endpoint_function("/rag")

mock_llm_config_instance = mock_llm_config_pydantic.return_value
mock_llm_config_instance.rag_query_max_length = 50

# Provide default values for all required fields of RAGRequest
# Pydantic model will use the mocked LLMConfig during instantiation
req = RAGRequest(
query="Short query.",
raw_answer=True # ensure at least one answer type is requested
)

# Define return value for the mocked function
# Assuming it returns a tuple of 4 strings based on previous test structure
self.mock_rag_answer_func.return_value = ("raw_res", "vector_res", "graph_res", "gv_res")

response = rag_answer_api_endpoint(req)

self.mock_rag_answer_func.assert_called_once()
# Check if the response is structured as expected
# Based on rag_api.py, it returns a dict including the query and results for requested answer types
self.assertIn("query", response)
self.assertEqual(response["query"], "Short query.")
self.assertIn("raw_answer", response) # since req.raw_answer = True
self.assertEqual(response["raw_answer"], "raw_res")

@patch('hugegraph_llm.api.models.rag_requests.LLMConfig')
def test_graph_rag_recall_api_query_too_long(self, mock_llm_config_pydantic):
graph_rag_recall_api_endpoint = self.get_endpoint_function("/rag/graph")

mock_llm_config_instance = mock_llm_config_pydantic.return_value
mock_llm_config_instance.rag_query_max_length = 10

with self.assertRaises(HTTPException) as cm:
# Similar to the above, simulating FastAPI's handling of Pydantic ValueError
try:
GraphRAGRequest(query="This is a very long query for graph recall that exceeds limit.")
except ValueError as e:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))


self.assertEqual(cm.exception.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
error_detail_str = str(cm.exception.detail)
self.assertIn("query", error_detail_str)
self.assertIn("Query exceeds maximum allowed length", error_detail_str)
self.mock_graph_rag_recall_func.assert_not_called()

@patch('hugegraph_llm.api.models.rag_requests.LLMConfig')
def test_graph_rag_recall_api_query_within_limit(self, mock_llm_config_pydantic):
graph_rag_recall_api_endpoint = self.get_endpoint_function("/rag/graph")

mock_llm_config_instance = mock_llm_config_pydantic.return_value
mock_llm_config_instance.rag_query_max_length = 50

req = GraphRAGRequest(query="Short graph query.")

expected_recall_result = {"keywords": ["short", "graph", "query"], "match_vids": ["id1"]}
self.mock_graph_rag_recall_func.return_value = expected_recall_result

response = graph_rag_recall_api_endpoint(req)

self.mock_graph_rag_recall_func.assert_called_once()
# Based on rag_api.py, the response is {"graph_recall": user_result}
self.assertIn("graph_recall", response)
# The user_result filters only specific keys, ensure they are present
self.assertIn("keywords", response["graph_recall"])
self.assertEqual(response["graph_recall"]["keywords"], expected_recall_result["keywords"])
self.assertIn("match_vids", response["graph_recall"])
self.assertEqual(response["graph_recall"]["match_vids"], expected_recall_result["match_vids"])

if __name__ == '__main__':
unittest.main()
Loading