feat: merge develop (#123)
* Support hybrid vector retrieval * Enable figures and table reading in Azure DI * Retrieve with multi-modal * Fix mixing up table * Add txt loader * Add Anthropic Chat * Raising error when retrieving help file * Allow same filename for different people if private is True * Allow declaring extra LLM vendors * Show chunks on the File page * Allow elasticsearch to get more docs * Fix Cohere response (#86) * Fix Cohere response * Remove Adobe pdfservice from dependency kotaemon doesn't rely more pdfservice for its core functionality, and pdfservice uses very out-dated dependency that causes conflict. --------- Co-authored-by: trducng <trungduc1992@gmail.com> * Add confidence score (#87) * Save question answering data as a log file * Save the original information besides the rewritten info * Export Cohere relevance score as confidence score * Fix style check * Upgrade the confidence score appearance (#90) * Highlight the relevance score * Round relevance score. Get key from config instead of env * Cohere return all scores * Display relevance score for image * Remove columns and rows in Excel loader which contains all NaN (#91) * remove columns and rows which contains all NaN * back to multiple joiner options * Fix style --------- Co-authored-by: linhnguyen-cinnamon <cinmc0019@CINMC0019-LinhNguyen.local> Co-authored-by: trducng <trungduc1992@gmail.com> * Track retriever state * Bump llama-index version 0.10 * feat/save-azuredi-mhtml-to-markdown (#93) * feat/save-azuredi-mhtml-to-markdown * fix: replace os.path to pathlib change theflow.settings * refactor: base on pre-commit * chore: move the func of saving content markdown above removed_spans --------- Co-authored-by: jacky0218 <jacky0218@github.com> * fix: losing first chunk (#94) * fix: losing first chunk. * fix: update the method of preventing losing chunks --------- Co-authored-by: jacky0218 <jacky0218@github.com> * fix: adding the base64 image in markdown (#95) * feat: more chunk info on UI * fix: error when reindexing files * refactor: allow more information exception trace when using gpt4v * feat: add excel reader that treats each worksheet as a document * Persist loader information when indexing file * feat: allow hiding unneeded setting panels * feat: allow specific timezone when creating conversation * feat: add more confidence score (#96) * Allow a list of rerankers * Export llm reranking score instead of filter with boolean * Get logprobs from LLMs * Rename cohere reranking score * Call 2 rerankers at once * Run QA pipeline for each chunk to get qa_score * Display more relevance scores * Define another LLMScoring instead of editing the original one * Export logprobs instead of probs * Call LLMScoring * Get qa_score only in the final answer * feat: replace text length with token in file list * ui: show index name instead of id in the settings * feat(ai): restrict the vision temperature * fix(ui): remove the misleading message about non-retrieved evidences * feat(ui): show the reasoning name and description in the reasoning setting page * feat(ui): show version on the main windows * feat(ui): show default llm name in the setting page * fix(conf): append the result of doc in llm_scoring (#97) * fix: constraint maximum number of images * feat(ui): allow filter file by name in file list page * Fix exceeding token length error for OpenAI embeddings by chunking then averaging (#99) * Average embeddings in case the text exceeds max size * Add docstring * fix: Allow empty string when calling embedding * fix: update trulens LLM ranking score for retrieval confidence, improve citation (#98) * Round when displaying not by default * Add LLMTrulens reranking model * Use llmtrulensscoring in pipeline * fix: update UI display for trulen score --------- Co-authored-by: taprosoft <tadashi@cinnamon.is> * feat: add question decomposition & few-shot rewrite pipeline (#89) * Create few-shot query-rewriting. Run and display the result in info_panel * Fix style check * Put the functions to separate modules * Add zero-shot question decomposition * Fix fewshot rewriting * Add default few-shot examples * Fix decompose question * Fix importing rewriting pipelines * fix: update decompose logic in fullQA pipeline --------- Co-authored-by: taprosoft <tadashi@cinnamon.is> * fix: add encoding utf-8 when save temporal markdown in vectorIndex (#101) * fix: improve retrieval pipeline and relevant score display (#102) * fix: improve retrieval pipeline by extending first round top_k with multiplier * fix: minor fix * feat: improve UI default settings and add quick switch option for pipeline * fix: improve agent logics (#103) * fix: improve agent progres display * fix: update retrieval logic * fix: UI display * fix: less verbose debug log * feat: add warning message for low confidence * fix: LLM scoring enabled by default * fix: minor update logics * fix: hotfix image citation * feat: update docx loader for handle merged table cells + handle zip file upload (#104) * feat: update docx loader for handle merged table cells * feat: handle zip file * refactor: pre-commit * fix: escape text in download UI * feat: optimize vector store query db (#105) * feat: optimize vector store query db * feat: add file_id to chroma metadatas * feat: remove unnecessary logs and update migrate script * feat: iterate through file index * fix: remove unused code --------- Co-authored-by: taprosoft <tadashi@cinnamon.is> * fix: add openai embedidng exponential back-off * fix: update import download_loader * refactor: codespell * fix: update some default settings * fix: update installation instruction * fix: default chunk length in simple QA * feat: add share converstation feature and enable retrieval history (#108) * feat: add share converstation feature and enable retrieval history * fix: update share conversation UI --------- Co-authored-by: taprosoft <tadashi@cinnamon.is> * fix: allow exponential backoff for failed OCR call (#109) * fix: update default prompt when no retrieval is used * fix: create embedding for long image chunks * fix: add exception handling for additional table retriever * fix: clean conversation & file selection UI * fix: elastic search with empty doc_ids * feat: add thumbnail PDF reader for quick multimodal QA * feat: add thumbnail handling logic in indexing * fix: UI text update * fix: PDF thumb loader page number logic * feat: add quick indexing pipeline and update UI * feat: add conv name suggestion * fix: minor UI change * feat: citation in thread * fix: add conv name suggestion in regen * chore: add assets for usage doc * chore: update usage doc * feat: pdf viewer (#110) * feat: update pdfviewer * feat: update missing files * fix: update rendering logic of infor panel * fix: improve thumbnail retrieval logic * fix: update PDF evidence rendering logic * fix: remove pdfjs built dist * fix: reduce thumbnail evidence count * chore: update gitignore * fix: add js event on chat msg select * fix: update css for viewer * fix: add env var for PDFJS prebuilt * fix: move language setting to reasoning utils --------- Co-authored-by: phv2312 <kat87yb@gmail.com> Co-authored-by: trducng <trungduc1992@gmail.com> * feat: graph rag (#116) * fix: reload server when add/delete index * fix: rework indexing pipeline to be able to disable vectorstore and splitter if needed * feat: add graphRAG index with plot view * fix: update requirement for graphRAG and lighten unnecessary packages * feat: add knowledge network index (#118) * feat: add Knowledge Network index * fix: update reader mode setting for knet * fix: update init knet * fix: update collection name to index pipeline * fix: missing req --------- Co-authored-by: jeff52415 <jeff.yang@cinnamon.is> * fix: update info panel return for graphrag * fix: retriever setting graphrag * feat: local llm settings (#122) * feat: expose context length as reasoning setting to better fit local models * fix: update context length setting for agents * fix: rework threadpool llm call * fix: fix improve indexing logic * fix: fix improve UI * feat: add lancedb * fix: improve lancedb logic * feat: add lancedb vectorstore * fix: lighten requirement * fix: improve lanceDB vs * fix: improve UI * fix: openai retry * fix: update reqs * fix: update launch command * feat: update Dockerfile * feat: add plot history * fix: update default config * fix: remove verbose print * fix: update default setting * fix: update gradio plot return * fix: default gradio tmp * fix: improve lancedb docstore * fix: fix question decompose pipeline * feat: add multimodal reader in UI * fix: udpate docs * fix: update default settings & docker build * fix: update app startup * chore: update documentation * chore: update README * chore: update README --------- Co-authored-by: trducng <trungduc1992@gmail.com> * chore: update README * chore: update README --------- Co-authored-by: trducng <trungduc1992@gmail.com> Co-authored-by: cin-ace <ace@cinnamon.is> Co-authored-by: Linh Nguyen <70562198+linhnguyen-cinnamon@users.noreply.github.com> Co-authored-by: linhnguyen-cinnamon <cinmc0019@CINMC0019-LinhNguyen.local> Co-authored-by: cin-jacky <101088014+jacky0218@users.noreply.github.com> Co-authored-by: jacky0218 <jacky0218@github.com> Co-authored-by: kan_cin <kan@cinnamon.is> Co-authored-by: phv2312 <kat87yb@gmail.com> Co-authored-by: jeff52415 <jeff.yang@cinnamon.is>
This commit is contained in:
committed by
GitHub
parent
86d60e1649
commit
2570e11501
@@ -1,15 +1,25 @@
|
||||
import asyncio
|
||||
import csv
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import gradio as gr
|
||||
from filelock import FileLock
|
||||
from ktem.app import BasePage
|
||||
from ktem.components import reasonings
|
||||
from ktem.db.models import Conversation, engine
|
||||
from ktem.index.file.ui import File
|
||||
from ktem.reasoning.prompt_optimization.suggest_conversation_name import (
|
||||
SuggestConvNamePipeline,
|
||||
)
|
||||
from plotly.io import from_json
|
||||
from sqlmodel import Session, select
|
||||
from theflow.settings import settings as flowsettings
|
||||
|
||||
from kotaemon.base import Document
|
||||
from kotaemon.indices.ingests.files import KH_DEFAULT_FILE_EXTRACTORS
|
||||
|
||||
from .chat_panel import ChatPanel
|
||||
from .chat_suggestion import ChatSuggestion
|
||||
@@ -17,23 +27,49 @@ from .common import STATE
|
||||
from .control import ConversationControl
|
||||
from .report import ReportIssue
|
||||
|
||||
DEFAULT_SETTING = "(default)"
|
||||
INFO_PANEL_SCALES = {True: 8, False: 4}
|
||||
|
||||
|
||||
pdfview_js = """
|
||||
function() {
|
||||
// Get all links and attach click event
|
||||
var links = document.getElementsByClassName("pdf-link");
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
links[i].onclick = openModal;
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class ChatPage(BasePage):
|
||||
def __init__(self, app):
|
||||
self._app = app
|
||||
self._indices_input = []
|
||||
|
||||
self.on_building_ui()
|
||||
self._reasoning_type = gr.State(value=None)
|
||||
self._llm_type = gr.State(value=None)
|
||||
self._conversation_renamed = gr.State(value=False)
|
||||
self.info_panel_expanded = gr.State(value=True)
|
||||
|
||||
def on_building_ui(self):
|
||||
with gr.Row():
|
||||
self.chat_state = gr.State(STATE)
|
||||
with gr.Column(scale=1, elem_id="conv-settings-panel"):
|
||||
self.state_chat = gr.State(STATE)
|
||||
self.state_retrieval_history = gr.State([])
|
||||
self.state_chat_history = gr.State([])
|
||||
self.state_plot_history = gr.State([])
|
||||
self.state_settings = gr.State({})
|
||||
self.state_info_panel = gr.State("")
|
||||
self.state_plot_panel = gr.State(None)
|
||||
|
||||
with gr.Column(scale=1, elem_id="conv-settings-panel") as self.conv_column:
|
||||
self.chat_control = ConversationControl(self._app)
|
||||
|
||||
if getattr(flowsettings, "KH_FEATURE_CHAT_SUGGESTION", False):
|
||||
self.chat_suggestion = ChatSuggestion(self._app)
|
||||
|
||||
for index in self._app.index_manager.indices:
|
||||
for index_id, index in enumerate(self._app.index_manager.indices):
|
||||
index.selector = None
|
||||
index_ui = index.get_selector_component_ui()
|
||||
if not index_ui:
|
||||
@@ -41,7 +77,9 @@ class ChatPage(BasePage):
|
||||
continue
|
||||
|
||||
index_ui.unrender() # need to rerender later within Accordion
|
||||
with gr.Accordion(label=f"{index.name} Index", open=True):
|
||||
with gr.Accordion(
|
||||
label=f"{index.name} Collection", open=index_id < 1
|
||||
):
|
||||
index_ui.render()
|
||||
gr_index = index_ui.as_gradio_component()
|
||||
if gr_index:
|
||||
@@ -60,14 +98,66 @@ class ChatPage(BasePage):
|
||||
self._indices_input.append(gr_index)
|
||||
setattr(self, f"_index_{index.id}", index_ui)
|
||||
|
||||
if len(self._app.index_manager.indices) > 0:
|
||||
with gr.Accordion(label="Quick Upload") as _:
|
||||
self.quick_file_upload = File(
|
||||
file_types=list(KH_DEFAULT_FILE_EXTRACTORS.keys()),
|
||||
file_count="multiple",
|
||||
container=True,
|
||||
show_label=False,
|
||||
)
|
||||
self.quick_file_upload_status = gr.Markdown()
|
||||
|
||||
self.report_issue = ReportIssue(self._app)
|
||||
|
||||
with gr.Column(scale=6, elem_id="chat-area"):
|
||||
self.chat_panel = ChatPanel(self._app)
|
||||
|
||||
with gr.Column(scale=3, elem_id="chat-info-panel"):
|
||||
with gr.Row():
|
||||
with gr.Accordion(label="Chat settings", open=False):
|
||||
# a quick switch for reasoning type option
|
||||
with gr.Row():
|
||||
gr.HTML("Reasoning method")
|
||||
gr.HTML("Model")
|
||||
|
||||
with gr.Row():
|
||||
reasoning_type_values = [
|
||||
(DEFAULT_SETTING, DEFAULT_SETTING)
|
||||
] + self._app.default_settings.reasoning.settings[
|
||||
"use"
|
||||
].choices
|
||||
self.reasoning_types = gr.Dropdown(
|
||||
choices=reasoning_type_values,
|
||||
value=DEFAULT_SETTING,
|
||||
container=False,
|
||||
show_label=False,
|
||||
)
|
||||
self.model_types = gr.Dropdown(
|
||||
choices=self._app.default_settings.reasoning.options[
|
||||
"simple"
|
||||
]
|
||||
.settings["llm"]
|
||||
.choices,
|
||||
value="",
|
||||
container=False,
|
||||
show_label=False,
|
||||
)
|
||||
|
||||
with gr.Column(
|
||||
scale=INFO_PANEL_SCALES[False], elem_id="chat-info-panel"
|
||||
) as self.info_column:
|
||||
with gr.Accordion(label="Information panel", open=True):
|
||||
self.info_panel = gr.HTML()
|
||||
self.modal = gr.HTML("<div id='pdf-modal'></div>")
|
||||
self.plot_panel = gr.Plot(visible=False)
|
||||
self.info_panel = gr.HTML(elem_id="html-info-panel")
|
||||
|
||||
def _json_to_plot(self, json_dict: dict | None):
|
||||
if json_dict:
|
||||
plot = from_json(json_dict)
|
||||
plot = gr.update(visible=True, value=plot)
|
||||
else:
|
||||
plot = gr.update(visible=False)
|
||||
return plot
|
||||
|
||||
def on_register_events(self):
|
||||
gr.on(
|
||||
@@ -98,27 +188,75 @@ class ChatPage(BasePage):
|
||||
self.chat_control.conversation_id,
|
||||
self.chat_panel.chatbot,
|
||||
self._app.settings_state,
|
||||
self.chat_state,
|
||||
self._reasoning_type,
|
||||
self._llm_type,
|
||||
self.state_chat,
|
||||
self._app.user_id,
|
||||
]
|
||||
+ self._indices_input,
|
||||
outputs=[
|
||||
self.chat_panel.chatbot,
|
||||
self.info_panel,
|
||||
self.chat_state,
|
||||
self.plot_panel,
|
||||
self.state_plot_panel,
|
||||
self.state_chat,
|
||||
],
|
||||
concurrency_limit=20,
|
||||
show_progress="minimal",
|
||||
).success(
|
||||
fn=self.backup_original_info,
|
||||
inputs=[
|
||||
self.chat_panel.chatbot,
|
||||
self._app.settings_state,
|
||||
self.info_panel,
|
||||
self.state_chat_history,
|
||||
],
|
||||
outputs=[
|
||||
self.state_chat_history,
|
||||
self.state_settings,
|
||||
self.state_info_panel,
|
||||
],
|
||||
).then(
|
||||
fn=self.update_data_source,
|
||||
fn=self.persist_data_source,
|
||||
inputs=[
|
||||
self.chat_control.conversation_id,
|
||||
self._app.user_id,
|
||||
self.info_panel,
|
||||
self.state_plot_panel,
|
||||
self.state_retrieval_history,
|
||||
self.state_plot_history,
|
||||
self.chat_panel.chatbot,
|
||||
self.chat_state,
|
||||
self.state_chat,
|
||||
]
|
||||
+ self._indices_input,
|
||||
outputs=None,
|
||||
outputs=[
|
||||
self.state_retrieval_history,
|
||||
self.state_plot_history,
|
||||
],
|
||||
concurrency_limit=20,
|
||||
).success(
|
||||
fn=self.check_and_suggest_name_conv,
|
||||
inputs=self.chat_panel.chatbot,
|
||||
outputs=[
|
||||
self.chat_control.conversation_rn,
|
||||
self._conversation_renamed,
|
||||
],
|
||||
).success(
|
||||
self.chat_control.rename_conv,
|
||||
inputs=[
|
||||
self.chat_control.conversation_id,
|
||||
self.chat_control.conversation_rn,
|
||||
self._conversation_renamed,
|
||||
self._app.user_id,
|
||||
],
|
||||
outputs=[
|
||||
self.chat_control.conversation,
|
||||
self.chat_control.conversation,
|
||||
self.chat_control.conversation_rn,
|
||||
],
|
||||
show_progress="hidden",
|
||||
).then(
|
||||
fn=None, inputs=None, outputs=None, js=pdfview_js
|
||||
)
|
||||
|
||||
self.chat_panel.regen_btn.click(
|
||||
@@ -127,33 +265,90 @@ class ChatPage(BasePage):
|
||||
self.chat_control.conversation_id,
|
||||
self.chat_panel.chatbot,
|
||||
self._app.settings_state,
|
||||
self.chat_state,
|
||||
self._reasoning_type,
|
||||
self._llm_type,
|
||||
self.state_chat,
|
||||
self._app.user_id,
|
||||
]
|
||||
+ self._indices_input,
|
||||
outputs=[
|
||||
self.chat_panel.chatbot,
|
||||
self.info_panel,
|
||||
self.chat_state,
|
||||
self.plot_panel,
|
||||
self.state_plot_panel,
|
||||
self.state_chat,
|
||||
],
|
||||
concurrency_limit=20,
|
||||
show_progress="minimal",
|
||||
).then(
|
||||
fn=self.update_data_source,
|
||||
fn=self.persist_data_source,
|
||||
inputs=[
|
||||
self.chat_control.conversation_id,
|
||||
self._app.user_id,
|
||||
self.info_panel,
|
||||
self.state_plot_panel,
|
||||
self.state_retrieval_history,
|
||||
self.state_plot_history,
|
||||
self.chat_panel.chatbot,
|
||||
self.chat_state,
|
||||
self.state_chat,
|
||||
]
|
||||
+ self._indices_input,
|
||||
outputs=None,
|
||||
outputs=[
|
||||
self.state_retrieval_history,
|
||||
self.state_plot_history,
|
||||
],
|
||||
concurrency_limit=20,
|
||||
).success(
|
||||
fn=self.check_and_suggest_name_conv,
|
||||
inputs=self.chat_panel.chatbot,
|
||||
outputs=[
|
||||
self.chat_control.conversation_rn,
|
||||
self._conversation_renamed,
|
||||
],
|
||||
).success(
|
||||
self.chat_control.rename_conv,
|
||||
inputs=[
|
||||
self.chat_control.conversation_id,
|
||||
self.chat_control.conversation_rn,
|
||||
self._conversation_renamed,
|
||||
self._app.user_id,
|
||||
],
|
||||
outputs=[
|
||||
self.chat_control.conversation,
|
||||
self.chat_control.conversation,
|
||||
self.chat_control.conversation_rn,
|
||||
],
|
||||
show_progress="hidden",
|
||||
).then(
|
||||
fn=None, inputs=None, outputs=None, js=pdfview_js
|
||||
)
|
||||
|
||||
self.chat_control.btn_info_expand.click(
|
||||
fn=lambda is_expanded: (
|
||||
gr.update(scale=INFO_PANEL_SCALES[is_expanded]),
|
||||
not is_expanded,
|
||||
),
|
||||
inputs=self.info_panel_expanded,
|
||||
outputs=[self.info_column, self.info_panel_expanded],
|
||||
)
|
||||
|
||||
self.chat_panel.chatbot.like(
|
||||
fn=self.is_liked,
|
||||
inputs=[self.chat_control.conversation_id],
|
||||
outputs=None,
|
||||
).success(
|
||||
self.save_log,
|
||||
inputs=[
|
||||
self.chat_control.conversation_id,
|
||||
self.chat_panel.chatbot,
|
||||
self._app.settings_state,
|
||||
self.info_panel,
|
||||
self.state_chat_history,
|
||||
self.state_settings,
|
||||
self.state_info_panel,
|
||||
gr.State(getattr(flowsettings, "KH_APP_DATA_DIR", "logs")),
|
||||
],
|
||||
outputs=None,
|
||||
)
|
||||
|
||||
self.chat_control.btn_new.click(
|
||||
@@ -163,17 +358,25 @@ class ChatPage(BasePage):
|
||||
show_progress="hidden",
|
||||
).then(
|
||||
self.chat_control.select_conv,
|
||||
inputs=[self.chat_control.conversation],
|
||||
inputs=[self.chat_control.conversation, self._app.user_id],
|
||||
outputs=[
|
||||
self.chat_control.conversation_id,
|
||||
self.chat_control.conversation,
|
||||
self.chat_control.conversation_rn,
|
||||
self.chat_panel.chatbot,
|
||||
self.info_panel,
|
||||
self.chat_state,
|
||||
self.state_plot_panel,
|
||||
self.state_retrieval_history,
|
||||
self.state_plot_history,
|
||||
self.chat_control.cb_is_public,
|
||||
self.state_chat,
|
||||
]
|
||||
+ self._indices_input,
|
||||
show_progress="hidden",
|
||||
).then(
|
||||
fn=self._json_to_plot,
|
||||
inputs=self.state_plot_panel,
|
||||
outputs=self.plot_panel,
|
||||
)
|
||||
|
||||
self.chat_control.btn_del.click(
|
||||
@@ -188,17 +391,25 @@ class ChatPage(BasePage):
|
||||
show_progress="hidden",
|
||||
).then(
|
||||
self.chat_control.select_conv,
|
||||
inputs=[self.chat_control.conversation],
|
||||
inputs=[self.chat_control.conversation, self._app.user_id],
|
||||
outputs=[
|
||||
self.chat_control.conversation_id,
|
||||
self.chat_control.conversation,
|
||||
self.chat_control.conversation_rn,
|
||||
self.chat_panel.chatbot,
|
||||
self.info_panel,
|
||||
self.chat_state,
|
||||
self.state_plot_panel,
|
||||
self.state_retrieval_history,
|
||||
self.state_plot_history,
|
||||
self.chat_control.cb_is_public,
|
||||
self.state_chat,
|
||||
]
|
||||
+ self._indices_input,
|
||||
show_progress="hidden",
|
||||
).then(
|
||||
fn=self._json_to_plot,
|
||||
inputs=self.state_plot_panel,
|
||||
outputs=self.plot_panel,
|
||||
).then(
|
||||
lambda: self.toggle_delete(""),
|
||||
outputs=[self.chat_control._new_delete, self.chat_control._delete_confirm],
|
||||
@@ -207,33 +418,80 @@ class ChatPage(BasePage):
|
||||
lambda: self.toggle_delete(""),
|
||||
outputs=[self.chat_control._new_delete, self.chat_control._delete_confirm],
|
||||
)
|
||||
self.chat_control.conversation_rn_btn.click(
|
||||
self.chat_control.btn_conversation_rn.click(
|
||||
lambda: gr.update(visible=True),
|
||||
outputs=[
|
||||
self.chat_control.conversation_rn,
|
||||
],
|
||||
)
|
||||
self.chat_control.conversation_rn.submit(
|
||||
self.chat_control.rename_conv,
|
||||
inputs=[
|
||||
self.chat_control.conversation_id,
|
||||
self.chat_control.conversation_rn,
|
||||
gr.State(value=True),
|
||||
self._app.user_id,
|
||||
],
|
||||
outputs=[self.chat_control.conversation, self.chat_control.conversation],
|
||||
outputs=[
|
||||
self.chat_control.conversation,
|
||||
self.chat_control.conversation,
|
||||
self.chat_control.conversation_rn,
|
||||
],
|
||||
show_progress="hidden",
|
||||
)
|
||||
|
||||
self.chat_control.conversation.select(
|
||||
self.chat_control.select_conv,
|
||||
inputs=[self.chat_control.conversation],
|
||||
inputs=[self.chat_control.conversation, self._app.user_id],
|
||||
outputs=[
|
||||
self.chat_control.conversation_id,
|
||||
self.chat_control.conversation,
|
||||
self.chat_control.conversation_rn,
|
||||
self.chat_panel.chatbot,
|
||||
self.info_panel,
|
||||
self.chat_state,
|
||||
self.state_plot_panel,
|
||||
self.state_retrieval_history,
|
||||
self.state_plot_history,
|
||||
self.chat_control.cb_is_public,
|
||||
self.state_chat,
|
||||
]
|
||||
+ self._indices_input,
|
||||
show_progress="hidden",
|
||||
).then(
|
||||
fn=self._json_to_plot,
|
||||
inputs=self.state_plot_panel,
|
||||
outputs=self.plot_panel,
|
||||
).then(
|
||||
lambda: self.toggle_delete(""),
|
||||
outputs=[self.chat_control._new_delete, self.chat_control._delete_confirm],
|
||||
).then(
|
||||
fn=None, inputs=None, outputs=None, js=pdfview_js
|
||||
)
|
||||
|
||||
# evidence display on message selection
|
||||
self.chat_panel.chatbot.select(
|
||||
self.message_selected,
|
||||
inputs=[
|
||||
self.state_retrieval_history,
|
||||
self.state_plot_history,
|
||||
],
|
||||
outputs=[
|
||||
self.info_panel,
|
||||
self.state_plot_panel,
|
||||
],
|
||||
).then(
|
||||
fn=self._json_to_plot,
|
||||
inputs=self.state_plot_panel,
|
||||
outputs=self.plot_panel,
|
||||
).then(
|
||||
fn=None, inputs=None, outputs=None, js=pdfview_js
|
||||
)
|
||||
|
||||
self.chat_control.cb_is_public.change(
|
||||
self.on_set_public_conversation,
|
||||
inputs=[self.chat_control.cb_is_public, self.chat_control.conversation],
|
||||
outputs=None,
|
||||
show_progress="hidden",
|
||||
)
|
||||
|
||||
self.report_issue.report_btn.click(
|
||||
@@ -247,11 +505,26 @@ class ChatPage(BasePage):
|
||||
self._app.settings_state,
|
||||
self._app.user_id,
|
||||
self.info_panel,
|
||||
self.chat_state,
|
||||
self.state_chat,
|
||||
]
|
||||
+ self._indices_input,
|
||||
outputs=None,
|
||||
)
|
||||
self.reasoning_types.change(
|
||||
self.reasoning_changed,
|
||||
inputs=[self.reasoning_types],
|
||||
outputs=[self._reasoning_type],
|
||||
)
|
||||
self.model_types.change(
|
||||
lambda x: x,
|
||||
inputs=[self.model_types],
|
||||
outputs=[self._llm_type],
|
||||
)
|
||||
self.chat_control.conversation_id.change(
|
||||
lambda: gr.update(visible=False),
|
||||
outputs=self.plot_panel,
|
||||
)
|
||||
|
||||
if getattr(flowsettings, "KH_FEATURE_CHAT_SUGGESTION", False):
|
||||
self.chat_suggestion.example.select(
|
||||
self.chat_suggestion.select_example,
|
||||
@@ -291,6 +564,28 @@ class ChatPage(BasePage):
|
||||
else:
|
||||
return gr.update(visible=True), gr.update(visible=False)
|
||||
|
||||
def on_set_public_conversation(self, is_public, convo_id):
|
||||
if not convo_id:
|
||||
gr.Warning("No conversation selected")
|
||||
return
|
||||
|
||||
with Session(engine) as session:
|
||||
statement = select(Conversation).where(Conversation.id == convo_id)
|
||||
|
||||
result = session.exec(statement).one()
|
||||
name = result.name
|
||||
|
||||
if result.is_public != is_public:
|
||||
# Only trigger updating when user
|
||||
# select different value from the current
|
||||
result.is_public = is_public
|
||||
session.add(result)
|
||||
session.commit()
|
||||
|
||||
gr.Info(
|
||||
f"Conversation: {name} is {'public' if is_public else 'private'}."
|
||||
)
|
||||
|
||||
def on_subscribe_public_events(self):
|
||||
if self._app.f_user_management:
|
||||
self._app.subscribe_event(
|
||||
@@ -306,25 +601,53 @@ class ChatPage(BasePage):
|
||||
self._app.subscribe_event(
|
||||
name="onSignOut",
|
||||
definition={
|
||||
"fn": lambda: self.chat_control.select_conv(""),
|
||||
"fn": lambda: self.chat_control.select_conv("", None),
|
||||
"outputs": [
|
||||
self.chat_control.conversation_id,
|
||||
self.chat_control.conversation,
|
||||
self.chat_control.conversation_rn,
|
||||
self.chat_panel.chatbot,
|
||||
self.info_panel,
|
||||
self.state_plot_panel,
|
||||
self.state_retrieval_history,
|
||||
self.state_plot_history,
|
||||
self.chat_control.cb_is_public,
|
||||
]
|
||||
+ self._indices_input,
|
||||
"show_progress": "hidden",
|
||||
},
|
||||
)
|
||||
|
||||
def update_data_source(self, convo_id, messages, state, *selecteds):
|
||||
def persist_data_source(
|
||||
self,
|
||||
convo_id,
|
||||
user_id,
|
||||
retrieval_msg,
|
||||
plot_data,
|
||||
retrival_history,
|
||||
plot_history,
|
||||
messages,
|
||||
state,
|
||||
*selecteds,
|
||||
):
|
||||
"""Update the data source"""
|
||||
if not convo_id:
|
||||
gr.Warning("No conversation selected")
|
||||
return
|
||||
|
||||
# if not regen, then append the new message
|
||||
if not state["app"].get("regen", False):
|
||||
retrival_history = retrival_history + [retrieval_msg]
|
||||
plot_history = plot_history + [plot_data]
|
||||
else:
|
||||
if retrival_history:
|
||||
print("Updating retrieval history (regen=True)")
|
||||
retrival_history[-1] = retrieval_msg
|
||||
plot_history[-1] = plot_data
|
||||
|
||||
# reset regen state
|
||||
state["app"]["regen"] = False
|
||||
|
||||
selecteds_ = {}
|
||||
for index in self._app.index_manager.indices:
|
||||
if index.selector is None:
|
||||
@@ -339,15 +662,29 @@ class ChatPage(BasePage):
|
||||
result = session.exec(statement).one()
|
||||
|
||||
data_source = result.data_source
|
||||
old_selecteds = data_source.get("selected", {})
|
||||
is_owner = result.user == user_id
|
||||
|
||||
# Write down to db
|
||||
result.data_source = {
|
||||
"selected": selecteds_,
|
||||
"selected": selecteds_ if is_owner else old_selecteds,
|
||||
"messages": messages,
|
||||
"retrieval_messages": retrival_history,
|
||||
"plot_history": plot_history,
|
||||
"state": state,
|
||||
"likes": deepcopy(data_source.get("likes", [])),
|
||||
}
|
||||
session.add(result)
|
||||
session.commit()
|
||||
|
||||
return retrival_history, plot_history
|
||||
|
||||
def reasoning_changed(self, reasoning_type):
|
||||
if reasoning_type != DEFAULT_SETTING:
|
||||
# override app settings state (temporary)
|
||||
gr.Info("Reasoning type changed to `{}`".format(reasoning_type))
|
||||
return reasoning_type
|
||||
|
||||
def is_liked(self, convo_id, liked: gr.LikeData):
|
||||
with Session(engine) as session:
|
||||
statement = select(Conversation).where(Conversation.id == convo_id)
|
||||
@@ -362,7 +699,19 @@ class ChatPage(BasePage):
|
||||
session.add(result)
|
||||
session.commit()
|
||||
|
||||
def create_pipeline(self, settings: dict, state: dict, user_id: int, *selecteds):
|
||||
def message_selected(self, retrieval_history, plot_history, msg: gr.SelectData):
|
||||
index = msg.index[0]
|
||||
return retrieval_history[index], plot_history[index]
|
||||
|
||||
def create_pipeline(
|
||||
self,
|
||||
settings: dict,
|
||||
session_reasoning_type: str,
|
||||
session_llm: str,
|
||||
state: dict,
|
||||
user_id: int,
|
||||
*selecteds,
|
||||
):
|
||||
"""Create the pipeline from settings
|
||||
|
||||
Args:
|
||||
@@ -374,10 +723,23 @@ class ChatPage(BasePage):
|
||||
Returns:
|
||||
- the pipeline objects
|
||||
"""
|
||||
reasoning_mode = settings["reasoning.use"]
|
||||
# override reasoning_mode by temporary chat page state
|
||||
print("Session reasoning type", session_reasoning_type)
|
||||
print("Session LLM", session_llm)
|
||||
reasoning_mode = (
|
||||
settings["reasoning.use"]
|
||||
if session_reasoning_type in (DEFAULT_SETTING, None)
|
||||
else session_reasoning_type
|
||||
)
|
||||
reasoning_cls = reasonings[reasoning_mode]
|
||||
print("Reasoning class", reasoning_cls)
|
||||
reasoning_id = reasoning_cls.get_info()["id"]
|
||||
|
||||
settings = deepcopy(settings)
|
||||
llm_setting_key = f"reasoning.options.{reasoning_id}.llm"
|
||||
if llm_setting_key in settings and session_llm not in (DEFAULT_SETTING, None):
|
||||
settings[llm_setting_key] = session_llm
|
||||
|
||||
# get retrievers
|
||||
retrievers = []
|
||||
for index in self._app.index_manager.indices:
|
||||
@@ -403,7 +765,15 @@ class ChatPage(BasePage):
|
||||
return pipeline, reasoning_state
|
||||
|
||||
def chat_fn(
|
||||
self, conversation_id, chat_history, settings, state, user_id, *selecteds
|
||||
self,
|
||||
conversation_id,
|
||||
chat_history,
|
||||
settings,
|
||||
reasoning_type,
|
||||
llm_type,
|
||||
state,
|
||||
user_id,
|
||||
*selecteds,
|
||||
):
|
||||
"""Chat function"""
|
||||
chat_input = chat_history[-1][0]
|
||||
@@ -413,18 +783,23 @@ class ChatPage(BasePage):
|
||||
|
||||
# construct the pipeline
|
||||
pipeline, reasoning_state = self.create_pipeline(
|
||||
settings, state, user_id, *selecteds
|
||||
settings, reasoning_type, llm_type, state, user_id, *selecteds
|
||||
)
|
||||
print("Reasoning state", reasoning_state)
|
||||
pipeline.set_output_queue(queue)
|
||||
|
||||
text, refs = "", ""
|
||||
text, refs, plot, plot_gr = "", "", None, gr.update(visible=False)
|
||||
msg_placeholder = getattr(
|
||||
flowsettings, "KH_CHAT_MSG_PLACEHOLDER", "Thinking ..."
|
||||
)
|
||||
print(msg_placeholder)
|
||||
yield chat_history + [(chat_input, text or msg_placeholder)], refs, state
|
||||
|
||||
len_ref = -1 # for logging purpose
|
||||
yield (
|
||||
chat_history + [(chat_input, text or msg_placeholder)],
|
||||
refs,
|
||||
plot_gr,
|
||||
plot,
|
||||
state,
|
||||
)
|
||||
|
||||
for response in pipeline.stream(chat_input, conversation_id, chat_history):
|
||||
|
||||
@@ -446,22 +821,42 @@ class ChatPage(BasePage):
|
||||
else:
|
||||
refs += response.content
|
||||
|
||||
if len(refs) > len_ref:
|
||||
print(f"Len refs: {len(refs)}")
|
||||
len_ref = len(refs)
|
||||
if response.channel == "plot":
|
||||
plot = response.content
|
||||
plot_gr = self._json_to_plot(plot)
|
||||
|
||||
state[pipeline.get_info()["id"]] = reasoning_state["pipeline"]
|
||||
yield chat_history + [(chat_input, text or msg_placeholder)], refs, state
|
||||
yield (
|
||||
chat_history + [(chat_input, text or msg_placeholder)],
|
||||
refs,
|
||||
plot_gr,
|
||||
plot,
|
||||
state,
|
||||
)
|
||||
|
||||
if not text:
|
||||
empty_msg = getattr(
|
||||
flowsettings, "KH_CHAT_EMPTY_MSG_PLACEHOLDER", "(Sorry, I don't know)"
|
||||
)
|
||||
print(f"Generate nothing: {empty_msg}")
|
||||
yield chat_history + [(chat_input, text or empty_msg)], refs, state
|
||||
yield (
|
||||
chat_history + [(chat_input, text or empty_msg)],
|
||||
refs,
|
||||
plot_gr,
|
||||
plot,
|
||||
state,
|
||||
)
|
||||
|
||||
def regen_fn(
|
||||
self, conversation_id, chat_history, settings, state, user_id, *selecteds
|
||||
self,
|
||||
conversation_id,
|
||||
chat_history,
|
||||
settings,
|
||||
reasoning_type,
|
||||
llm_type,
|
||||
state,
|
||||
user_id,
|
||||
*selecteds,
|
||||
):
|
||||
"""Regen function"""
|
||||
if not chat_history:
|
||||
@@ -470,11 +865,119 @@ class ChatPage(BasePage):
|
||||
return
|
||||
|
||||
state["app"]["regen"] = True
|
||||
for chat, refs, state in self.chat_fn(
|
||||
conversation_id, chat_history, settings, state, user_id, *selecteds
|
||||
):
|
||||
new_state = deepcopy(state)
|
||||
new_state["app"]["regen"] = False
|
||||
yield chat, refs, new_state
|
||||
yield from self.chat_fn(
|
||||
conversation_id,
|
||||
chat_history,
|
||||
settings,
|
||||
reasoning_type,
|
||||
llm_type,
|
||||
state,
|
||||
user_id,
|
||||
*selecteds,
|
||||
)
|
||||
|
||||
state["app"]["regen"] = False
|
||||
def check_and_suggest_name_conv(self, chat_history):
|
||||
suggest_pipeline = SuggestConvNamePipeline()
|
||||
new_name = gr.update()
|
||||
renamed = False
|
||||
|
||||
# check if this is a newly created conversation
|
||||
if len(chat_history) == 1:
|
||||
suggested_name = suggest_pipeline(chat_history).text[:40]
|
||||
new_name = gr.update(value=suggested_name)
|
||||
renamed = True
|
||||
|
||||
return new_name, renamed
|
||||
|
||||
def backup_original_info(
|
||||
self, chat_history, settings, info_pannel, original_chat_history
|
||||
):
|
||||
original_chat_history.append(chat_history[-1])
|
||||
return original_chat_history, settings, info_pannel
|
||||
|
||||
def save_log(
|
||||
self,
|
||||
conversation_id,
|
||||
chat_history,
|
||||
settings,
|
||||
info_panel,
|
||||
original_chat_history,
|
||||
original_settings,
|
||||
original_info_panel,
|
||||
log_dir,
|
||||
):
|
||||
if not Path(log_dir).exists():
|
||||
Path(log_dir).mkdir(parents=True)
|
||||
|
||||
lock = FileLock(Path(log_dir) / ".lock")
|
||||
# get current date
|
||||
today = datetime.now()
|
||||
formatted_date = today.strftime("%d%m%Y_%H")
|
||||
|
||||
with Session(engine) as session:
|
||||
statement = select(Conversation).where(Conversation.id == conversation_id)
|
||||
result = session.exec(statement).one()
|
||||
|
||||
data_source = deepcopy(result.data_source)
|
||||
likes = data_source.get("likes", [])
|
||||
if not likes:
|
||||
return
|
||||
|
||||
feedback = likes[-1][-1]
|
||||
message_index = likes[-1][0]
|
||||
|
||||
current_message = chat_history[message_index[0]]
|
||||
original_message = original_chat_history[message_index[0]]
|
||||
is_original = all(
|
||||
[
|
||||
current_item == original_item
|
||||
for current_item, original_item in zip(
|
||||
current_message, original_message
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
dataframe = [
|
||||
[
|
||||
conversation_id,
|
||||
message_index,
|
||||
current_message[0],
|
||||
current_message[1],
|
||||
chat_history,
|
||||
settings,
|
||||
info_panel,
|
||||
feedback,
|
||||
is_original,
|
||||
original_message[1],
|
||||
original_chat_history,
|
||||
original_settings,
|
||||
original_info_panel,
|
||||
]
|
||||
]
|
||||
|
||||
with lock:
|
||||
log_file = Path(log_dir) / f"{formatted_date}_log.csv"
|
||||
is_log_file_exist = log_file.is_file()
|
||||
with open(log_file, "a") as f:
|
||||
writer = csv.writer(f)
|
||||
# write headers
|
||||
if not is_log_file_exist:
|
||||
writer.writerow(
|
||||
[
|
||||
"Conversation ID",
|
||||
"Message ID",
|
||||
"Question",
|
||||
"Answer",
|
||||
"Chat History",
|
||||
"Settings",
|
||||
"Evidences",
|
||||
"Feedback",
|
||||
"Original/ Rewritten",
|
||||
"Original Answer",
|
||||
"Original Chat History",
|
||||
"Original Settings",
|
||||
"Original Evidences",
|
||||
]
|
||||
)
|
||||
|
||||
writer.writerows(dataframe)
|
||||
|
@@ -1,13 +1,20 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
import gradio as gr
|
||||
from ktem.app import BasePage
|
||||
from ktem.db.models import Conversation, engine
|
||||
from sqlmodel import Session, select
|
||||
from ktem.db.models import Conversation, User, engine
|
||||
from sqlmodel import Session, or_, select
|
||||
|
||||
import flowsettings
|
||||
|
||||
from ...utils.conversation import sync_retrieval_n_message
|
||||
from .common import STATE
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
ASSETS_DIR = "assets/icons"
|
||||
if not os.path.isdir(ASSETS_DIR):
|
||||
ASSETS_DIR = "libs/ktem/ktem/assets/icons"
|
||||
|
||||
|
||||
def is_conv_name_valid(name):
|
||||
@@ -35,14 +42,47 @@ class ConversationControl(BasePage):
|
||||
label="Chat sessions",
|
||||
choices=[],
|
||||
container=False,
|
||||
filterable=False,
|
||||
filterable=True,
|
||||
interactive=True,
|
||||
elem_classes=["unset-overflow"],
|
||||
)
|
||||
|
||||
with gr.Row() as self._new_delete:
|
||||
self.btn_new = gr.Button(value="New", min_width=10, variant="primary")
|
||||
self.btn_del = gr.Button(value="Delete", min_width=10, variant="stop")
|
||||
self.btn_new = gr.Button(
|
||||
value="",
|
||||
icon=f"{ASSETS_DIR}/new.svg",
|
||||
min_width=2,
|
||||
scale=1,
|
||||
size="sm",
|
||||
elem_classes=["no-background", "body-text-color"],
|
||||
)
|
||||
self.btn_del = gr.Button(
|
||||
value="",
|
||||
icon=f"{ASSETS_DIR}/delete.svg",
|
||||
min_width=2,
|
||||
scale=1,
|
||||
size="sm",
|
||||
elem_classes=["no-background", "body-text-color"],
|
||||
)
|
||||
self.btn_conversation_rn = gr.Button(
|
||||
value="",
|
||||
icon=f"{ASSETS_DIR}/rename.svg",
|
||||
min_width=2,
|
||||
scale=1,
|
||||
size="sm",
|
||||
elem_classes=["no-background", "body-text-color"],
|
||||
)
|
||||
self.btn_info_expand = gr.Button(
|
||||
value="",
|
||||
icon=f"{ASSETS_DIR}/sidebar.svg",
|
||||
min_width=2,
|
||||
scale=1,
|
||||
size="sm",
|
||||
elem_classes=["no-background", "body-text-color"],
|
||||
)
|
||||
self.cb_is_public = gr.Checkbox(
|
||||
value=False, label="Shared", min_width=10, scale=4
|
||||
)
|
||||
|
||||
with gr.Row(visible=False) as self._delete_confirm:
|
||||
self.btn_del_conf = gr.Button(
|
||||
@@ -54,28 +94,60 @@ class ConversationControl(BasePage):
|
||||
|
||||
with gr.Row():
|
||||
self.conversation_rn = gr.Text(
|
||||
label="(Enter) to save",
|
||||
placeholder="Conversation name",
|
||||
container=False,
|
||||
container=True,
|
||||
scale=5,
|
||||
min_width=10,
|
||||
interactive=True,
|
||||
)
|
||||
self.conversation_rn_btn = gr.Button(
|
||||
value="Rename",
|
||||
scale=1,
|
||||
min_width=10,
|
||||
elem_classes=["no-background", "body-text-color", "bold-text"],
|
||||
visible=False,
|
||||
)
|
||||
|
||||
def load_chat_history(self, user_id):
|
||||
"""Reload chat history"""
|
||||
|
||||
# In case user are admin. They can also watch the
|
||||
# public conversations
|
||||
can_see_public: bool = False
|
||||
with Session(engine) as session:
|
||||
statement = select(User).where(User.id == user_id)
|
||||
result = session.exec(statement).one_or_none()
|
||||
|
||||
if result is not None:
|
||||
if flowsettings.KH_USER_CAN_SEE_PUBLIC:
|
||||
can_see_public = (
|
||||
result.username == flowsettings.KH_USER_CAN_SEE_PUBLIC
|
||||
)
|
||||
else:
|
||||
can_see_public = True
|
||||
|
||||
print(f"User-id: {user_id}, can see public conversations: {can_see_public}")
|
||||
|
||||
options = []
|
||||
with Session(engine) as session:
|
||||
statement = (
|
||||
select(Conversation)
|
||||
.where(Conversation.user == user_id)
|
||||
.order_by(Conversation.date_created.desc()) # type: ignore
|
||||
)
|
||||
# Define condition based on admin-role:
|
||||
# - can_see: can see their conversations & public files
|
||||
# - can_not_see: only see their conversations
|
||||
if can_see_public:
|
||||
statement = (
|
||||
select(Conversation)
|
||||
.where(
|
||||
or_(
|
||||
Conversation.user == user_id,
|
||||
Conversation.is_public,
|
||||
)
|
||||
)
|
||||
.order_by(
|
||||
Conversation.is_public.desc(), Conversation.date_created.desc()
|
||||
) # type: ignore
|
||||
)
|
||||
else:
|
||||
statement = (
|
||||
select(Conversation)
|
||||
.where(Conversation.user == user_id)
|
||||
.order_by(Conversation.date_created.desc()) # type: ignore
|
||||
)
|
||||
|
||||
results = session.exec(statement).all()
|
||||
for result in results:
|
||||
options.append((result.name, result.id))
|
||||
@@ -129,7 +201,7 @@ class ConversationControl(BasePage):
|
||||
else:
|
||||
return None, gr.update(value=None, choices=[])
|
||||
|
||||
def select_conv(self, conversation_id):
|
||||
def select_conv(self, conversation_id, user_id):
|
||||
"""Select the conversation"""
|
||||
with Session(engine) as session:
|
||||
statement = select(Conversation).where(Conversation.id == conversation_id)
|
||||
@@ -137,18 +209,46 @@ class ConversationControl(BasePage):
|
||||
result = session.exec(statement).one()
|
||||
id_ = result.id
|
||||
name = result.name
|
||||
selected = result.data_source.get("selected", {})
|
||||
is_conv_public = result.is_public
|
||||
|
||||
# disable file selection ids state if
|
||||
# not the owner of the conversation
|
||||
if user_id == result.user:
|
||||
selected = result.data_source.get("selected", {})
|
||||
else:
|
||||
selected = {}
|
||||
|
||||
chats = result.data_source.get("messages", [])
|
||||
info_panel = ""
|
||||
|
||||
retrieval_history: list[str] = result.data_source.get(
|
||||
"retrieval_messages", []
|
||||
)
|
||||
plot_history: list[dict] = result.data_source.get("plot_history", [])
|
||||
|
||||
# On initialization
|
||||
# Ensure len of retrieval and messages are equal
|
||||
retrieval_history = sync_retrieval_n_message(chats, retrieval_history)
|
||||
|
||||
info_panel = (
|
||||
retrieval_history[-1]
|
||||
if retrieval_history
|
||||
else "<h5><b>No evidence found.</b></h5>"
|
||||
)
|
||||
plot_data = plot_history[-1] if plot_history else None
|
||||
state = result.data_source.get("state", STATE)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
id_ = ""
|
||||
name = ""
|
||||
selected = {}
|
||||
chats = []
|
||||
retrieval_history = []
|
||||
plot_history = []
|
||||
info_panel = ""
|
||||
plot_data = None
|
||||
state = STATE
|
||||
is_conv_public = False
|
||||
|
||||
indices = []
|
||||
for index in self._app.index_manager.indices:
|
||||
@@ -160,10 +260,29 @@ class ConversationControl(BasePage):
|
||||
if isinstance(index.selector, tuple):
|
||||
indices.extend(selected.get(str(index.id), index.default_selector))
|
||||
|
||||
return id_, id_, name, chats, info_panel, state, *indices
|
||||
return (
|
||||
id_,
|
||||
id_,
|
||||
name,
|
||||
chats,
|
||||
info_panel,
|
||||
plot_data,
|
||||
retrieval_history,
|
||||
plot_history,
|
||||
is_conv_public,
|
||||
state,
|
||||
*indices,
|
||||
)
|
||||
|
||||
def rename_conv(self, conversation_id, new_name, user_id):
|
||||
def rename_conv(self, conversation_id, new_name, is_renamed, user_id):
|
||||
"""Rename the conversation"""
|
||||
if not is_renamed:
|
||||
return (
|
||||
gr.update(),
|
||||
conversation_id,
|
||||
gr.update(visible=False),
|
||||
)
|
||||
|
||||
if user_id is None:
|
||||
gr.Warning("Please sign in first (Settings → User Settings)")
|
||||
return gr.update(), ""
|
||||
@@ -185,7 +304,12 @@ class ConversationControl(BasePage):
|
||||
session.commit()
|
||||
|
||||
history = self.load_chat_history(user_id)
|
||||
return gr.update(choices=history), conversation_id
|
||||
gr.Info("Conversation renamed.")
|
||||
return (
|
||||
gr.update(choices=history),
|
||||
conversation_id,
|
||||
gr.update(visible=False),
|
||||
)
|
||||
|
||||
def _on_app_created(self):
|
||||
"""Reload the conversation once the app is created"""
|
||||
|
@@ -12,7 +12,7 @@ class ReportIssue(BasePage):
|
||||
self.on_building_ui()
|
||||
|
||||
def on_building_ui(self):
|
||||
with gr.Accordion(label="Report", open=False):
|
||||
with gr.Accordion(label="Feedback", open=False):
|
||||
self.correctness = gr.Radio(
|
||||
choices=[
|
||||
("The answer is correct", "correct"),
|
||||
|
@@ -9,6 +9,7 @@ from theflow.settings import settings
|
||||
def get_remote_doc(url: str) -> str:
|
||||
try:
|
||||
res = requests.get(url)
|
||||
res.raise_for_status()
|
||||
return res.text
|
||||
except Exception as e:
|
||||
print(f"Failed to fetch document from {url}: {e}")
|
||||
|
@@ -7,9 +7,9 @@ from sqlmodel import Session, select
|
||||
|
||||
fetch_creds = """
|
||||
function() {
|
||||
const username = getStorage('username')
|
||||
const password = getStorage('password')
|
||||
return [username, password];
|
||||
const username = getStorage('username', '')
|
||||
const password = getStorage('password', '')
|
||||
return [username, password, null];
|
||||
}
|
||||
"""
|
||||
|
||||
|
@@ -15,18 +15,18 @@ class ResourcesTab(BasePage):
|
||||
self.on_building_ui()
|
||||
|
||||
def on_building_ui(self):
|
||||
if self._app.f_user_management:
|
||||
with gr.Tab("User Management", visible=False) as self.user_management_tab:
|
||||
self.user_management = UserManagement(self._app)
|
||||
with gr.Tab("Index Collections") as self.index_management_tab:
|
||||
self.index_management = IndexManagement(self._app)
|
||||
|
||||
with gr.Tab("LLMs") as self.llm_management_tab:
|
||||
self.llm_management = LLMManagement(self._app)
|
||||
|
||||
with gr.Tab("Embedding Models") as self.emb_management_tab:
|
||||
with gr.Tab("Embeddings") as self.emb_management_tab:
|
||||
self.emb_management = EmbeddingManagement(self._app)
|
||||
|
||||
with gr.Tab("Index Management") as self.index_management_tab:
|
||||
self.index_management = IndexManagement(self._app)
|
||||
if self._app.f_user_management:
|
||||
with gr.Tab("Users", visible=False) as self.user_management_tab:
|
||||
self.user_management = UserManagement(self._app)
|
||||
|
||||
def on_subscribe_public_events(self):
|
||||
if self._app.f_user_management:
|
||||
|
@@ -94,6 +94,28 @@ def validate_password(pwd, pwd_cnf):
|
||||
return ""
|
||||
|
||||
|
||||
def create_user(usn, pwd) -> bool:
|
||||
with Session(engine) as session:
|
||||
statement = select(User).where(User.username_lower == usn.lower())
|
||||
result = session.exec(statement).all()
|
||||
if result:
|
||||
print(f'User "{usn}" already exists')
|
||||
return False
|
||||
|
||||
else:
|
||||
hashed_password = hashlib.sha256(pwd.encode()).hexdigest()
|
||||
user = User(
|
||||
username=usn,
|
||||
username_lower=usn.lower(),
|
||||
password=hashed_password,
|
||||
admin=True,
|
||||
)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class UserManagement(BasePage):
|
||||
def __init__(self, app):
|
||||
self._app = app
|
||||
@@ -105,23 +127,9 @@ class UserManagement(BasePage):
|
||||
usn = flowsettings.KH_FEATURE_USER_MANAGEMENT_ADMIN
|
||||
pwd = flowsettings.KH_FEATURE_USER_MANAGEMENT_PASSWORD
|
||||
|
||||
with Session(engine) as session:
|
||||
statement = select(User).where(User.username_lower == usn.lower())
|
||||
result = session.exec(statement).all()
|
||||
if result:
|
||||
print(f'User "{usn}" already exists')
|
||||
|
||||
else:
|
||||
hashed_password = hashlib.sha256(pwd.encode()).hexdigest()
|
||||
user = User(
|
||||
username=usn,
|
||||
username_lower=usn.lower(),
|
||||
password=hashed_password,
|
||||
admin=True,
|
||||
)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
gr.Info(f'User "{usn}" created successfully')
|
||||
is_created = create_user(usn, pwd)
|
||||
if is_created:
|
||||
gr.Info(f'User "{usn}" created successfully')
|
||||
|
||||
def on_building_ui(self):
|
||||
with gr.Tab(label="User list"):
|
||||
@@ -224,7 +232,7 @@ class UserManagement(BasePage):
|
||||
gr.update(visible=False),
|
||||
gr.update(visible=False),
|
||||
),
|
||||
inputs=None,
|
||||
inputs=[],
|
||||
outputs=[self.btn_delete, self.btn_delete_yes, self.btn_delete_no],
|
||||
show_progress="hidden",
|
||||
)
|
||||
|
@@ -2,13 +2,15 @@ import hashlib
|
||||
|
||||
import gradio as gr
|
||||
from ktem.app import BasePage
|
||||
from ktem.components import reasonings
|
||||
from ktem.db.models import Settings, User, engine
|
||||
from sqlmodel import Session, select
|
||||
|
||||
signout_js = """
|
||||
function() {
|
||||
function(u, c, pw, pwc) {
|
||||
removeFromStorage('username');
|
||||
removeFromStorage('password');
|
||||
return [u, c, pw, pwc];
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -72,6 +74,10 @@ class SettingsPage(BasePage):
|
||||
self._components = {}
|
||||
self._reasoning_mode = {}
|
||||
|
||||
# store llms and embeddings components
|
||||
self._llms = []
|
||||
self._embeddings = []
|
||||
|
||||
# render application page if there are application settings
|
||||
self._render_app_tab = False
|
||||
if self._default_settings.application.settings:
|
||||
@@ -101,14 +107,13 @@ class SettingsPage(BasePage):
|
||||
|
||||
def on_building_ui(self):
|
||||
if self._app.f_user_management:
|
||||
with gr.Tab("Users"):
|
||||
with gr.Tab("User settings"):
|
||||
self.user_tab()
|
||||
with gr.Tab("General"):
|
||||
self.app_tab()
|
||||
with gr.Tab("Document Indices"):
|
||||
self.index_tab()
|
||||
with gr.Tab("Reasoning Pipelines"):
|
||||
self.reasoning_tab()
|
||||
|
||||
self.app_tab()
|
||||
self.index_tab()
|
||||
self.reasoning_tab()
|
||||
|
||||
self.setting_save_btn = gr.Button(
|
||||
"Save changes", variant="primary", scale=1, elem_classes=["right-button"]
|
||||
)
|
||||
@@ -192,7 +197,7 @@ class SettingsPage(BasePage):
|
||||
)
|
||||
onSignOutClick = self.signout.click(
|
||||
lambda: (None, "Current user: ___", "", ""),
|
||||
inputs=None,
|
||||
inputs=[],
|
||||
outputs=[
|
||||
self._user_id,
|
||||
self.current_name,
|
||||
@@ -248,10 +253,14 @@ class SettingsPage(BasePage):
|
||||
return "", ""
|
||||
|
||||
def app_tab(self):
|
||||
with gr.Tab("General application settings", visible=self._render_app_tab):
|
||||
with gr.Tab("General", visible=self._render_app_tab):
|
||||
for n, si in self._default_settings.application.settings.items():
|
||||
obj = render_setting_item(si, si.value)
|
||||
self._components[f"application.{n}"] = obj
|
||||
if si.special_type == "llm":
|
||||
self._llms.append(obj)
|
||||
if si.special_type == "embedding":
|
||||
self._embeddings.append(obj)
|
||||
|
||||
def index_tab(self):
|
||||
# TODO: double check if we need general
|
||||
@@ -260,12 +269,18 @@ class SettingsPage(BasePage):
|
||||
# obj = render_setting_item(si, si.value)
|
||||
# self._components[f"index.{n}"] = obj
|
||||
|
||||
with gr.Tab("Index settings", visible=self._render_index_tab):
|
||||
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():
|
||||
with gr.Tab(f"Index {pn}"):
|
||||
name = "{} Collection".format(id2name.get(pn, f"<id {pn}>"))
|
||||
with gr.Tab(name):
|
||||
for n, si in sig.settings.items():
|
||||
obj = render_setting_item(si, si.value)
|
||||
self._components[f"index.options.{pn}.{n}"] = obj
|
||||
if si.special_type == "llm":
|
||||
self._llms.append(obj)
|
||||
if si.special_type == "embedding":
|
||||
self._embeddings.append(obj)
|
||||
|
||||
def reasoning_tab(self):
|
||||
with gr.Tab("Reasoning settings", visible=self._render_reasoning_tab):
|
||||
@@ -275,6 +290,10 @@ class SettingsPage(BasePage):
|
||||
continue
|
||||
obj = render_setting_item(si, si.value)
|
||||
self._components[f"reasoning.{n}"] = obj
|
||||
if si.special_type == "llm":
|
||||
self._llms.append(obj)
|
||||
if si.special_type == "embedding":
|
||||
self._embeddings.append(obj)
|
||||
|
||||
gr.Markdown("### Reasoning-specific settings")
|
||||
self._components["reasoning.use"] = render_setting_item(
|
||||
@@ -289,10 +308,19 @@ class SettingsPage(BasePage):
|
||||
visible=idx == 0,
|
||||
elem_id=pn,
|
||||
) as self._reasoning_mode[pn]:
|
||||
gr.Markdown("**Name**: Description")
|
||||
reasoning = reasonings.get(pn, None)
|
||||
if reasoning is None:
|
||||
gr.Markdown("**Name**: Description")
|
||||
else:
|
||||
info = reasoning.get_info()
|
||||
gr.Markdown(f"**{info['name']}**: {info['description']}")
|
||||
for n, si in sig.settings.items():
|
||||
obj = render_setting_item(si, si.value)
|
||||
self._components[f"reasoning.options.{pn}.{n}"] = obj
|
||||
if si.special_type == "llm":
|
||||
self._llms.append(obj)
|
||||
if si.special_type == "embedding":
|
||||
self._embeddings.append(obj)
|
||||
|
||||
def change_reasoning_mode(self, value):
|
||||
output = []
|
||||
@@ -360,3 +388,38 @@ class SettingsPage(BasePage):
|
||||
outputs=[self._settings_state] + self.components(),
|
||||
show_progress="hidden",
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
for llm in self._llms:
|
||||
self._app.app.load(
|
||||
update_llms,
|
||||
inputs=[],
|
||||
outputs=[llm],
|
||||
show_progress="hidden",
|
||||
)
|
||||
for emb in self._embeddings:
|
||||
self._app.app.load(
|
||||
update_embeddings,
|
||||
inputs=[],
|
||||
outputs=[emb],
|
||||
show_progress="hidden",
|
||||
)
|
||||
|
Reference in New Issue
Block a user