diff --git a/.env.example b/.env.example index 27cbc2c15..29cf565b2 100644 --- a/.env.example +++ b/.env.example @@ -50,11 +50,13 @@ PDFJS_VERSION_DIST="pdfjs-4.0.379-dist" # settings for PaddleOCR PADDLE_DEVICE=gpu +# setting for Tavily API (for web search) +TAVILY_API_KEY= + # variable for authentication method selection # for authentication with google leave empty # for authentication with keycloak : # AUTHENTICATION_METHOD="KEYCLOAK" - AUTHENTICATION_METHOD= # settings for keycloak @@ -62,3 +64,9 @@ KEYCLOAK_SERVER_URL= KEYCLOAK_CLIENT_ID= KEYCLOAK_REALM= KEYCLOAK_CLIENT_SECRET= + +# settings for Gradio UI +# maximum concurrent indexing jobs from the file/index page +KH_GRADIO_INDEX_CONCURRENCY_LIMIT=20 +# maximum concurrent quick indexing jobs from the chat upload flow +KH_GRADIO_QUICK_INDEX_CONCURRENCY_LIMIT=10 diff --git a/app.py b/app.py index 393abb323..8c19b9c4c 100644 --- a/app.py +++ b/app.py @@ -15,7 +15,12 @@ app = App() demo = app.make() -demo.queue().launch( +queue_kwargs = {} +default_concurrency_limit = os.getenv("KH_GRADIO_DEFAULT_CONCURRENCY_LIMIT") +if default_concurrency_limit: + queue_kwargs["default_concurrency_limit"] = int(default_concurrency_limit) + +demo.queue(**queue_kwargs).launch( favicon_path=app._favicon, inbrowser=True, allowed_paths=[ diff --git a/libs/ktem/ktem/assets/js/main.js b/libs/ktem/ktem/assets/js/main.js index 8e6385357..9d50a1c19 100644 --- a/libs/ktem/ktem/assets/js/main.js +++ b/libs/ktem/ktem/assets/js/main.js @@ -151,8 +151,12 @@ function run() { ); var last_bot_message = bot_messages[bot_messages.length - 1]; - // check if the last bot message has class "text_selection" - if (last_bot_message.classList.contains("text_selection")) { + if (!last_bot_message) { + return; + } + + // check if the last bot message has already been initialized + if (last_bot_message.dataset.evidenceSearchReady === "true") { return; } @@ -165,6 +169,10 @@ function run() { ); console.log("Indexing evidences", evidences); + if (evidences.length === 0) { + return; + } + const segmenterEn = new Intl.Segmenter("en", { granularity: "sentence" }); // Split sentences and save to all_segments list var all_segments = []; @@ -191,6 +199,10 @@ function run() { } } + if (all_segments.length === 0) { + return; + } + let miniSearch = new MiniSearch({ fields: ["text"], // fields to index for full-text search storeFields: ["text"], @@ -198,6 +210,7 @@ function run() { // Index all documents miniSearch.addAll(all_segments); + last_bot_message.dataset.evidenceSearchReady = "true"; last_bot_message.addEventListener("mouseup", () => { let selection = window.getSelection().toString(); diff --git a/libs/ktem/ktem/embeddings/ui.py b/libs/ktem/ktem/embeddings/ui.py index 29a52ffb0..891804cc4 100644 --- a/libs/ktem/ktem/embeddings/ui.py +++ b/libs/ktem/ktem/embeddings/ui.py @@ -132,10 +132,12 @@ def _on_app_created(self): self.list_embeddings, inputs=[], outputs=[self.emb_list], + show_progress="hidden", ) self._app.app.load( lambda: gr.update(choices=list(embedding_models_manager.vendors().keys())), outputs=[self.emb_choices], + show_progress="hidden", ) def on_emb_vendor_change(self, vendor): @@ -206,7 +208,7 @@ def on_register_events(self): inputs=[self.selected_emb_name], outputs=[self.selected_emb_name], show_progress="hidden", - ).then( + ).success( self.list_embeddings, inputs=[], outputs=[self.emb_list], @@ -231,7 +233,7 @@ def on_register_events(self): ], outputs=[self.selected_emb_name], show_progress="hidden", - ).then( + ).success( self.list_embeddings, inputs=[], outputs=[self.emb_list], diff --git a/libs/ktem/ktem/index/file/ui.py b/libs/ktem/ktem/index/file/ui.py index 5c7d558c0..fc2f54048 100644 --- a/libs/ktem/ktem/index/file/ui.py +++ b/libs/ktem/ktem/index/file/ui.py @@ -28,6 +28,12 @@ DOWNLOAD_MESSAGE = "Start download" MAX_FILENAME_LENGTH = 20 MAX_FILE_COUNT = 200 +DEFAULT_INDEX_CONCURRENCY_LIMIT = int( + os.getenv("KH_GRADIO_INDEX_CONCURRENCY_LIMIT", "20") +) +DEFAULT_QUICK_INDEX_CONCURRENCY_LIMIT = int( + os.getenv("KH_GRADIO_QUICK_INDEX_CONCURRENCY_LIMIT", "10") +) chat_input_focus_js = """ function() { @@ -723,7 +729,7 @@ def on_register_quick_uploads(self): self._app.user_id, ], outputs=self.quick_upload_state, - concurrency_limit=10, + concurrency_limit=DEFAULT_QUICK_INDEX_CONCURRENCY_LIMIT, ) .success( fn=lambda: [ @@ -752,10 +758,16 @@ def on_register_quick_uploads(self): outputs=self._app.chat_page.quick_file_upload_status, ) .then( - fn=self.list_file, - inputs=[self._app.user_id, self.filter], + fn=self.list_file_if_index_changed, + inputs=[ + self.quick_upload_state, + self._app.user_id, + self.filter, + self.file_list_state, + self.file_list, + ], outputs=[self.file_list_state, self.file_list], - concurrency_limit=20, + concurrency_limit=DEFAULT_INDEX_CONCURRENCY_LIMIT, ) .then( fn=lambda: True, @@ -782,7 +794,7 @@ def on_register_quick_uploads(self): self._app.user_id, ], outputs=self.quick_upload_state, - concurrency_limit=10, + concurrency_limit=DEFAULT_QUICK_INDEX_CONCURRENCY_LIMIT, ) .success( fn=lambda: [ @@ -809,10 +821,16 @@ def on_register_quick_uploads(self): if not KH_DEMO_MODE: quickURLUploadedEvent = quickURLUploadedEvent.then( - fn=self.list_file, - inputs=[self._app.user_id, self.filter], + fn=self.list_file_if_index_changed, + inputs=[ + self.quick_upload_state, + self._app.user_id, + self.filter, + self.file_list_state, + self.file_list, + ], outputs=[self.file_list_state, self.file_list], - concurrency_limit=20, + concurrency_limit=DEFAULT_INDEX_CONCURRENCY_LIMIT, ) quickURLUploadedEvent = quickURLUploadedEvent.then( @@ -981,7 +999,7 @@ def on_register_events(self): self._app.user_id, ], outputs=[self.upload_result, self.upload_info], - concurrency_limit=20, + concurrency_limit=DEFAULT_INDEX_CONCURRENCY_LIMIT, ) .then( fn=lambda: gr.update(value=""), @@ -993,7 +1011,7 @@ def on_register_events(self): fn=self.list_file, inputs=[self._app.user_id, self.filter], outputs=[self.file_list_state, self.file_list], - concurrency_limit=20, + concurrency_limit=DEFAULT_INDEX_CONCURRENCY_LIMIT, ) for event in self._app.get_event(f"onFileIndex{self._index.id}Changed"): uploadedEvent = uploadedEvent.then(**event) @@ -1532,6 +1550,20 @@ def list_file(self, user_id, name_pattern=""): return results, file_list + def list_file_if_index_changed( + self, + indexed_ids, + user_id, + name_pattern, + current_file_list_state, + current_file_list, + ): + if not isinstance(indexed_ids, (list, tuple, set)) or not any( + bool(item) for item in indexed_ids + ): + return current_file_list_state, current_file_list + return self.list_file(user_id, name_pattern) + def list_file_names(self, file_list_state): if file_list_state: file_names = [(item["name"], item["id"]) for item in file_list_state] diff --git a/libs/ktem/ktem/index/ui.py b/libs/ktem/ktem/index/ui.py index 6cf4f6907..98781612a 100644 --- a/libs/ktem/ktem/index/ui.py +++ b/libs/ktem/ktem/index/ui.py @@ -113,6 +113,7 @@ def _on_app_created(self): self.list_indices, inputs=[], outputs=[self.index_list], + show_progress="hidden", ) self._app.app.load( lambda: gr.update( @@ -121,6 +122,7 @@ def _on_app_created(self): ] ), outputs=[self.index_type], + show_progress="hidden", ) def on_register_events(self): @@ -184,7 +186,7 @@ def on_register_events(self): inputs=[self.selected_index_id], outputs=[self.selected_index_id], show_progress="hidden", - ).then(self.list_indices, inputs=[], outputs=[self.index_list],).success( + ).success(self.list_indices, inputs=[], outputs=[self.index_list],).success( update_current_module_atime ) self.btn_delete_no.click( @@ -211,7 +213,7 @@ def on_register_events(self): self.edit_spec, ], show_progress="hidden", - ).then( + ).success( self.list_indices, inputs=[], outputs=[self.index_list], diff --git a/libs/ktem/ktem/llms/ui.py b/libs/ktem/ktem/llms/ui.py index 73e6c1915..0600ab11c 100644 --- a/libs/ktem/ktem/llms/ui.py +++ b/libs/ktem/ktem/llms/ui.py @@ -131,10 +131,12 @@ def _on_app_created(self): self.list_llms, inputs=[], outputs=[self.llm_list], + show_progress="hidden", ) self._app.app.load( lambda: gr.update(choices=list(llms.vendors().keys())), outputs=[self.llm_choices], + show_progress="hidden", ) def on_llm_vendor_change(self, vendor): @@ -205,7 +207,7 @@ def on_register_events(self): inputs=[self.selected_llm_name], outputs=[self.selected_llm_name], show_progress="hidden", - ).then( + ).success( self.list_llms, inputs=[], outputs=[self.llm_list], @@ -230,7 +232,7 @@ def on_register_events(self): ], outputs=[self.selected_llm_name], show_progress="hidden", - ).then( + ).success( self.list_llms, inputs=[], outputs=[self.llm_list], diff --git a/libs/ktem/ktem/mcp/ui.py b/libs/ktem/ktem/mcp/ui.py index ae4dc10bd..042f43e7c 100644 --- a/libs/ktem/ktem/mcp/ui.py +++ b/libs/ktem/ktem/mcp/ui.py @@ -30,6 +30,7 @@ def __init__(self, app): def on_building_ui(self): with gr.Tab(label="View"): + self.last_fetched_mcp_name = gr.State(value="") self.mcp_list = gr.DataFrame( headers=["name", "config"], interactive=False, @@ -95,6 +96,7 @@ def _on_app_created(self): self.list_servers, inputs=[], outputs=[self.mcp_list], + show_progress="hidden", ) def on_register_events(self): @@ -140,9 +142,9 @@ def on_register_events(self): ], show_progress="hidden", ).then( - self.fetch_tools_for_view, - inputs=[self.selected_mcp_name], - outputs=[self.edit_tools_display], + self.fetch_tools_for_view_if_needed, + inputs=[self.selected_mcp_name, self.last_fetched_mcp_name], + outputs=[self.edit_tools_display, self.last_fetched_mcp_name], ) # Delete flow @@ -157,7 +159,7 @@ def on_register_events(self): inputs=[self.selected_mcp_name], outputs=[self.selected_mcp_name], show_progress="hidden", - ).then(self.list_servers, inputs=[], outputs=[self.mcp_list]) + ).success(self.list_servers, inputs=[], outputs=[self.mcp_list]) for event in self._app.get_event("onMCPServersChanged"): delete_chain = delete_chain.then(**event) self.btn_delete_no.click( @@ -179,7 +181,7 @@ def on_register_events(self): outputs=[self.edit_tools_display], show_progress="hidden", ) - .then(self.list_servers, inputs=[], outputs=[self.mcp_list]) + .success(self.list_servers, inputs=[], outputs=[self.mcp_list]) .then( self.fetch_tools_for_view, inputs=[self.selected_mcp_name], @@ -192,7 +194,12 @@ def on_register_events(self): # Close panel self.btn_close.click(lambda: "", outputs=[self.selected_mcp_name]) - # --- Handlers --- + # --- Handlers --- + + def fetch_tools_for_view_if_needed(self, selected_name, last_fetched_name): + if not selected_name or selected_name == last_fetched_name: + return gr.update(), last_fetched_name + return self.fetch_tools_for_view(selected_name), selected_name def _fetch_tools_markdown(self, config: dict) -> str: """Fetch tools from MCP server and return as formatted HTML.""" diff --git a/libs/ktem/ktem/pages/chat/__init__.py b/libs/ktem/ktem/pages/chat/__init__.py index 132832040..c6438a377 100644 --- a/libs/ktem/ktem/pages/chat/__init__.py +++ b/libs/ktem/ktem/pages/chat/__init__.py @@ -57,12 +57,26 @@ REASONING_LIMITS = 2 if KH_DEMO_MODE else 10 DEFAULT_SETTING = "(default)" INFO_PANEL_SCALES = {True: 8, False: 4} +PDFVIEW_SETUP_MARKERS = ( + "pdf-link", + "citation", + 'details class="evidence', + "details class='evidence", + "markmap", +) DEFAULT_QUESTION = ( "What is the summary of this document?" if not KH_DEMO_MODE else "What is the summary of this paper?" ) + +def _info_panel_requires_pdfview_setup(info_panel: object) -> bool: + if not isinstance(info_panel, str) or not info_panel: + return False + return any(marker in info_panel for marker in PDFVIEW_SETUP_MARKERS) + + chat_input_focus_js = """ function() { let chatInput = document.querySelector("#chat-input textarea"); @@ -115,28 +129,42 @@ ); bot_messages.forEach(message => { message.classList.remove("text_selection"); + delete message.dataset.evidenceSearchReady; }); } """ pdfview_js = """ function() { - setTimeout(fullTextSearch(), 100); - // Get all links and attach click event var links = document.getElementsByClassName("pdf-link"); + var citation_links = document.querySelectorAll("a.citation"); + var evidence_nodes = document.querySelectorAll( + "#html-info-panel details.evidence" + ); + var mindmap_el_script = document.querySelector('div.markmap script'); + + if ( + links.length === 0 && + citation_links.length === 0 && + evidence_nodes.length === 0 && + !mindmap_el_script + ) { + return [0] + } + + setTimeout(fullTextSearch, 100); + for (var i = 0; i < links.length; i++) { links[i].onclick = openModal; } // Get all citation links and attach click event - var links = document.querySelectorAll("a.citation"); - for (var i = 0; i < links.length; i++) { - links[i].onclick = scrollToCitation; + for (var i = 0; i < citation_links.length; i++) { + citation_links[i].onclick = scrollToCitation; } var markmap_div = document.querySelector("div.markmap"); - var mindmap_el_script = document.querySelector('div.markmap script'); if (mindmap_el_script) { markmap_div_html = markmap_div.outerHTML; @@ -181,7 +209,7 @@ if (markmap_div_html) { var link = document.getElementById("mindmap-export"); if (link) { - link.addEventListener('click', on_svg_export); + link.onclick = on_svg_export; } } } @@ -483,8 +511,10 @@ def on_register_events(self): show_progress="minimal", ) .then( - fn=lambda: True, - inputs=None, + fn=lambda info_panel: True + if _info_panel_requires_pdfview_setup(info_panel) + else gr.update(value=False), + inputs=[self.info_panel], outputs=[self._preview_links], js=pdfview_js, ) @@ -751,8 +781,10 @@ def on_register_events(self): js=clear_bot_message_selection_js, ) .then( - fn=lambda: True, - inputs=None, + fn=lambda info_panel: True + if _info_panel_requires_pdfview_setup(info_panel) + else gr.update(value=False), + inputs=[self.info_panel], outputs=[self._preview_links], js=pdfview_js, ) @@ -776,8 +808,10 @@ def on_register_events(self): inputs=self.state_plot_panel, outputs=self.plot_panel, ).then( - fn=lambda: True, - inputs=None, + fn=lambda info_panel: True + if _info_panel_requires_pdfview_setup(info_panel) + else gr.update(value=False), + inputs=[self.info_panel], outputs=[self._preview_links], js=pdfview_js, ) diff --git a/libs/ktem/ktem/pages/help.py b/libs/ktem/ktem/pages/help.py index 2ecdf7eb7..ca2ab77ac 100644 --- a/libs/ktem/ktem/pages/help.py +++ b/libs/ktem/ktem/pages/help.py @@ -1,4 +1,3 @@ -from importlib.metadata import version from pathlib import Path import gradio as gr @@ -31,6 +30,10 @@ def download_changelogs(release_url: str) -> str: return "" +def get_changelog_cache_path(changelogs_cache_dir: Path, app_version: str) -> Path: + return changelogs_cache_dir / f"{app_version}.md" + + class HelpPage: def __init__( self, @@ -94,9 +97,12 @@ def __init__( if self.app_version: # try retrieve from cache changelogs = "" + changelog_cache_path = get_changelog_cache_path( + self.changelogs_cache_dir, self.app_version + ) - if (self.changelogs_cache_dir / f"{version}.md").exists(): - with open(self.changelogs_cache_dir / f"{version}.md", "r") as fi: + if changelog_cache_path.exists(): + with open(changelog_cache_path, "r") as fi: changelogs = fi.read() else: release_url_base = ( @@ -109,9 +115,7 @@ def __init__( # cache the changelogs if not self.changelogs_cache_dir.exists(): self.changelogs_cache_dir.mkdir(parents=True, exist_ok=True) - with open( - self.changelogs_cache_dir / f"{self.app_version}.md", "w" - ) as fi: + with open(changelog_cache_path, "w") as fi: fi.write(changelogs) if changelogs: diff --git a/libs/ktem/ktem/pages/resources/user.py b/libs/ktem/ktem/pages/resources/user.py index bb5cb56a2..9ab429ec4 100644 --- a/libs/ktem/ktem/pages/resources/user.py +++ b/libs/ktem/ktem/pages/resources/user.py @@ -183,7 +183,7 @@ def on_register_events(self): self.create_user, inputs=[self.usn_new, self.pwd_new, self.pwd_cnf_new], outputs=[self.usn_new, self.pwd_new, self.pwd_cnf_new], - ).then( + ).success( self.list_users, inputs=self._app.user_id, outputs=[self.state_user_list, self.user_list], @@ -223,7 +223,7 @@ def on_register_events(self): inputs=[self._app.user_id, self.selected_user_id], outputs=[self.selected_user_id], show_progress="hidden", - ).then( + ).success( self.list_users, inputs=self._app.user_id, outputs=[self.state_user_list, self.user_list], @@ -249,7 +249,7 @@ def on_register_events(self): ], outputs=[self.pwd_edit, self.pwd_cnf_edit], show_progress="hidden", - ).then( + ).success( self.list_users, inputs=self._app.user_id, outputs=[self.state_user_list, self.user_list], @@ -266,6 +266,7 @@ def on_subscribe_public_events(self): "fn": self.list_users, "inputs": [self._app.user_id], "outputs": [self.state_user_list, self.user_list], + "show_progress": "hidden", }, ) self._app.subscribe_event( @@ -280,6 +281,7 @@ def on_subscribe_public_events(self): self.user_list, self.selected_user_id, ], + "show_progress": "hidden", }, ) diff --git a/libs/ktem/ktem/pages/settings.py b/libs/ktem/ktem/pages/settings.py index b60cec5bc..3b6e99d60 100644 --- a/libs/ktem/ktem/pages/settings.py +++ b/libs/ktem/ktem/pages/settings.py @@ -34,6 +34,21 @@ } +def build_choice_updates( + count: int, + default_name: str | None, + option_names, + random_label: str, +): + if default_name: + choices = [(f"{default_name} (default)", "")] + else: + choices = [(random_label, "")] + choices += [(name, name) for name in option_names] + + return [gr.update(choices=list(choices)) for _ in range(count)] + + def render_setting_item(setting_item, value): """Render the setting component into corresponding Gradio UI component""" kwargs = { @@ -453,34 +468,34 @@ def _on_app_created(self): def update_llms(): from ktem.llms.manager import llms - if llms._default: - llm_choices = [(f"{llms._default} (default)", "")] - else: - llm_choices = [("(random)", "")] - llm_choices += [(_, _) for _ in llms.options().keys()] - return gr.update(choices=llm_choices) + return build_choice_updates( + count=len(self._llms), + default_name=llms._default, + option_names=llms.options().keys(), + random_label="(random)", + ) def update_embeddings(): from ktem.embeddings.manager import embedding_models_manager - if embedding_models_manager._default: - emb_choices = [(f"{embedding_models_manager._default} (default)", "")] - else: - emb_choices = [("(random)", "")] - emb_choices += [(_, _) for _ in embedding_models_manager.options().keys()] - return gr.update(choices=emb_choices) + return build_choice_updates( + count=len(self._embeddings), + default_name=embedding_models_manager._default, + option_names=embedding_models_manager.options().keys(), + random_label="(random)", + ) - for llm in self._llms: + if self._llms: self._app.app.load( update_llms, inputs=[], - outputs=[llm], + outputs=self._llms, show_progress="hidden", ) - for emb in self._embeddings: + if self._embeddings: self._app.app.load( update_embeddings, inputs=[], - outputs=[emb], + outputs=self._embeddings, show_progress="hidden", ) diff --git a/libs/ktem/ktem/rerankings/ui.py b/libs/ktem/ktem/rerankings/ui.py index 5e2c78b6c..da0c52359 100644 --- a/libs/ktem/ktem/rerankings/ui.py +++ b/libs/ktem/ktem/rerankings/ui.py @@ -134,10 +134,12 @@ def _on_app_created(self): self.list_rerankings, inputs=[], outputs=[self.rerank_list], + show_progress="hidden", ) self._app.app.load( lambda: gr.update(choices=list(reranking_models_manager.vendors().keys())), outputs=[self.rerank_choices], + show_progress="hidden", ) def on_rerank_vendor_change(self, vendor): @@ -208,7 +210,7 @@ def on_register_events(self): inputs=[self.selected_rerank_name], outputs=[self.selected_rerank_name], show_progress="hidden", - ).then( + ).success( self.list_rerankings, inputs=[], outputs=[self.rerank_list], @@ -233,7 +235,7 @@ def on_register_events(self): ], outputs=[self.selected_rerank_name], show_progress="hidden", - ).then( + ).success( self.list_rerankings, inputs=[], outputs=[self.rerank_list],