This is an automated email from the ASF dual-hosted git repository.
jin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-hugegraph-ai.git
The following commit(s) were added to refs/heads/main by this push:
new 41412a6 feat(llm): support log_block in RAG platform & rest-api (#110)
41412a6 is described below
commit 41412a6320860a05d5e9d0b1826a6460664c1844
Author: Dylan <[email protected]>
AuthorDate: Sat Nov 16 14:51:28 2024 +0800
feat(llm): support log_block in RAG platform & rest-api (#110)
curl command for rolling logs in cmd:
```bash
curl -X POST "http://127.0.0.1:8001/logs" -H "Content-Type:
application/json" \
-H "Authorization: Bearer 123456" \
-d "{\"log_token\": \"000000\", \"log_file\": \"llm-server.log\"}"
```
---------
Co-authored-by: imbajin <[email protected]>
---
hugegraph-llm/src/hugegraph_llm/api/admin_api.py | 36 +++++
.../src/hugegraph_llm/api/models/rag_requests.py | 5 +
hugegraph-llm/src/hugegraph_llm/api/rag_api.py | 4 +
hugegraph-llm/src/hugegraph_llm/config/config.py | 25 +++-
.../src/hugegraph_llm/config/config_data.py | 6 +-
hugegraph-llm/src/hugegraph_llm/config/generate.py | 2 +-
.../src/hugegraph_llm/demo/rag_demo/admin_block.py | 153 +++++++++++++++++++++
.../src/hugegraph_llm/demo/rag_demo/app.py | 15 +-
.../src/hugegraph_llm/demo/rag_demo/other_block.py | 2 +-
.../src/hugegraph_llm/demo/rag_demo/rag_block.py | 28 ++--
.../demo/rag_demo/vector_graph_block.py | 2 +-
11 files changed, 251 insertions(+), 27 deletions(-)
diff --git a/hugegraph-llm/src/hugegraph_llm/api/admin_api.py
b/hugegraph-llm/src/hugegraph_llm/api/admin_api.py
new file mode 100644
index 0000000..f933b71
--- /dev/null
+++ b/hugegraph-llm/src/hugegraph_llm/api/admin_api.py
@@ -0,0 +1,36 @@
+# 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.
+import os
+
+from fastapi import status, APIRouter
+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
+
+
+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 os.getenv('ADMIN_TOKEN') != req.admin_token:
+ raise
generate_response(RAGResponse(status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid admin_token"))
+ else:
+ log_path = os.path.join("logs", req.log_file)
+
+ # Create a StreamingResponse that reads from the log stream
generator
+ return StreamingResponse(log_stream(log_path),
media_type="text/plain")
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 0e7c666..5ee5c05 100644
--- a/hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py
+++ b/hugegraph-llm/src/hugegraph_llm/api/models/rag_requests.py
@@ -70,3 +70,8 @@ class RerankerConfigRequest(BaseModel):
reranker_type: str
api_key: str
cohere_base_url: Optional[str] = None
+
+class LogStreamRequest(BaseModel):
+ admin_token: Optional[str] = None
+ log_file: Optional[str] = 'llm-server.log'
+
\ No newline at end of file
diff --git a/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
b/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
index 8379550..cf57095 100644
--- a/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
+++ b/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
@@ -14,10 +14,12 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+import os
import json
from typing import Literal
from fastapi import status, APIRouter, HTTPException
+from fastapi.responses import StreamingResponse
from hugegraph_llm.api.exceptions.rag_exceptions import generate_response
from hugegraph_llm.api.models.rag_requests import (
@@ -25,6 +27,7 @@ from hugegraph_llm.api.models.rag_requests import (
GraphConfigRequest,
LLMConfigRequest,
RerankerConfigRequest, GraphRAGRequest,
+ LogStreamRequest,
)
from hugegraph_llm.api.models.rag_response import RAGResponse
from hugegraph_llm.config import settings
@@ -126,3 +129,4 @@ def rag_http_api(
else:
res = status.HTTP_501_NOT_IMPLEMENTED
return generate_response(RAGResponse(status_code=res, message="Missing
Value"))
+
\ No newline at end of file
diff --git a/hugegraph-llm/src/hugegraph_llm/config/config.py
b/hugegraph-llm/src/hugegraph_llm/config/config.py
index c6e06e7..499d6a0 100644
--- a/hugegraph-llm/src/hugegraph_llm/config/config.py
+++ b/hugegraph-llm/src/hugegraph_llm/config/config.py
@@ -39,7 +39,7 @@ def read_dotenv() -> dict[str, Optional[str]]:
log.info("Loading %s successfully!", env_path)
for key, value in env_config.items():
if key not in os.environ:
- os.environ[key] = value or ""
+ os.environ[key] = value or "" # upper
return env_config
@@ -50,10 +50,10 @@ class Config(ConfigData):
if os.path.exists(env_path):
env_config = read_dotenv()
for key, value in env_config.items():
- if key in self.__annotations__ and value:
- if self.__annotations__[key] in [int, Optional[int]]:
+ if key.lower() in self.__annotations__ and value:
+ if self.__annotations__[key.lower()] in [int,
Optional[int]]:
value = int(value)
- setattr(self, key, value)
+ setattr(self, key.lower(), value)
else:
self.generate_env()
@@ -67,7 +67,7 @@ class Config(ConfigData):
else:
config_dict = {}
for k, v in self.__dict__.items():
- config_dict[k] = v
+ config_dict[k.upper()] = v
with open(env_path, "w", encoding="utf-8") as f:
for k, v in config_dict.items():
if v is None:
@@ -76,10 +76,23 @@ class Config(ConfigData):
f.write(f"{k}={v}\n")
log.info("Generate %s successfully!", env_path)
+
+ def check_env(self):
+ config_dict = {}
+ for k, v in self.__dict__.items():
+ config_dict[k.upper()] = str(v) if v else ""
+ env_config = dotenv_values(f"{env_path}")
+ for k, v in config_dict.items():
+ if k in env_config:
+ continue
+ log.info("Update %s: %s=%s", env_path, k, v)
+ set_key(env_path, k, v, quote_mode="never")
+
+
def update_env(self):
config_dict = {}
for k, v in self.__dict__.items():
- config_dict[k] = str(v) if v else ""
+ config_dict[k.upper()] = str(v) if v else ""
env_config = dotenv_values(f"{env_path}")
for k, v in config_dict.items():
if k in env_config and env_config[k] == v:
diff --git a/hugegraph-llm/src/hugegraph_llm/config/config_data.py
b/hugegraph-llm/src/hugegraph_llm/config/config_data.py
index 3610aa2..004a29e 100644
--- a/hugegraph-llm/src/hugegraph_llm/config/config_data.py
+++ b/hugegraph-llm/src/hugegraph_llm/config/config_data.py
@@ -71,10 +71,14 @@ class ConfigData:
graph_pwd: Optional[str] = "xxx"
graph_space: Optional[str] = None
+ """Admin settings"""
+ enable_login: Optional[str] = "False"
+ user_token: Optional[str] = "4321"
+ admin_token: Optional[str] = "xxxx"
+
# Additional static content like PromptConfig
class PromptData:
-
# Data is detached from llm_op/answer_synthesize.py
answer_prompt = """You are an expert in knowledge graphs and natural
language processing.
Your task is to provide a precise and accurate answer based on the given
context.
diff --git a/hugegraph-llm/src/hugegraph_llm/config/generate.py
b/hugegraph-llm/src/hugegraph_llm/config/generate.py
index 0961b39..1d85feb 100644
--- a/hugegraph-llm/src/hugegraph_llm/config/generate.py
+++ b/hugegraph-llm/src/hugegraph_llm/config/generate.py
@@ -22,7 +22,7 @@ from hugegraph_llm.config import Config, PromptConfig
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate hugegraph-llm
config file")
- parser.add_argument("-U", "--update", action="store_true", help="Update
the config file")
+ parser.add_argument("-U", "--update", default=True, action="store_true",
help="Update the config file")
args = parser.parse_args()
if args.update:
Config().generate_env()
diff --git a/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/admin_block.py
b/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/admin_block.py
new file mode 100644
index 0000000..db34b7e
--- /dev/null
+++ b/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/admin_block.py
@@ -0,0 +1,153 @@
+# 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.
+
+import asyncio
+import os
+
+import gradio as gr
+from gradio import Request
+
+from hugegraph_llm.utils.log import log
+
+
+async def log_stream(log_path: str):
+ """
+ Stream the content of a log file like `tail -f`.
+ """
+ try:
+ with open(log_path, 'r') as file:
+ while True:
+ line = file.readline()
+ if line:
+ yield line
+ else:
+ await asyncio.sleep(0.1) # Non-blocking sleep
+ except FileNotFoundError:
+ raise Exception(f"Log file not found: {log_path}")
+ except Exception as e:
+ raise Exception(f"An error occurred while reading the log: {str(e)}")
+
+
+# Functions to read each log file
+def read_llm_server_log():
+ try:
+ with open("logs/llm-server.log", "r") as f:
+ # TODO: avoid read the whole file (read latest N lines instead)
+ return f.read()
+ except FileNotFoundError:
+ return "LLM Server log file not found."
+
+
+# Functions to clear each log file
+def clear_llm_server_log():
+ with open("logs/llm-server.log", "w") as f:
+ f.truncate(0) # Clear the contents of the file
+ return "LLM Server log cleared."
+
+
+# Function to validate password and control access to logs
+def check_password(password, request: Request = None):
+ client_ip = request.client.host if request else "Unknown IP"
+
+ if password == os.getenv('ADMIN_TOKEN'):
+ # Return logs and update visibility
+ llm_log = read_llm_server_log()
+ # Log the successful access with the IP address
+ log.info(f"Logs accessed successfully from IP: {client_ip}")
+ return (
+ llm_log,
+ gr.update(visible=True),
+ gr.update(visible=True),
+ gr.update(visible=True),
+ gr.update(visible=False))
+ else:
+ # Log the failed attempt with IP address
+ log.error(f"Incorrect password attempt from IP: {client_ip}")
+ return (
+ "",
+ gr.update(visible=False),
+ gr.update(visible=False),
+ gr.update(visible=False),
+ gr.update(value="Incorrect password. Access denied.", visible=True)
+ )
+
+
+def create_admin_block():
+ with gr.Blocks():
+ gr.Markdown("## Admin Info - Password Protected")
+
+ # Password input
+ password_input = gr.Textbox(
+ label="Enter Password",
+ type="password",
+ placeholder="Enter password to access admin information",
+ )
+
+ # Error message box, initially hidden
+ error_message = gr.Textbox(
+ label="",
+ visible=False,
+ interactive=False,
+ elem_classes="error-message"
+ )
+
+ # Button to submit password
+ submit_button = gr.Button("Submit")
+
+ with gr.Row(visible=False) as hidden_row:
+ with gr.Column():
+ # LLM Server log display, refreshes every 1 second
+ gr.Markdown("### LLM Server Log")
+ llm_server_log_output = gr.Textbox(
+ label="LLM Server Log (llm-server.log)",
+ lines=20,
+ value=read_llm_server_log, # Initial value using the
function
+ show_copy_button=True,
+ elem_classes="log-container",
+ every=60, # Refresh every 60 second
+ autoscroll=True # Enable auto-scroll
+ )
+ with gr.Row():
+ with gr.Column():
+ # Button to clear LLM Server log, initially hidden
+ clear_llm_server_button = gr.Button("Clear LLM Server
Log", visible=False)
+ with gr.Column():
+ # Button to refresh LLM Server log manually
+ refresh_llm_server_button = gr.Button("Refresh LLM
Server Log", visible=False,
+
variant="primary")
+
+ # Define what happens when the password is submitted
+ submit_button.click(
+ fn=check_password,
+ inputs=[password_input],
+ outputs=[llm_server_log_output, hidden_row,
clear_llm_server_button,
+ refresh_llm_server_button, error_message],
+ )
+
+ # Define what happens when the Clear LLM Server Log button is clicked
+ clear_llm_server_button.click(
+ fn=clear_llm_server_log,
+ inputs=[],
+ outputs=[llm_server_log_output],
+ )
+
+ # Define what happens when the Refresh LLM Server Log button is clicked
+ refresh_llm_server_button.click(
+ fn=read_llm_server_log,
+ inputs=[],
+ outputs=[llm_server_log_output],
+ )
diff --git a/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/app.py
b/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/app.py
index 89696b9..16cabf4 100644
--- a/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/app.py
+++ b/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/app.py
@@ -25,6 +25,7 @@ from fastapi import FastAPI, Depends, APIRouter
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from hugegraph_llm.api.rag_api import rag_http_api
+from hugegraph_llm.api.admin_api import admin_http_api
from hugegraph_llm.config import settings, prompt
from hugegraph_llm.demo.rag_demo.configs_block import (
create_configs_block,
@@ -36,6 +37,7 @@ from hugegraph_llm.demo.rag_demo.configs_block import (
from hugegraph_llm.demo.rag_demo.other_block import create_other_block
from hugegraph_llm.demo.rag_demo.rag_block import create_rag_block, rag_answer
from hugegraph_llm.demo.rag_demo.vector_graph_block import
create_vector_graph_block
+from hugegraph_llm.demo.rag_demo.admin_block import create_admin_block,
log_stream
from hugegraph_llm.resources.demo.css import CSS
from hugegraph_llm.utils.log import log
@@ -43,7 +45,7 @@ sec = HTTPBearer()
def authenticate(credentials: HTTPAuthorizationCredentials = Depends(sec)):
- correct_token = os.getenv("TOKEN")
+ correct_token = os.getenv("USER_TOKEN")
if credentials.credentials != correct_token:
from fastapi import HTTPException
@@ -91,8 +93,11 @@ def init_rag_ui() -> gr.Interface:
textbox_input_schema, textbox_info_extract_template =
create_vector_graph_block()
with gr.Tab(label="2. (Graph)RAG & User Functions 📖"):
textbox_inp, textbox_answer_prompt_input = create_rag_block()
- with gr.Tab(label="3. Others Tools 🚧"):
+ with gr.Tab(label="3. Graph Tools 🚧"):
create_other_block()
+ with gr.Tab(label="4. Admin Tools ⚙️"):
+ create_admin_block()
+
def refresh_ui_config_prompt() -> tuple:
settings.from_env()
@@ -128,11 +133,15 @@ if __name__ == "__main__":
args = parser.parse_args()
app = FastAPI()
api_auth = APIRouter(dependencies=[Depends(authenticate)])
+
+ settings.check_env()
+ prompt.update_yaml_file()
hugegraph_llm = init_rag_ui()
rag_http_api(api_auth, rag_answer, apply_graph_config, apply_llm_config,
apply_embedding_config,
apply_reranker_config)
-
+ admin_http_api(api_auth, log_stream)
+
app.include_router(api_auth)
auth_enabled = os.getenv("ENABLE_LOGIN", "False").lower() == "true"
log.info("(Status) Authentication is %s now.", "enabled" if auth_enabled
else "disabled")
diff --git a/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/other_block.py
b/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/other_block.py
index 5297f8a..a156442 100644
--- a/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/other_block.py
+++ b/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/other_block.py
@@ -21,7 +21,7 @@ from hugegraph_llm.utils.hugegraph_utils import
init_hg_test_data, run_gremlin_q
def create_other_block():
- gr.Markdown("""## 4. Other Tools """)
+ gr.Markdown("""## Other Tools """)
with gr.Row():
inp = gr.Textbox(value="g.V().limit(10)", label="Gremlin query",
show_copy_button=True, lines=8)
out = gr.Code(label="Output", language="json",
elem_classes="code-container-show")
diff --git a/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/rag_block.py
b/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/rag_block.py
index 3dd9eec..c4263f4 100644
--- a/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/rag_block.py
+++ b/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/rag_block.py
@@ -91,7 +91,7 @@ def rag_answer(
def create_rag_block():
# pylint: disable=R0915 (too-many-statements)
- gr.Markdown("""## 2. RAG with HugeGraph""")
+ gr.Markdown("""## 1. HugeGraph RAG Query""")
with gr.Row():
with gr.Column(scale=2):
inp = gr.Textbox(value=prompt.default_question, label="Question",
show_copy_button=True, lines=2)
@@ -122,7 +122,7 @@ def create_rag_block():
value="reranker" if online_rerank else "bleu",
label="Rerank method",
)
- graph_ratio = gr.Slider(0, 1, 0.5, label="Graph Ratio",
step=0.1, interactive=False)
+ graph_ratio = gr.Slider(0, 1, 0.6, label="Graph Ratio",
step=0.1, interactive=False)
graph_vector_radio.change(
toggle_slider, inputs=graph_vector_radio,
outputs=graph_ratio
@@ -155,7 +155,7 @@ def create_rag_block():
outputs=[raw_out, vector_only_out, graph_only_out, graph_vector_out],
)
- gr.Markdown("""## 3. User Functions (Back-testing)
+ gr.Markdown("""## 2. (Batch) Back-testing )
> 1. Download the template file & fill in the questions you want to test.
> 2. Upload the file & click the button to generate answers. (Preview
shows the first 40 lines)
> 3. The answer options are the same as the above RAG/Q&A frame
@@ -200,17 +200,17 @@ def create_rag_block():
return df
def several_rag_answer(
- is_raw_answer: bool,
- is_vector_only_answer: bool,
- is_graph_only_answer: bool,
- is_graph_vector_answer: bool,
- graph_ratio: float,
- rerank_method: Literal["bleu", "reranker"],
- near_neighbor_first: bool,
- custom_related_information: str,
- answer_prompt: str,
- progress=gr.Progress(track_tqdm=True),
- answer_max_line_count: int = 1,
+ is_raw_answer: bool,
+ is_vector_only_answer: bool,
+ is_graph_only_answer: bool,
+ is_graph_vector_answer: bool,
+ graph_ratio: float,
+ rerank_method: Literal["bleu", "reranker"],
+ near_neighbor_first: bool,
+ custom_related_information: str,
+ answer_prompt: str,
+ progress=gr.Progress(track_tqdm=True),
+ answer_max_line_count: int = 1,
):
df = pd.read_excel(questions_path, dtype=str)
total_rows = len(df)
diff --git
a/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/vector_graph_block.py
b/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/vector_graph_block.py
index 1af8a51..b8e087c 100644
--- a/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/vector_graph_block.py
+++ b/hugegraph-llm/src/hugegraph_llm/demo/rag_demo/vector_graph_block.py
@@ -43,7 +43,7 @@ def store_prompt(schema, example_prompt):
def create_vector_graph_block():
# pylint: disable=no-member
gr.Markdown(
- """## 1. Build Vector/Graph Index & Extract Knowledge Graph
+ """## Build Vector/Graph Index & Extract Knowledge Graph
- Docs:
- text: Build rag index from plain text
- file: Upload file(s) which should be <u>TXT</u> or <u>.docx</u> (Multiple
files can be selected together)