diff --git a/flowsettings.py b/flowsettings.py index b8d4c3555..e248fac94 100644 --- a/flowsettings.py +++ b/flowsettings.py @@ -26,6 +26,7 @@ KH_ENABLE_FIRST_SETUP = True KH_DEMO_MODE = config("KH_DEMO_MODE", default=False, cast=bool) +KH_OLLAMA_URL = config("KH_OLLAMA_URL", default="http://localhost:11434/v1/") # App can be ran from anywhere and it's not trivial to decide where to store app data. # So let's use the same directory as the flowsetting.py file. @@ -162,7 +163,7 @@ KH_LLMS["ollama"] = { "spec": { "__type__": "kotaemon.llms.ChatOpenAI", - "base_url": "http://localhost:11434/v1/", + "base_url": KH_OLLAMA_URL, "model": config("LOCAL_MODEL", default="llama3.1:8b"), "api_key": "ollama", }, @@ -171,7 +172,7 @@ KH_EMBEDDINGS["ollama"] = { "spec": { "__type__": "kotaemon.embeddings.OpenAIEmbeddings", - "base_url": "http://localhost:11434/v1/", + "base_url": KH_OLLAMA_URL, "model": config("LOCAL_MODEL_EMBEDDINGS", default="nomic-embed-text"), "api_key": "ollama", }, @@ -195,11 +196,11 @@ }, "default": False, } -KH_LLMS["gemini"] = { +KH_LLMS["google"] = { "spec": { "__type__": "kotaemon.llms.chats.LCGeminiChat", - "model_name": "gemini-1.5-pro", - "api_key": "your-key", + "model_name": "gemini-1.5-flash", + "api_key": config("GOOGLE_API_KEY", default="your-key"), }, "default": False, } @@ -231,6 +232,13 @@ }, "default": False, } +KH_EMBEDDINGS["google"] = { + "spec": { + "__type__": "kotaemon.embeddings.LCGoogleEmbeddings", + "model": "models/text-embedding-004", + "google_api_key": config("GOOGLE_API_KEY", default="your-key"), + } +} # KH_EMBEDDINGS["huggingface"] = { # "spec": { # "__type__": "kotaemon.embeddings.LCHuggingFaceEmbeddings", @@ -303,7 +311,8 @@ GRAPHRAG_INDICES = [ { - "name": graph_type.split(".")[-1].replace("Index", ""), # get last name + "name": graph_type.split(".")[-1].replace("Index", "") + + " Collection", # get last name "config": { "supported_file_types": ( ".png, .jpeg, .jpg, .tiff, .tif, .pdf, .xls, .xlsx, .doc, .docx, " @@ -318,7 +327,7 @@ KH_INDICES = [ { - "name": "File", + "name": "File Collection", "config": { "supported_file_types": ( ".png, .jpeg, .jpg, .tiff, .tif, .pdf, .xls, .xlsx, .doc, .docx, " diff --git a/libs/kotaemon/kotaemon/embeddings/__init__.py b/libs/kotaemon/kotaemon/embeddings/__init__.py index 92b3d1f4b..0ff777428 100644 --- a/libs/kotaemon/kotaemon/embeddings/__init__.py +++ b/libs/kotaemon/kotaemon/embeddings/__init__.py @@ -4,6 +4,7 @@ from .langchain_based import ( LCAzureOpenAIEmbeddings, LCCohereEmbeddings, + LCGoogleEmbeddings, LCHuggingFaceEmbeddings, LCOpenAIEmbeddings, ) @@ -18,6 +19,7 @@ "LCAzureOpenAIEmbeddings", "LCCohereEmbeddings", "LCHuggingFaceEmbeddings", + "LCGoogleEmbeddings", "OpenAIEmbeddings", "AzureOpenAIEmbeddings", "FastEmbedEmbeddings", diff --git a/libs/kotaemon/kotaemon/embeddings/langchain_based.py b/libs/kotaemon/kotaemon/embeddings/langchain_based.py index 03ff9c670..9e8422a04 100644 --- a/libs/kotaemon/kotaemon/embeddings/langchain_based.py +++ b/libs/kotaemon/kotaemon/embeddings/langchain_based.py @@ -219,3 +219,38 @@ def _get_lc_class(self): from langchain.embeddings import HuggingFaceBgeEmbeddings return HuggingFaceBgeEmbeddings + + +class LCGoogleEmbeddings(LCEmbeddingMixin, BaseEmbeddings): + """Wrapper around Langchain's Google GenAI embedding, focusing on key parameters""" + + google_api_key: str = Param( + help="API key (https://aistudio.google.com/app/apikey)", + default=None, + required=True, + ) + model: str = Param( + help="Model name to use (https://ai.google.dev/gemini-api/docs/models/gemini#text-embedding-and-embedding)", # noqa + default="models/text-embedding-004", + required=True, + ) + + def __init__( + self, + model: str = "models/text-embedding-004", + google_api_key: Optional[str] = None, + **params, + ): + super().__init__( + model=model, + google_api_key=google_api_key, + **params, + ) + + def _get_lc_class(self): + try: + from langchain_google_genai import GoogleGenerativeAIEmbeddings + except ImportError: + raise ImportError("Please install langchain-google-genai") + + return GoogleGenerativeAIEmbeddings diff --git a/libs/ktem/ktem/assets/css/main.css b/libs/ktem/ktem/assets/css/main.css index dba11efe9..6c2e87cf4 100644 --- a/libs/ktem/ktem/assets/css/main.css +++ b/libs/ktem/ktem/assets/css/main.css @@ -97,7 +97,7 @@ button.selected { #chat-info-panel { max-height: var(--main-area-height) !important; overflow: auto !important; - transition: all 0.5s; + transition: all 0.4s; } body.dark #chat-info-panel figure>img{ @@ -109,12 +109,12 @@ body.dark #chat-info-panel figure>img{ flex-wrap: unset; overflow-y: scroll !important; position: sticky; - min-width: min(305px, 100%) !important; column-gap: 2px !important; scrollbar-width: none; /* Firefox */ -ms-overflow-style: none; /* Internet Explorer 10+ */ + transition: all 0.3s; } #conv-settings-panel::-webkit-scrollbar { @@ -204,6 +204,13 @@ mark { right: 15px; } +#chat-expand-button { + position: absolute; + top: 6px; + right: -10px; + z-index: 10; +} + #use-mindmap-checkbox { position: absolute; width: 110px; diff --git a/libs/ktem/ktem/assets/icons/expand.svg b/libs/ktem/ktem/assets/icons/expand.svg new file mode 100644 index 000000000..36e87b0e8 --- /dev/null +++ b/libs/ktem/ktem/assets/icons/expand.svg @@ -0,0 +1 @@ + diff --git a/libs/ktem/ktem/assets/js/main.js b/libs/ktem/ktem/assets/js/main.js index 30c406717..ec6ea530c 100644 --- a/libs/ktem/ktem/assets/js/main.js +++ b/libs/ktem/ktem/assets/js/main.js @@ -16,6 +16,29 @@ function run() { let chat_info_panel = document.getElementById("info-expand"); chat_info_panel.insertBefore(info_expand_button, chat_info_panel.childNodes[2]); + // move toggle-side-bar button + let chat_expand_button = document.getElementById("chat-expand-button"); + let chat_column = document.getElementById("main-chat-bot"); + let conv_column = document.getElementById("conv-settings-panel"); + + let default_conv_column_min_width = "min(300px, 100%)"; + conv_column.style.minWidth = default_conv_column_min_width + + globalThis.toggleChatColumn = (() => { + /* get flex-grow value of chat_column */ + let flex_grow = conv_column.style.flexGrow; + console.log("chat col", flex_grow); + if (flex_grow == '0') { + conv_column.style.flexGrow = '1'; + conv_column.style.minWidth = default_conv_column_min_width; + } else { + conv_column.style.flexGrow = '0'; + conv_column.style.minWidth = "0px"; + } + }); + + chat_column.insertBefore(chat_expand_button, chat_column.firstChild); + // move use mind-map checkbox let mindmap_checkbox = document.getElementById("use-mindmap-checkbox"); let chat_setting_panel = document.getElementById("chat-settings-expand"); diff --git a/libs/ktem/ktem/embeddings/manager.py b/libs/ktem/ktem/embeddings/manager.py index c33d151db..1c1c47027 100644 --- a/libs/ktem/ktem/embeddings/manager.py +++ b/libs/ktem/ktem/embeddings/manager.py @@ -57,6 +57,7 @@ def load_vendors(self): AzureOpenAIEmbeddings, FastEmbedEmbeddings, LCCohereEmbeddings, + LCGoogleEmbeddings, LCHuggingFaceEmbeddings, OpenAIEmbeddings, TeiEndpointEmbeddings, @@ -68,6 +69,7 @@ def load_vendors(self): FastEmbedEmbeddings, LCCohereEmbeddings, LCHuggingFaceEmbeddings, + LCGoogleEmbeddings, TeiEndpointEmbeddings, ] diff --git a/libs/ktem/ktem/index/file/base.py b/libs/ktem/ktem/index/file/base.py index 427a3965f..d57943ba9 100644 --- a/libs/ktem/ktem/index/file/base.py +++ b/libs/ktem/ktem/index/file/base.py @@ -55,6 +55,8 @@ class BaseFileIndexIndexing(BaseComponent): FSPath = Param(help="The file storage path") user_id = Param(help="The user id") private = Param(False, help="Whether this is private index") + chunk_size = Param(help="Chunk size for this index") + chunk_overlap = Param(help="Chunk overlap for this index") def run( self, file_paths: str | Path | list[str | Path], *args, **kwargs diff --git a/libs/ktem/ktem/index/file/index.py b/libs/ktem/ktem/index/file/index.py index f202a6a8e..9092d488a 100644 --- a/libs/ktem/ktem/index/file/index.py +++ b/libs/ktem/ktem/index/file/index.py @@ -404,6 +404,25 @@ def get_admin_settings(cls): "choices": [("Yes", True), ("No", False)], "info": "If private, files will not be accessible across users.", }, + "chunk_size": { + "name": "Size of chunk (number of tokens)", + "value": 0, + "component": "number", + "info": ( + "Number of tokens of each text segment. " + "Set 0 to use developer setting." + ), + }, + "chunk_overlap": { + "name": "Number of overlapping tokens between chunks", + "value": 0, + "component": "number", + "info": ( + "Number of tokens that consecutive text segments " + "should overlap with each other. " + "Set 0 to use developer setting." + ), + }, } def get_indexing_pipeline(self, settings, user_id) -> BaseFileIndexIndexing: @@ -423,6 +442,8 @@ def get_indexing_pipeline(self, settings, user_id) -> BaseFileIndexIndexing: obj.FSPath = self._fs_path obj.user_id = user_id obj.private = self.config.get("private", False) + obj.chunk_size = self.config.get("chunk_size", 0) + obj.chunk_overlap = self.config.get("chunk_overlap", 0) return obj diff --git a/libs/ktem/ktem/index/file/pipelines.py b/libs/ktem/ktem/index/file/pipelines.py index 6b0033cc9..4d53e6538 100644 --- a/libs/ktem/ktem/index/file/pipelines.py +++ b/libs/ktem/ktem/index/file/pipelines.py @@ -729,7 +729,11 @@ def route(self, file_path: str | Path) -> IndexPipeline: Can subclass this method for a more elaborate pipeline routing strategy. """ - _, chunk_size, chunk_overlap = dev_settings() + + _, dev_chunk_size, dev_chunk_overlap = dev_settings() + + chunk_size = self.chunk_size or dev_chunk_size + chunk_overlap = self.chunk_overlap or dev_chunk_overlap # check if file_path is a URL if self.is_url(file_path): @@ -744,12 +748,14 @@ def route(self, file_path: str | Path) -> IndexPipeline: "the suitable pipeline for this file type in the settings." ) + print(f"Chunk size: {chunk_size}, chunk overlap: {chunk_overlap}") + print("Using reader", reader) pipeline: IndexPipeline = IndexPipeline( loader=reader, splitter=TokenSplitter( chunk_size=chunk_size or 1024, - chunk_overlap=chunk_overlap if chunk_overlap is not None else 256, + chunk_overlap=chunk_overlap or 256, separator="\n\n", backup_separators=["\n", ".", "\u200B"], ), diff --git a/libs/ktem/ktem/main.py b/libs/ktem/ktem/main.py index 00d23f20a..deeb415df 100644 --- a/libs/ktem/ktem/main.py +++ b/libs/ktem/ktem/main.py @@ -84,7 +84,7 @@ def ui(self): ) as self._tabs["indices-tab"]: for index in self.index_manager.indices: with gr.Tab( - f"{index.name} Collection", + index.name, elem_id=f"{index.id}-tab", ) as self._tabs[f"{index.id}-tab"]: page = index.get_index_page_ui() diff --git a/libs/ktem/ktem/pages/chat/__init__.py b/libs/ktem/ktem/pages/chat/__init__.py index 045358735..8e3dbc021 100644 --- a/libs/ktem/ktem/pages/chat/__init__.py +++ b/libs/ktem/ktem/pages/chat/__init__.py @@ -8,7 +8,7 @@ from ktem.app import BasePage from ktem.components import reasonings from ktem.db.models import Conversation, engine -from ktem.index.file.ui import File, chat_input_focus_js +from ktem.index.file.ui import File from ktem.reasoning.prompt_optimization.suggest_conversation_name import ( SuggestConvNamePipeline, ) @@ -31,6 +31,12 @@ DEFAULT_SETTING = "(default)" INFO_PANEL_SCALES = {True: 8, False: 4} +chat_input_focus_js = """ +function() { + let chatInput = document.querySelector("#chat-input textarea"); + chatInput.focus(); +} +""" pdfview_js = """ function() { @@ -126,9 +132,7 @@ def on_building_ui(self): continue index_ui.unrender() # need to rerender later within Accordion - with gr.Accordion( - label=f"{index.name} Collection", open=index_id < 1 - ): + with gr.Accordion(label=index.name, open=index_id < 1): index_ui.render() gr_index = index_ui.as_gradio_component() @@ -403,6 +407,9 @@ def on_register_events(self): inputs=self._info_panel_expanded, outputs=[self.info_column, self._info_panel_expanded], ) + self.chat_control.btn_chat_expand.click( + fn=None, inputs=None, js="function() {toggleChatColumn();}" + ) self.chat_panel.chatbot.like( fn=self.is_liked, diff --git a/libs/ktem/ktem/pages/chat/control.py b/libs/ktem/ktem/pages/chat/control.py index db48cd643..ec11ef26a 100644 --- a/libs/ktem/ktem/pages/chat/control.py +++ b/libs/ktem/ktem/pages/chat/control.py @@ -48,9 +48,17 @@ def on_building_ui(self): elem_classes=["no-background", "body-text-color"], elem_id="toggle-dark-button", ) + self.btn_chat_expand = gr.Button( + value="", + icon=f"{ASSETS_DIR}/expand.svg", + scale=1, + size="sm", + elem_classes=["no-background", "body-text-color"], + elem_id="chat-expand-button", + ) self.btn_info_expand = gr.Button( value="", - icon=f"{ASSETS_DIR}/sidebar.svg", + icon=f"{ASSETS_DIR}/expand.svg", min_width=2, scale=1, size="sm", diff --git a/libs/ktem/ktem/pages/settings.py b/libs/ktem/ktem/pages/settings.py index f60d86683..b74d641f0 100644 --- a/libs/ktem/ktem/pages/settings.py +++ b/libs/ktem/ktem/pages/settings.py @@ -272,7 +272,7 @@ def index_tab(self): id2name = {k: v.name for k, v in self._app.index_manager.info().items()} with gr.Tab("Retrieval settings", visible=self._render_index_tab): for pn, sig in self._default_settings.index.options.items(): - name = "{} Collection".format(id2name.get(pn, f"")) + name = id2name.get(pn, f"") with gr.Tab(name): for n, si in sig.settings.items(): obj = render_setting_item(si, si.value) diff --git a/libs/ktem/ktem/pages/setup.py b/libs/ktem/ktem/pages/setup.py index f7e70a118..21efa5d9a 100644 --- a/libs/ktem/ktem/pages/setup.py +++ b/libs/ktem/ktem/pages/setup.py @@ -9,7 +9,10 @@ from theflow.settings import settings as flowsettings KH_DEMO_MODE = getattr(flowsettings, "KH_DEMO_MODE", False) -DEFAULT_OLLAMA_URL = "http://localhost:11434/api" +KH_OLLAMA_URL = getattr(flowsettings, "KH_OLLAMA_URL", "http://localhost:11434/v1/") +DEFAULT_OLLAMA_URL = KH_OLLAMA_URL.replace("v1", "api") +if DEFAULT_OLLAMA_URL.endswith("/"): + DEFAULT_OLLAMA_URL = DEFAULT_OLLAMA_URL[:-1] DEMO_MESSAGE = ( @@ -55,8 +58,9 @@ def on_building_ui(self): gr.Markdown(f"# Welcome to {self._app.app_name} first setup!") self.radio_model = gr.Radio( [ - ("Cohere API (*free registration* available) - recommended", "cohere"), - ("OpenAI API (for more advance models)", "openai"), + ("Cohere API (*free registration*) - recommended", "cohere"), + ("Google API (*free registration*)", "google"), + ("OpenAI API (for GPT-based models)", "openai"), ("Local LLM (for completely *private RAG*)", "ollama"), ], label="Select your model provider", @@ -92,6 +96,18 @@ def on_building_ui(self): show_label=False, placeholder="Cohere API Key" ) + with gr.Column(visible=False) as self.google_option: + gr.Markdown( + ( + "#### Google API Key\n\n" + "(register your free API key " + "at https://aistudio.google.com/app/apikey)" + ) + ) + self.google_api_key = gr.Textbox( + show_label=False, placeholder="Google API Key" + ) + with gr.Column(visible=False) as self.ollama_option: gr.Markdown( ( @@ -119,7 +135,12 @@ def on_register_events(self): self.openai_api_key.submit, ], fn=self.update_model, - inputs=[self.cohere_api_key, self.openai_api_key, self.radio_model], + inputs=[ + self.cohere_api_key, + self.openai_api_key, + self.google_api_key, + self.radio_model, + ], outputs=[self.setup_log], show_progress="hidden", ) @@ -147,13 +168,19 @@ def on_register_events(self): fn=self.switch_options_view, inputs=[self.radio_model], show_progress="hidden", - outputs=[self.cohere_option, self.openai_option, self.ollama_option], + outputs=[ + self.cohere_option, + self.openai_option, + self.ollama_option, + self.google_option, + ], ) def update_model( self, cohere_api_key, openai_api_key, + google_api_key, radio_model_value, ): # skip if KH_DEMO_MODE @@ -221,12 +248,32 @@ def update_model( }, default=True, ) + elif radio_model_value == "google": + if google_api_key: + llms.update( + name="google", + spec={ + "__type__": "kotaemon.llms.chats.LCGeminiChat", + "model_name": "gemini-1.5-flash", + "api_key": google_api_key, + }, + default=True, + ) + embeddings.update( + name="google", + spec={ + "__type__": "kotaemon.embeddings.LCGoogleEmbeddings", + "model": "models/text-embedding-004", + "google_api_key": google_api_key, + }, + default=True, + ) elif radio_model_value == "ollama": llms.update( name="ollama", spec={ "__type__": "kotaemon.llms.ChatOpenAI", - "base_url": "http://localhost:11434/v1/", + "base_url": KH_OLLAMA_URL, "model": "llama3.1:8b", "api_key": "ollama", }, @@ -236,7 +283,7 @@ def update_model( name="ollama", spec={ "__type__": "kotaemon.embeddings.OpenAIEmbeddings", - "base_url": "http://localhost:11434/v1/", + "base_url": KH_OLLAMA_URL, "model": "nomic-embed-text", "api_key": "ollama", }, @@ -270,7 +317,7 @@ def update_model( yield log_content except Exception as e: log_content += ( - "Make sure you have download and installed Ollama correctly." + "Make sure you have download and installed Ollama correctly. " f"Got error: {str(e)}" ) yield log_content @@ -345,9 +392,9 @@ def update_default_settings(self, radio_model_value, default_settings): return default_settings def switch_options_view(self, radio_model_value): - components_visible = [gr.update(visible=False) for _ in range(3)] + components_visible = [gr.update(visible=False) for _ in range(4)] - values = ["cohere", "openai", "ollama", None] + values = ["cohere", "openai", "ollama", "google", None] assert radio_model_value in values, f"Invalid value {radio_model_value}" if radio_model_value is not None: