[AUR-411] Adopt to Example2 project (#28)
Add the chatbot from Example2. Create the UI for chat.
This commit is contained in:
committed by
GitHub
parent
533fffa6db
commit
6e7905cbc0
45
knowledgehub/contribs/promptui/ui/__init__.py
Normal file
45
knowledgehub/contribs/promptui/ui/__init__.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from typing import Union
|
||||
|
||||
import gradio as gr
|
||||
import yaml
|
||||
from theflow.utils.modules import import_dotted_string
|
||||
|
||||
from ..themes import John
|
||||
from .chat import build_chat_ui
|
||||
from .pipeline import build_pipeline_ui
|
||||
|
||||
|
||||
def build_from_dict(config: Union[str, dict]):
|
||||
"""Build a full UI from YAML config file"""
|
||||
|
||||
if isinstance(config, str):
|
||||
with open(config) as f:
|
||||
config_dict: dict = yaml.safe_load(f)
|
||||
elif isinstance(config, dict):
|
||||
config_dict = config
|
||||
else:
|
||||
raise ValueError(
|
||||
f"config must be either a yaml path or a dict, got {type(config)}"
|
||||
)
|
||||
|
||||
demos = []
|
||||
for key, value in config_dict.items():
|
||||
pipeline_def = import_dotted_string(key, safe=False)
|
||||
if value["ui-type"] == "chat":
|
||||
demos.append(build_chat_ui(value, pipeline_def))
|
||||
else:
|
||||
demos.append(build_pipeline_ui(value, pipeline_def))
|
||||
if len(demos) == 1:
|
||||
demo = demos[0]
|
||||
else:
|
||||
demo = gr.TabbedInterface(
|
||||
demos,
|
||||
tab_names=list(config_dict.keys()),
|
||||
title="PromptUI from kotaemon",
|
||||
analytics_enabled=False,
|
||||
theme=John(),
|
||||
)
|
||||
|
||||
demo.queue()
|
||||
|
||||
return demo
|
282
knowledgehub/contribs/promptui/ui/chat.py
Normal file
282
knowledgehub/contribs/promptui/ui/chat.py
Normal file
@@ -0,0 +1,282 @@
|
||||
import pickle
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import gradio as gr
|
||||
from theflow.storage import storage
|
||||
|
||||
from kotaemon.chatbot import ChatConversation
|
||||
from kotaemon.contribs.promptui.base import get_component
|
||||
from kotaemon.contribs.promptui.export import export
|
||||
|
||||
USAGE_INSTRUCTION = """## How to use:
|
||||
|
||||
1. Set the desired parameters.
|
||||
2. Click "New chat" to start a chat session with the supplied parameters. This
|
||||
set of parameters will persist until the end of the chat session. During an
|
||||
ongoing chat session, changing the parameters will not take any effect.
|
||||
3. Chat and interact with the chat bot on the right panel. You can add any
|
||||
additional input (if any), and they will be supplied to the chatbot.
|
||||
4. During chat, the log of the chat will show up in the "Output" tabs. This is
|
||||
empty by default, so if you want to show the log here, tell the AI developers
|
||||
to configure the UI settings.
|
||||
5. When finishing chat, select your preference in the radio box. Click "End chat".
|
||||
This will save the chat log and the preference to disk.
|
||||
6. To compare the result of different run, click "Export" to get an Excel
|
||||
spreadsheet summary of different run.
|
||||
|
||||
## Support:
|
||||
|
||||
In case of errors, you can:
|
||||
|
||||
- PromptUI instruction:
|
||||
https://github.com/Cinnamon/kotaemon/wiki/Utilities#prompt-engineering-ui
|
||||
- Create bug fix and make PR at: https://github.com/Cinnamon/kotaemon
|
||||
- Ping any of @john @tadashi @ian @jacky in Slack channel #llm-productization
|
||||
|
||||
## Contribute:
|
||||
|
||||
- Follow installation at: https://github.com/Cinnamon/kotaemon/
|
||||
"""
|
||||
|
||||
|
||||
def construct_chat_ui(
|
||||
config, func_new_chat, func_chat, func_end_chat, func_export_to_excel
|
||||
) -> gr.Blocks:
|
||||
"""Construct the prompt engineering UI for chat
|
||||
|
||||
Args:
|
||||
config: the UI config
|
||||
func_new_chat: the function for starting a new chat session
|
||||
func_chat: the function for chatting interaction
|
||||
func_end_chat: the function for ending and saving the chat
|
||||
func_export_to_excel: the function to export the logs to excel
|
||||
|
||||
Returns:
|
||||
the UI object
|
||||
"""
|
||||
inputs, outputs, params = [], [], []
|
||||
for name, component_def in config.get("inputs", {}).items():
|
||||
if "params" not in component_def:
|
||||
component_def["params"] = {}
|
||||
component_def["params"]["interactive"] = True
|
||||
component = get_component(component_def)
|
||||
if hasattr(component, "label") and not component.label: # type: ignore
|
||||
component.label = name # type: ignore
|
||||
|
||||
inputs.append(component)
|
||||
|
||||
for name, component_def in config.get("params", {}).items():
|
||||
if "params" not in component_def:
|
||||
component_def["params"] = {}
|
||||
component_def["params"]["interactive"] = True
|
||||
component = get_component(component_def)
|
||||
if hasattr(component, "label") and not component.label: # type: ignore
|
||||
component.label = name # type: ignore
|
||||
|
||||
params.append(component)
|
||||
|
||||
for idx, component_def in enumerate(config.get("outputs", [])):
|
||||
if "params" not in component_def:
|
||||
component_def["params"] = {}
|
||||
component_def["params"]["interactive"] = False
|
||||
component = get_component(component_def)
|
||||
if hasattr(component, "label") and not component.label: # type: ignore
|
||||
component.label = f"Output {idx}" # type: ignore
|
||||
|
||||
outputs.append(component)
|
||||
|
||||
sess = gr.State(value=None)
|
||||
chatbot = gr.Chatbot(label="Chatbot")
|
||||
chat = gr.ChatInterface(func_chat, chatbot=chatbot, additional_inputs=[sess])
|
||||
param_state = gr.Textbox(interactive=False)
|
||||
|
||||
with gr.Blocks(analytics_enabled=False, title="Welcome to PromptUI") as demo:
|
||||
sess.render()
|
||||
with gr.Accordion(label="HOW TO", open=False):
|
||||
gr.Markdown(USAGE_INSTRUCTION)
|
||||
with gr.Row():
|
||||
run_btn = gr.Button("New chat")
|
||||
run_btn.click(
|
||||
func_new_chat,
|
||||
inputs=params,
|
||||
outputs=[
|
||||
chat.chatbot,
|
||||
chat.chatbot_state,
|
||||
chat.saved_input,
|
||||
param_state,
|
||||
sess,
|
||||
],
|
||||
)
|
||||
with gr.Accordion(label="End chat", open=False):
|
||||
likes = gr.Radio(["like", "dislike", "neutral"], value="neutral")
|
||||
save_log = gr.Checkbox(
|
||||
value=True,
|
||||
label="Save log",
|
||||
info="If saved, log can be exported later",
|
||||
show_label=True,
|
||||
)
|
||||
end_btn = gr.Button("End chat")
|
||||
end_btn.click(
|
||||
func_end_chat,
|
||||
inputs=[likes, save_log, sess],
|
||||
outputs=[param_state, sess],
|
||||
)
|
||||
with gr.Accordion(label="Export", open=False):
|
||||
exported_file = gr.File(
|
||||
label="Output file", show_label=True, height=100
|
||||
)
|
||||
export_btn = gr.Button("Export")
|
||||
export_btn.click(
|
||||
func_export_to_excel, inputs=None, outputs=exported_file
|
||||
)
|
||||
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
with gr.Tab("Params"):
|
||||
for component in params:
|
||||
component.render()
|
||||
with gr.Accordion(label="Session state", open=False):
|
||||
param_state.render()
|
||||
|
||||
with gr.Tab("Outputs"):
|
||||
for component in outputs:
|
||||
component.render()
|
||||
with gr.Column():
|
||||
chat.render()
|
||||
|
||||
return demo.queue()
|
||||
|
||||
|
||||
def build_chat_ui(config, pipeline_def):
|
||||
"""Build the chat UI
|
||||
|
||||
Args:
|
||||
config: the UI config
|
||||
pipeline_def: the pipeline definition
|
||||
|
||||
Returns:
|
||||
the UI object
|
||||
"""
|
||||
output_dir: Path = Path(storage.url(pipeline_def().config.store_result))
|
||||
exported_dir = output_dir.parent / "exported"
|
||||
exported_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def new_chat(*args):
|
||||
"""Start a new chat function
|
||||
|
||||
Args:
|
||||
*args: the pipeline init params
|
||||
|
||||
Returns:
|
||||
new empty states
|
||||
"""
|
||||
gr.Info("Starting new session...")
|
||||
param_dicts = {
|
||||
name: value for name, value in zip(config["params"].keys(), args)
|
||||
}
|
||||
for key in param_dicts.keys():
|
||||
if config["params"][key].get("component").lower() == "file":
|
||||
param_dicts[key] = param_dicts[key].name
|
||||
|
||||
# TODO: currently hard-code as ChatConversation
|
||||
pipeline = pipeline_def()
|
||||
session = ChatConversation(bot=pipeline)
|
||||
session.set(param_dicts)
|
||||
session.start_session()
|
||||
|
||||
param_state_str = "\n".join(
|
||||
f"- {name}: {value}" for name, value in param_dicts.items()
|
||||
)
|
||||
|
||||
gr.Info("New chat session started.")
|
||||
return [], [], None, param_state_str, session
|
||||
|
||||
def chat(message, history, session, *args):
|
||||
"""The chat interface
|
||||
|
||||
# TODO: wrap the input and output of this chat function so that it
|
||||
work with more types of chat conversation than simple text
|
||||
|
||||
Args:
|
||||
message: the message from the user
|
||||
history: the gradio history of the chat
|
||||
session: the chat object session
|
||||
*args: the additional inputs
|
||||
|
||||
Returns:
|
||||
the response from the chatbot
|
||||
"""
|
||||
if session is None:
|
||||
raise gr.Error(
|
||||
"No active chat session. Please set the params and click New chat"
|
||||
)
|
||||
|
||||
return session(message).content
|
||||
|
||||
def end_chat(preference: str, save_log: bool, session):
|
||||
"""End the chat session
|
||||
|
||||
Args:
|
||||
preference: the preference of the user
|
||||
save_log: whether to save the result
|
||||
session: the chat object session
|
||||
|
||||
Returns:
|
||||
the new empty state
|
||||
"""
|
||||
gr.Info("Ending session...")
|
||||
session.end_session()
|
||||
output_dir: Path = (
|
||||
Path(storage.url(session.config.store_result)) / session.last_run.id()
|
||||
)
|
||||
|
||||
if not save_log:
|
||||
if output_dir.exists():
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(output_dir)
|
||||
|
||||
session = None
|
||||
param_state = ""
|
||||
gr.Info("End session without saving log.")
|
||||
return param_state, session
|
||||
|
||||
# add preference result to progress
|
||||
with (output_dir / "progress.pkl").open("rb") as fi:
|
||||
progress = pickle.load(fi)
|
||||
progress["preference"] = preference
|
||||
with (output_dir / "progress.pkl").open("wb") as fo:
|
||||
pickle.dump(progress, fo)
|
||||
|
||||
# get the original params
|
||||
param_dicts = {name: session.getx(name) for name in config["params"].keys()}
|
||||
with (output_dir / "params.pkl").open("wb") as fo:
|
||||
pickle.dump(param_dicts, fo)
|
||||
|
||||
session = None
|
||||
param_state = ""
|
||||
gr.Info("End session and save log.")
|
||||
return param_state, session
|
||||
|
||||
def export_func():
|
||||
name = (
|
||||
f"{pipeline_def.__module__}.{pipeline_def.__name__}_{datetime.now()}.xlsx"
|
||||
)
|
||||
path = str(exported_dir / name)
|
||||
gr.Info(f"Begin exporting {name}...")
|
||||
try:
|
||||
export(config=config, pipeline_def=pipeline_def, output_path=path)
|
||||
except Exception as e:
|
||||
raise gr.Error(f"Failed to export. Please contact project's AIR: {e}")
|
||||
gr.Info(f"Exported {name}. Please go to the `Exported file` tab to download")
|
||||
return path
|
||||
|
||||
demo = construct_chat_ui(
|
||||
config=config,
|
||||
func_new_chat=new_chat,
|
||||
func_chat=chat,
|
||||
func_end_chat=end_chat,
|
||||
func_export_to_excel=export_func,
|
||||
)
|
||||
return demo
|
242
knowledgehub/contribs/promptui/ui/pipeline.py
Normal file
242
knowledgehub/contribs/promptui/ui/pipeline.py
Normal file
@@ -0,0 +1,242 @@
|
||||
import pickle
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
import gradio as gr
|
||||
import pandas as pd
|
||||
from theflow.storage import storage
|
||||
|
||||
from kotaemon.contribs.promptui.base import get_component
|
||||
from kotaemon.contribs.promptui.export import export
|
||||
|
||||
from ..logs import ResultLog
|
||||
|
||||
USAGE_INSTRUCTION = """## How to use:
|
||||
|
||||
1. Set the desired parameters.
|
||||
2. Set the desired inputs.
|
||||
3. Click "Run" to execute the pipeline with the supplied parameters and inputs
|
||||
4. The pipeline output will show up in the output panel.
|
||||
5. Repeat from step 1.
|
||||
6. To compare the result of different run, click "Export" to get an Excel
|
||||
spreadsheet summary of different run.
|
||||
|
||||
## Support:
|
||||
|
||||
In case of errors, you can:
|
||||
|
||||
- PromptUI instruction:
|
||||
https://github.com/Cinnamon/kotaemon/wiki/Utilities#prompt-engineering-ui
|
||||
- Create bug fix and make PR at: https://github.com/Cinnamon/kotaemon
|
||||
- Ping any of @john @tadashi @ian @jacky in Slack channel #llm-productization
|
||||
|
||||
## Contribute:
|
||||
|
||||
- Follow installation at: https://github.com/Cinnamon/kotaemon/
|
||||
"""
|
||||
|
||||
|
||||
def construct_pipeline_ui(
|
||||
config, func_run, func_save, func_load_params, func_activate_params, func_export
|
||||
) -> gr.Blocks:
|
||||
"""Create UI from config file. Execute the UI from config file
|
||||
|
||||
- Can do now: Log from stdout to UI
|
||||
- In the future, we can provide some hooks and callbacks to let developers better
|
||||
fine-tune the UI behavior.
|
||||
"""
|
||||
inputs, outputs, params = [], [], []
|
||||
for name, component_def in config.get("inputs", {}).items():
|
||||
if "params" not in component_def:
|
||||
component_def["params"] = {}
|
||||
component_def["params"]["interactive"] = True
|
||||
component = get_component(component_def)
|
||||
if hasattr(component, "label") and not component.label: # type: ignore
|
||||
component.label = name # type: ignore
|
||||
|
||||
inputs.append(component)
|
||||
|
||||
for name, component_def in config.get("params", {}).items():
|
||||
if "params" not in component_def:
|
||||
component_def["params"] = {}
|
||||
component_def["params"]["interactive"] = True
|
||||
component = get_component(component_def)
|
||||
if hasattr(component, "label") and not component.label: # type: ignore
|
||||
component.label = name # type: ignore
|
||||
|
||||
params.append(component)
|
||||
|
||||
for idx, component_def in enumerate(config.get("outputs", [])):
|
||||
if "params" not in component_def:
|
||||
component_def["params"] = {}
|
||||
component_def["params"]["interactive"] = False
|
||||
component = get_component(component_def)
|
||||
if hasattr(component, "label") and not component.label: # type: ignore
|
||||
component.label = f"Output {idx}" # type: ignore
|
||||
|
||||
outputs.append(component)
|
||||
|
||||
exported_file = gr.File(label="Output file", show_label=True)
|
||||
history_dataframe = gr.DataFrame(wrap=True)
|
||||
|
||||
temp = gr.Tab
|
||||
with gr.Blocks(analytics_enabled=False, title="Welcome to PromptUI") as demo:
|
||||
with gr.Accordion(label="HOW TO", open=False):
|
||||
gr.Markdown(USAGE_INSTRUCTION)
|
||||
with gr.Accordion(label="Params History", open=False):
|
||||
with gr.Row():
|
||||
save_btn = gr.Button("Save params")
|
||||
save_btn.click(func_save, inputs=params, outputs=history_dataframe)
|
||||
load_params_btn = gr.Button("Reload params")
|
||||
load_params_btn.click(
|
||||
func_load_params, inputs=None, outputs=history_dataframe
|
||||
)
|
||||
history_dataframe.render()
|
||||
history_dataframe.select(
|
||||
func_activate_params, inputs=params, outputs=params
|
||||
)
|
||||
with gr.Row():
|
||||
run_btn = gr.Button("Run")
|
||||
run_btn.click(func_run, inputs=inputs + params, outputs=outputs)
|
||||
export_btn = gr.Button(
|
||||
"Export (Result will be in Exported file next to Output)"
|
||||
)
|
||||
export_btn.click(func_export, inputs=None, outputs=exported_file)
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
with temp("Params"):
|
||||
for component in params:
|
||||
component.render()
|
||||
with temp("Inputs"):
|
||||
for component in inputs:
|
||||
component.render()
|
||||
with gr.Column():
|
||||
with temp("Outputs"):
|
||||
for component in outputs:
|
||||
component.render()
|
||||
with temp("Exported file"):
|
||||
exported_file.render()
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
def load_saved_params(path: str) -> Dict:
|
||||
"""Load the saved params from path to a dataframe"""
|
||||
# get all pickle files
|
||||
files = list(sorted(Path(path).glob("*.pkl")))
|
||||
data: Dict[str, Any] = {"_id": [None] * len(files)}
|
||||
for idx, each_file in enumerate(files):
|
||||
with open(each_file, "rb") as f:
|
||||
each_data = pickle.load(f)
|
||||
data["_id"][idx] = Path(each_file).stem
|
||||
for key, value in each_data.items():
|
||||
if key not in data:
|
||||
data[key] = [None] * len(files)
|
||||
data[key][idx] = value
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def build_pipeline_ui(config: dict, pipeline_def):
|
||||
"""Build a tab from config file"""
|
||||
inputs_name = list(config.get("inputs", {}).keys())
|
||||
params_name = list(config.get("params", {}).keys())
|
||||
outputs_def = config.get("outputs", [])
|
||||
|
||||
output_dir: Path = Path(storage.url(pipeline_def().config.store_result))
|
||||
exported_dir = output_dir.parent / "exported"
|
||||
exported_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
save_dir = (
|
||||
output_dir.parent
|
||||
/ "saved"
|
||||
/ f"{pipeline_def.__module__}.{pipeline_def.__name__}"
|
||||
)
|
||||
save_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
resultlog = getattr(pipeline_def, "_promptui_resultlog", ResultLog)
|
||||
allowed_resultlog_callbacks = {i for i in dir(resultlog) if not i.startswith("__")}
|
||||
|
||||
def run_func(*args):
|
||||
inputs = {
|
||||
name: value for name, value in zip(inputs_name, args[: len(inputs_name)])
|
||||
}
|
||||
params = {
|
||||
name: value for name, value in zip(params_name, args[len(inputs_name) :])
|
||||
}
|
||||
pipeline = pipeline_def()
|
||||
pipeline.set(params)
|
||||
pipeline(**inputs)
|
||||
with storage.open(
|
||||
storage.url(
|
||||
pipeline.config.store_result, pipeline.last_run.id(), "params.pkl"
|
||||
),
|
||||
"wb",
|
||||
) as f:
|
||||
pickle.dump(params, f)
|
||||
if outputs_def:
|
||||
outputs = []
|
||||
for output_def in outputs_def:
|
||||
output = pipeline.last_run.logs(output_def["step"])
|
||||
getter = output_def.get("getter", None)
|
||||
if getter and getter in allowed_resultlog_callbacks:
|
||||
output = getattr(resultlog, getter)(output)
|
||||
outputs.append(output)
|
||||
if len(outputs_def) == 1:
|
||||
return outputs[0]
|
||||
return outputs
|
||||
|
||||
def save_func(*args):
|
||||
params = {name: value for name, value in zip(params_name, args)}
|
||||
filename = save_dir / f"{int(time.time())}.pkl"
|
||||
with open(filename, "wb") as f:
|
||||
pickle.dump(params, f)
|
||||
gr.Info("Params saved")
|
||||
|
||||
data = load_saved_params(str(save_dir))
|
||||
return pd.DataFrame(data)
|
||||
|
||||
def load_params_func():
|
||||
data = load_saved_params(str(save_dir))
|
||||
return pd.DataFrame(data)
|
||||
|
||||
def activate_params_func(ev: gr.SelectData, *args):
|
||||
data = load_saved_params(str(save_dir))
|
||||
output_args = [each for each in args]
|
||||
if ev.value is None:
|
||||
gr.Info(f'Blank value: "{ev.value}". Skip')
|
||||
return output_args
|
||||
|
||||
column = list(data.keys())[ev.index[1]]
|
||||
|
||||
if column not in params_name:
|
||||
gr.Info(f'Column "{column}" not in params. Skip')
|
||||
return output_args
|
||||
|
||||
value = data[column][ev.index[0]]
|
||||
if value is None:
|
||||
gr.Info(f'Blank value: "{ev.value}". Skip')
|
||||
return output_args
|
||||
|
||||
output_args[params_name.index(column)] = value
|
||||
|
||||
return output_args
|
||||
|
||||
def export_func():
|
||||
name = (
|
||||
f"{pipeline_def.__module__}.{pipeline_def.__name__}_{datetime.now()}.xlsx"
|
||||
)
|
||||
path = str(exported_dir / name)
|
||||
gr.Info(f"Begin exporting {name}...")
|
||||
try:
|
||||
export(config=config, pipeline_def=pipeline_def, output_path=path)
|
||||
except Exception as e:
|
||||
raise gr.Error(f"Failed to export. Please contact project's AIR: {e}")
|
||||
gr.Info(f"Exported {name}. Please go to the `Exported file` tab to download")
|
||||
return path
|
||||
|
||||
return construct_pipeline_ui(
|
||||
config, run_func, save_func, load_params_func, activate_params_func, export_func
|
||||
)
|
Reference in New Issue
Block a user