semmyKG / app.py
semmyk's picture
v0.2.8.6: Baseline 03 - fix require einops for nomic - update Gradio embed components, add queue - update README - attempt fixing GenAI role:assistant with modify_history_in_place()
a50f7ce
import os ## replace with Path
from pathlib import Path
import numpy as np ##SMY
import gradio as gr
#from watchfiles import run_process ##gradio reload watch
from app_gradio_lightrag import LightRAGApp ##SMY lightrag logging
from utils.llm_login import get_login_token
from utils.file_utils import accumulate_dir
import asyncio
import nest_asyncio
nest_asyncio.apply #
import logging, logging.config
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
'''
# Pythonic error handling decorator
def handle_errors(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
return gr.update(value=f"Error: {e}")
return wrapper
'''
# Instantiate app logic
#app_logic = LightRAGApp() ## See main()
# Gradio UI
def gradio_ui(app_logic: LightRAGApp):
css_icon = """
/* Make reveal button looks like an icon rather than a standard button */
.password-box {
position: relative;
display: flex;
align-items: center;
min-width: 30;
}
.password-box > :first-child {
flex-grow: 1;
}
.icon-button {
position: absolute;
right: 10px;
top: 70%;
transform: translateY(-50%);
border: none;
background: none;
color: #4B4B4B;
font-size: 1.2em;
padding: 0;
min-width: 0;
box-shadow: none;
cursor: pointer;
z-index: 100; /* on top */
}
"""
with gr.Blocks(theme=gr.themes.Soft(), title="SemmyKG - LightRAG Knowledge Graph App", css=css_icon) as gradio_ui: #demo:
gr.Markdown("""
# SemmyKG: LightRAG-based Knowledge Graph RAG
Upload your markdown docs, index and build a knowledge graph, and query with OpenAI or Ollama. Visualise the KG interactively.
""")
# Step 0: Section 1
# Define ext type (in lieu of getting from global var)
#ext = (".md", "md") #SMY disused: 'tuple' object has no attribute '_id'
# Define openai_api textbox initial value
openai_api_key_init = os.getenv("OPENAI_API_KEY", "jan-ai")
with gr.Sidebar(position="right"):
system_prompt_tb = gr.Textbox(
value="You are a helpful assistant. You answer questions based on the provided context.", # If you don't know the answer, just say so. Don't make up information.",
label="System Prompt",
lines=3,
interactive=True,
show_copy_button=True,
)
with gr.Accordion(label="🛞 LLM settings", open=False):
with gr.Row():
llm_backend_cb = gr.Radio(["OpenAI", "Ollama", "GenAI"], value="OpenAI", label="LLM Backend: OpenAI, Local or GenAI")
llm_model_name_tb = gr.Textbox(value=os.getenv("LLM_MODEL", "openai/gpt-oss-120b"), label="LLM Model Name", show_copy_button=True) #.split('/')[1], label="LLM Model Name") "meta-llama/Llama-4-Maverick-17B-128E-Instruct")), #image-Text-to-Text #"openai/gpt-oss-120b",
#with gr.Row():
with gr.Row(): #elem_classes="password-box"):
#openai_key_tb = gr.Textbox(value=os.getenv("OPENAI_API_KEY", "jan-ai"), label="OpenAI API Key",
# type="password", elem_classes="password-box", container=False, interactive=True, info="OpenAI API Key") #, show_copy_button=True)
openai_key_tb = gr.Textbox(value=openai_api_key_init, label="OpenAI API Key",
type="password", elem_classes="password-box", container=False, interactive=True, info="OpenAI API Key") #, show_copy_button=True)
toggle_btn_openai_key = gr.Button(
value="👁️", # Initial eye icon
elem_classes="icon-button", size="sm") #, min_width=50)
with gr.Row():
openai_baseurl_tb = gr.Textbox(value=os.getenv("OPENAI_API_BASE", "https://router.huggingface.co/v1"), label="OpenAI baseurl", show_copy_button=True)
ollama_host_tb = gr.Textbox(value=os.getenv("OLLAMA_HOST", "http://localhost:1234/v1"), label="Ollama Host", show_copy_button=True)
#ollama_host_tb = gr.Textbox(value=os.getenv("OPENAI_API_EMBED_BASE", ""), label="Ollama Host")
with gr.Row():
embed_backend_dd = gr.Dropdown(choices=["Transformer", "Provider"], value="Transformer", label="Embedding Type")
openai_baseurl_embed_tb = gr.Textbox(placeholder=os.getenv("OPENAI_API_EMBED_BASE", "http://localhost:1234/v1"), label="LLM Embed baseurl", show_copy_button=True)
with gr.Row():
llm_model_embed_tb = gr.Textbox(placeholder=os.getenv("LLM_MODEL_EMBED","text-embedding-bge-m3"), label="LLM Embedding Model", show_copy_button=True) #.split('/')[1], label="Embedding Model")
with gr.Row(): #elem_classes="password-box"):
openai_key_embed_tb = gr.Textbox(value=os.getenv("OPENAI_API_KEY_EMBED", "jan-ai"), label="LLM API Key Embed", #lm-studio
type="password", elem_classes="password-box", container=False, interactive=True, info="LLM API Key Embed") #, show_copy_button=True)
toggle_btn_openai_key_embed = gr.Button(
value="👁️", # Initial eye icon
elem_classes="icon-button", size="sm") #, min_width=50)
#openai_key_embed_tb = gr.Textbox(value=os.getenv("OPENAI_API_KEY_EMBED", "jan-ai"), label="OpenAI API Key Embed", type="password", show_copy_button=True) #("OLLAMA_API_KEY", ""), label="OpenAI API Key Embed", type="password")
# Step 1: Section 2
with gr.Row():
with gr.Column():
#data_folder_tb = gr.Textbox(value="dataset/data/docs2", label="Data Folder (markdown only)", show_copy_button=True)
dir_btn = gr.UploadButton(
#value='dataset/data/', #docs2 #[Errno 13] Permission denied
label="📁 Upload Folder",
#file_types=ext, #["file"],
file_count="directory",
)
upload_count_md = gr.Markdown(visible=False)
working_dir_tb = gr.Textbox(value="./working_folder1", label="lightRAG working folder", show_copy_button=True)
working_dir_reset_cb = gr.Checkbox(value=False, label="Reset working files?")
with gr.Accordion("🤗 HuggingFace Client Control", open=True): #, open=False):
# HuggingFace controls
hf_login_logout_btn = gr.LoginButton( variant="huggingface", value="Sign in to HuggingFace 🤗", logout_value="Logout of HF: ({}) 🤗 \n [NB: check LLM settings & reload files (upload folder)]",)
gr.Markdown("---") #gr.HTML("<hr>")
setup_btn = gr.Button("Initialise App", variant="primary")
status_box = gr.Textbox(label="Status / Progress", interactive=True) #interactive=False)
# Step 2: Section 3
gr.HTML("<hr>") #gr.Markdown("---")
with gr.Row():
index_btn = gr.Button("Index Documents", interactive=False)
stop_btn = gr.Button("Stop", variant="stop") ## Add cancel event button
query_text_tb = gr.Textbox(label="Your Query")
mode_dd = gr.Dropdown(["naive", "local", "global", "hybrid", "mix"], value="hybrid", label="Query Mode")
query_btn = gr.Button("Query")
answer_box_md = gr.Markdown(label="Answer")
# Step 3: Section 4
kg_btn = gr.Button("Visualise Knowledge Graph")
kg_html = gr.HTML(label="Knowledge Graph Visualisation")
# Add progress tracking
progress_tb = gr.Textbox(label="Progress", interactive=False)
##### Processing #####
## Note: 1.4.9 query `references` field, `user_prompt` | lightRAG 1.4.0: QueryParam updated. Remove dependency on graspologic
# Initialise gr.State ##gr.State component initial value must be able to be deepcopied
st_openai_key = gr.State(value=openai_api_key_init) #gr.State("")
st_password1 = gr.State(value="password")
st_password2 = gr.State(value="password")
state_uploaded_file_list = gr.State(value=[])
### Change handling
# Change Handling: update state value
def update_state_stored_value(new_component_input):
""" Updates stored state for Gradio Component
for instance: st_openai_key.value = openai_key_tb
Args:
new_component_input: New value from component
Returns:
Updated value for state
"""
return new_component_input
# Change Handling: update Ollama
def update_ollama(llm_backend):
""" Update LLM settings fields with ollama values"""
# Get model name excluding the model provider: # llm_model_name.rpartition("/")[-1]
if llm_backend == "Ollama":
return {
#llm_backend_cb: gr.update(value="Ollama"),
llm_model_name_tb: gr.update(value=os.getenv("LLM_MODEL", "meta-llama/Llama-4-Maverick-17B-128E-Instruct").rpartition("/")[-1]), #image-Text-to-Text #"openai/gpt-oss-120b", ##Text-to-Text) #(value="llama2"),
openai_key_tb: gr.update(value=os.getenv("OPENAI_API_KEY", "jan-ai"), info="LLM API Key"),
openai_baseurl_tb: gr.update(value=os.getenv("OPENAI_API_BASE", "https://router.huggingface.co/v1")),
ollama_host_tb: gr.update(value=os.getenv("OLLAMA_HOST", "http://localhost:1234/v1")), #"http://localhost:11434"
openai_baseurl_embed_tb: gr.update(value=os.getenv("OPENAI_API_EMBED_BASE", "http://localhost:1234/v1")), #"http://localhost:1234/v1/embeddings"
llm_model_embed_tb: gr.update(value=os.getenv("LLM_MODEL_EMBED","nomic-embed-text")), #"nomic-ai/nomic-embed-text-v1.5"
openai_key_embed_tb: gr.update(value=os.getenv("OPENAI_API_KEY_EMBED", "jan-ai"))
}
elif llm_backend == "GenAI":
return {
llm_model_name_tb: gr.update(value=os.getenv("LLM_MODEL", "google/gemini-2.5-flash-preview-09-2025").rpartition("/")[-1]), #image-Text-to-Text #"google/gemini-2.0-flash-exp:free" #"openai/gpt-oss-120b", ##Text-to-Text) #(value="llama2"),
openai_key_tb: gr.update(value=os.getenv("GEMINI_API_KEY", "jan-ai"), info="GenAI API Key"),
openai_baseurl_tb: gr.update(value=os.getenv("GEMINI_API_BASE", "https://generativelanguage.googleapis.com/v1beta/openai/"), label="GenAI baaseurl"),
ollama_host_tb: gr.update(value=os.getenv("OLLAMA_HOST", "http://localhost:11434")), #"http://localhost:1234/v1"
openai_baseurl_embed_tb: gr.update(value=os.getenv("OPENAI_API_EMBED_BASE", "http://localhost:1234/v1")), #"http://localhost:1234/v1/embeddings"
llm_model_embed_tb: gr.update(value=os.getenv("LLM_MODEL_EMBED", "nomic-ai/nomic-embed-text-v1.5")), #"all-MiniLM-L6-v2")),
openai_key_embed_tb: gr.update(value=os.getenv("OPENAI_API_KEY_EMBED", "jan-ai"))
}
elif llm_backend == "OpenAI":
return {
llm_model_name_tb: gr.update(value=os.getenv("LLM_MODEL", "openai/gpt-oss-120b")), #image-Text-to-Text #"openai/gpt-oss-120b", ##Text-to-Text) #(value="llama2"),
openai_key_tb: gr.update(value=os.getenv("OPENAI_API_KEY", ""), info="OpenAI API Key"),
openai_baseurl_tb: gr.update(value=os.getenv("OPENAI_API_BASE", "https://router.huggingface.co/v1"), label="OpenAI baseurl"),
ollama_host_tb: gr.update(value=os.getenv("OLLAMA_HOST", "http://localhost:11434")), #"http://localhost:1234/v1"
openai_baseurl_embed_tb: gr.update(value=os.getenv("OPENAI_API_EMBED_BASE", "https://api.openai.com/v1")), #"http://localhost:1234/v1/embeddings"
llm_model_embed_tb: gr.update(value=os.getenv("LLM_MODEL_EMBED","text-embedding-3-small")),
openai_key_embed_tb: gr.update(value=os.getenv("OPENAI_API_KEY_EMBED", ""))
}
# Change Handling: update Ollama
def update_embedding_backend(embedding_backend):
""" Update LLM settings fields with ollama values"""
# Get model name excluding the model provider: # llm_model_name.rpartition("/")[-1]
if embedding_backend == "Provider":
return {
openai_baseurl_embed_tb: gr.update(value=os.getenv("OPENAI_API_EMBED_BASE", "http://localhost:1234/v1")), #"http://localhost:1234/v1/embeddings"
llm_model_embed_tb: gr.update(value=os.getenv("LLM_MODEL_EMBED","nomic-embed-text")),
openai_key_embed_tb: gr.update(value=os.getenv("OPENAI_API_KEY_EMBED", "jan-ai"))
}
elif embedding_backend == "Transformer":
return {
openai_baseurl_embed_tb: gr.update(value=None, placeholder=os.getenv("OPENAI_API_EMBED_BASE", "http://localhost:1234/v1")), #"http://localhost:1234/v1/embeddings"
llm_model_embed_tb: gr.update(value=None, placeholder=os.getenv("LLM_MODEL_EMBED", "nomic-ai/nomic-embed-text-v1.5")), #(value="all-MiniLM-L6-v2"),
openai_key_embed_tb: gr.update(value=None, placeholder="jan-ai")
}
# Change Handling: Update password reveal state - reusable function for toggling password visibility
def toggle_password(current_state):
""" Change state
Change password input field between visible/hidden
Args:
current_state: Current password visibility state
Returns:
Tuple of updates for textbox, button and state
"""
new_state = "text" if current_state == "password" else "password"
new_icon = "👁️" if new_state == "password" else "👁️‍🗨️" # Change icon with state
return [ #(
gr.update(type=new_state), #gr.Textbox.update(type=new_state),
gr.update(value=new_icon), #gr.Button.update(value=new_icon),
new_state ] #)
# Update gr.State values on HF login change.
def custom_do_logout(openai_key, oauth_token: gr.OAuthToken | None=None,):
#''' ##SMY: TO DELETE
st_openai_key_get = os.getenv("OPENAI_API_KEY", default="") #"" ##SMY: # UnboundLocalError: not catching
try:
if oauth_token or oauth_token is not None: ##SMY: hack: is not None!
st_openai_key_get= update_state_stored_value(oauth_token.token) ##SMY: currently not used optimally
except AttributeError:
st_openai_key_get= get_login_token(openai_key) #(openai_key_tb)
#'''
#return gr.update(value="Sign in to HuggingFace 🤗")
return gr.update(value="Sign in to HuggingFace 🤗"), gr.update(value=st_openai_key_get) #, gr.update(visible=True, value=msg) #, state_api_token_arg
# Button logic with async handling
async def setup_wrapper(df, wd, wd_reset, llm_back, embed_back, oai, base, base_embed, model, model_embed, host, embedkey, sys_prompt):
return await app_logic.setup(df, wd, wd_reset, llm_back, embed_back, oai,
base, base_embed, model, model_embed, host, embedkey, sys_prompt)
async def index_wrapper(df):
return await app_logic.index_documents(df)
async def query_wrapper(q, m):
return await app_logic.query(q, m)
def stop_wrapper(): ##SMY sync or async
"""Cancel event wrapper"""
app_logic.trigger_cancel()
return "Cancellation requested. Awaiting current step to finish..."
### Change handlers
llm_backend_cb.change(show_progress="hidden", fn=update_ollama, inputs=llm_backend_cb, #inputs=None,
outputs=[llm_model_name_tb, openai_key_tb, openai_baseurl_tb, ollama_host_tb, openai_baseurl_embed_tb, llm_model_embed_tb, openai_key_embed_tb])
embed_backend_dd.change(show_progress="hidden", fn=update_embedding_backend, inputs=embed_backend_dd,
outputs=[openai_baseurl_embed_tb,llm_model_embed_tb, openai_key_embed_tb],)
### Button handlers
#hf_login_logout_btn.click(update_state_stored_value, inputs=openai_key_tb, outputs=st_openai_key)
hf_login_logout_btn.click(fn=custom_do_logout, inputs=openai_key_tb, outputs=[hf_login_logout_btn, st_openai_key])
dir_btn.upload(
fn=accumulate_dir,
inputs=[dir_btn, state_uploaded_file_list],
outputs=[state_uploaded_file_list, index_btn, upload_count_md, status_box],
show_progress="hidden"
)
toggle_btn_openai_key.click(
fn=toggle_password,
inputs=[st_password1],
outputs=[openai_key_tb, toggle_btn_openai_key, st_password1],
show_progress="hidden"
)
toggle_btn_openai_key_embed.click(
fn=toggle_password,
inputs=[st_password2],
outputs=[openai_key_embed_tb, toggle_btn_openai_key_embed, st_password2],
show_progress="hidden"
)
'''
async def setup(self, data_folder: str, working_dir: str, wdir_reset: bool, llm_backend: str, embed_backend: str, openai_key: str,
openai_baseurl: str, openai_baseurl_embed: str, llm_model_name: str, llm_model_embed: str,
ollama_host: str, embed_key: str, system_prompt: str) -> str:
'''
inputs_arg = [state_uploaded_file_list, working_dir_tb, working_dir_reset_cb, llm_backend_cb, embed_backend_dd, st_openai_key, #openai_key_tb,
openai_baseurl_tb, openai_baseurl_embed_tb, llm_model_name_tb, llm_model_embed_tb,
ollama_host_tb, openai_key_embed_tb, system_prompt_tb] #data_folder_tb,
setup_btn.click(
fn=setup_wrapper,
#inputs=[data_folder_tb, working_dir_tb, llm_backend_cb, openai_key_tb, openai_baseurl_tb, openai_baseurl_embed_tb, llm_model_name_tb, llm_model_embed_tb, ollama_host_tb, openai_key_embed_tb],
inputs=inputs_arg,
outputs=status_box,
show_progress=True
)
index_btn.click(
fn=index_wrapper,
inputs=state_uploaded_file_list, #[data_folder_tb],
outputs=[status_box, progress_tb],
show_progress=True
)
query_btn.click(
fn=query_wrapper,
inputs=[query_text_tb, mode_dd],
outputs=answer_box_md
)
kg_btn.click(
fn=app_logic.show_kg,
inputs=None,
outputs=kg_html,
show_progress=True
)
stop_btn.click(
fn=stop_wrapper,
inputs=[],
outputs=[status_box]
)
return gradio_ui
if __name__ == "__main__":
#gradio_ui().launch()
##SMY: assist: https://www.gradio.app/guides/developing-faster-with-reload-mode
##SMY: NB: gradio app_gradio_lightrag.py --demo-name=gradio_ui
async def main():
from app_gradio_lightrag import LightRAGApp
# Instantiate LightRAG and launch Gradio
try:
app_logic = LightRAGApp()
# Launch Gradio with queue enable to enable >60s timeout
gradio_ui(app_logic).queue().launch() #(server_port=7866)
except Exception as e:
print(f"An error occurred: {e}")
finally:
if app_logic.rag:
await app_logic.rag.finalize_storages()
from utils.logger import get_logger, setup_logging
setup_logging() ## set logging
logger_kg = get_logger("semmyKG") ## app logging
##SMY Initialise logging before running the main function: See lightrag_openai_compatible_demo.py
from app_gradio_lightrag import handle_errors, configure_logging
configure_logging() ## lightRAG logging
asyncio.run(main())
##SMY: gradio reload-mode watch: https://github.com/huggingface/smolagents/issues/789
#run_process(".", target=gradio_ui)