ling-series-spaces / tab_chat.py
GitHub Action
Sync ling-space changes from GitHub commit 08fb219
875b439
from mimetypes import init
import gradio as gr
import uuid
from datetime import datetime
import pandas as pd
import re
from model_handler import ModelHandler
from config import CHAT_MODEL_SPECS, LING_1T
from recommand_config import get_recommended_inputs
from ui_components.model_selector import create_model_selector
from i18n import get_text
def on_app_load(request: gr.Request, history, conv_id, current_lang_state):
"""
Handles the application's initial state on load.
- Determines language from URL parameter.
- Loads conversation history or creates a new one.
"""
# --- Language Detection ---
query_params = dict(request.query_params)
url_lang = query_params.get("lang")
updated_lang = current_lang_state # Start with the default
if url_lang and url_lang in ["en", "zh"]:
updated_lang = url_lang
# --- History Loading Logic ---
if not history:
# First time ever, create a new conversation
conv_id = str(uuid.uuid4())
new_convo_title = get_text("chat_new_conversation_title", updated_lang)
new_convo = {
"id": conv_id,
"title": new_convo_title,
"messages": [],
"timestamp": datetime.now().isoformat(),
"system_prompt": "",
"model": CHAT_MODEL_SPECS[LING_1T]["display_name"],
"temperature": 0.7
}
history = [new_convo]
return (
conv_id,
history,
gr.update(value=get_history_df(history, updated_lang)),
[],
updated_lang,
)
if conv_id and any(c["id"] == conv_id for c in history):
# Valid last session, load it
for convo in history:
if convo["id"] == conv_id:
return (
conv_id,
history,
gr.update(value=get_history_df(history, updated_lang)),
convo["messages"],
updated_lang,
)
# Fallback to most recent conversation
most_recent_convo = history[0]
return (
most_recent_convo["id"],
history,
gr.update(value=get_history_df(history, updated_lang)),
most_recent_convo["messages"],
updated_lang,
)
def generate_conversation_title(messages, system_prompt):
"""
Generates a conversation title based on a heuristic, defensively handling
multiple possible message formats.
1. Tries to use the first user query.
2. Falls back to the system prompt.
3. Falls back to the current time.
"""
first_query = None
# Rule 1: Try to extract the first user query from various possible formats
if messages:
first_message = messages[0]
# Case 1: List[List[str]] -> [['user', 'assistant'], ...]
if isinstance(first_message, (list, tuple)) and len(first_message) > 0:
first_query = first_message[0]
# Case 2: List[Dict] (OpenAI format or others)
elif isinstance(first_message, dict):
if first_message.get("role") == "user":
first_query = first_message.get("content")
elif "text" in first_message: # Fallback for other observed formats
first_query = first_message["text"]
if first_query and isinstance(first_query, str):
# Split by common Chinese and English punctuation and whitespace
delimiters = r"[,。?!,?!.\s]+"
segments = re.split(delimiters, first_query)
title = ""
for seg in segments:
if seg:
title += seg
if len(title) > 3:
return title[:50] # Limit title length
if title:
return title[:50]
# Rule 2: Use the system prompt
if system_prompt:
return system_prompt[:32]
# Rule 3: Use the current time
return datetime.now().strftime("%H:%M")
def get_history_df(history, lang: str):
"""
Generates a language-aware DataFrame for the conversation history.
"""
if not history:
# Provide explicit column names for an empty DataFrame
return pd.DataFrame({'ID': pd.Series(dtype='str'), get_text('chat_history_dataframe_header', lang): pd.Series(dtype='str')})
df = pd.DataFrame(history)
# Ensure columns exist before renaming
if 'id' in df.columns and 'title' in df.columns:
header_text = get_text('chat_history_dataframe_header', lang)
# Ensure title is a string
df['title'] = df['title'].astype(str)
return df[['id', 'title']].rename(columns={'id': 'ID', 'title': header_text})
else:
return pd.DataFrame({'ID': pd.Series(dtype='str'), get_text('chat_history_dataframe_header', lang): pd.Series(dtype='str')})
def create_chat_tab(initial_lang: str, current_lang_state: gr.State):
model_handler = ModelHandler()
# Browser-side storage for conversation history and current ID
conversation_store = gr.BrowserState(default_value=[], storage_key="ling_conversation_history")
current_conversation_id = gr.BrowserState(default_value=None, storage_key="ling_current_conversation_id")
def handle_new_chat(history, current_conv_id, lang):
current_convo = next((c for c in history if c["id"] == current_conv_id), None) if history else None
if current_convo and not current_convo.get("messages", []):
return current_conv_id, history, [], gr.update(value=get_history_df(history, lang))
conv_id = str(uuid.uuid4())
new_convo_title = get_text('chat_new_conversation_title', lang)
new_convo = {
"id": conv_id, "title": new_convo_title,
"messages": [], "timestamp": datetime.now().isoformat(),
"system_prompt": "",
"model": CHAT_MODEL_SPECS[LING_1T]["display_name"],
"temperature": 0.7
}
updated_history = [new_convo] + (history or [])
return conv_id, updated_history, [], gr.update(value=get_history_df(updated_history, lang))
def load_conversation_from_df(df: pd.DataFrame, evt: gr.SelectData, history, lang):
if evt.index is None or len(df) == 0:
return None, [], "", CHAT_MODEL_SPECS[LING_1T]["display_name"], 0.7, ""
selected_id = df.iloc[evt.index[0]]['ID']
convo = next((c for c in history if c["id"] == selected_id), None)
if convo:
# Use .get() to provide defaults for old conversations
system_prompt = convo.get("system_prompt", "")
model = convo.get("model", CHAT_MODEL_SPECS[LING_1T]["display_name"])
temperature = convo.get("temperature", 0.7)
# Return updates for all components
return selected_id, convo["messages"], system_prompt, model, temperature, ""
# Fallback to creating a new chat if something goes wrong
new_id, _, new_msgs, _ = handle_new_chat(history, None, lang)
return new_id, new_msgs, "", CHAT_MODEL_SPECS[LING_1T]["display_name"], 0.7, ""
with gr.Row(equal_height=False, elem_id="indicator-chat-tab"):
with gr.Column(scale=1):
new_chat_btn = gr.Button(get_text('chat_new_chat_button', initial_lang))
history_df = gr.DataFrame(
value=get_history_df(conversation_store.value, initial_lang),
headers=["ID", get_text('chat_history_dataframe_header', initial_lang)],
datatype=["str", "str"],
interactive=False,
visible=True,
column_widths=["0%", "100%"]
)
with gr.Column(scale=4):
chatbot = gr.Chatbot(height=500, placeholder=get_text('chat_chatbot_placeholder', initial_lang))
with gr.Row():
textbox = gr.Textbox(placeholder=get_text('chat_textbox_placeholder', initial_lang), container=False, scale=7)
submit_btn = gr.Button(get_text('chat_submit_button', initial_lang), scale=1)
recommended_title = gr.Markdown(get_text('chat_recommended_dialogues_title', initial_lang))
recommended_dataset = gr.Dataset(
components=[gr.Textbox(visible=False)],
samples=[[item["task"]] for item in get_recommended_inputs(initial_lang)],
label=get_text('chat_recommended_dataset_label', initial_lang),
headers=[get_text('chat_recommended_dataset_header', initial_lang)],
)
with gr.Column(scale=1):
model_dropdown, model_description_markdown = create_model_selector(
model_specs=CHAT_MODEL_SPECS,
default_model_constant=LING_1T,
lang_state=current_lang_state,
initial_lang=initial_lang
)
system_prompt_textbox = gr.Textbox(label=get_text('chat_system_prompt_label', initial_lang), lines=5, placeholder=get_text('chat_system_prompt_placeholder', initial_lang))
temperature_slider = gr.Slider(minimum=0, maximum=1.0, value=0.7, step=0.1, label=get_text('chat_temperature_slider_label', initial_lang))
# --- Event Handlers --- #
def on_select_recommendation(evt: gr.SelectData, history, current_conv_id, lang):
selected_task = evt.value[0]
item = next((i for i in get_recommended_inputs(lang) if i["task"] == selected_task), None)
if not item:
return gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
new_id, new_history, new_messages, history_df_update = handle_new_chat(history, current_conv_id, lang)
return (
new_id, new_history,
gr.update(value=item["model"]),
gr.update(value=item["system_prompt"]),
gr.update(value=item["temperature"]),
gr.update(value=item["user_message"]),
history_df_update,
new_messages
)
def chat_stream(conv_id, history, model_display_name, message, chat_history, system_prompt, temperature):
if not message:
yield chat_history
return
model_constant = next((k for k, v in CHAT_MODEL_SPECS.items() if v["display_name"] == model_display_name), LING_1T)
response_generator = model_handler.get_response(model_constant, message, chat_history, system_prompt, temperature)
for history_update in response_generator:
yield history_update
def on_chat_stream_complete(conv_id, history, final_chat_history, system_prompt, model_display_name, temperature, lang):
current_convo = next((c for c in history if c["id"] == conv_id), None)
if not current_convo:
return history, gr.update()
# Check if this is the first turn of a new conversation
new_convo_title_default = get_text('chat_new_conversation_title', lang)
is_new_conversation = current_convo["title"] == new_convo_title_default
# If it's a new conversation and we have messages, generate a title and save metadata
if is_new_conversation and len(final_chat_history) > len(current_convo.get("messages", [])):
current_convo["system_prompt"] = system_prompt
current_convo["model"] = model_display_name
current_convo["temperature"] = temperature
new_title = generate_conversation_title(final_chat_history, system_prompt)
current_convo["title"] = new_title
current_convo["messages"] = final_chat_history
current_convo["timestamp"] = datetime.now().isoformat()
history = sorted([c for c in history if c["id"] != conv_id] + [current_convo], key=lambda x: x["timestamp"], reverse=True)
return history, gr.update(value=get_history_df(history, lang))
# Store all components that need i18n updates
components = {
"new_chat_btn": new_chat_btn,
"history_df": history_df,
"chatbot": chatbot,
"textbox": textbox,
"submit_btn": submit_btn,
"recommended_title": recommended_title,
"recommended_dataset": recommended_dataset,
"system_prompt_textbox": system_prompt_textbox,
"temperature_slider": temperature_slider,
"model_dropdown": model_dropdown,
"model_description_markdown": model_description_markdown,
# Non-updatable components needed for event handlers and app.py
"conversation_store": conversation_store,
"current_conversation_id": current_conversation_id,
}
# Wire event handlers
recommended_dataset.select(on_select_recommendation, inputs=[conversation_store, current_conversation_id, current_lang_state], outputs=[current_conversation_id, conversation_store, model_dropdown, system_prompt_textbox, temperature_slider, textbox, history_df, chatbot], show_progress="hidden")
submit_btn.click(
chat_stream,
[current_conversation_id, conversation_store, model_dropdown, textbox, chatbot, system_prompt_textbox, temperature_slider],
[chatbot]
).then(
on_chat_stream_complete,
[current_conversation_id, conversation_store, chatbot, system_prompt_textbox, model_dropdown, temperature_slider, current_lang_state],
[conversation_store, history_df]
)
textbox.submit(
chat_stream,
[current_conversation_id, conversation_store, model_dropdown, textbox, chatbot, system_prompt_textbox, temperature_slider],
[chatbot]
).then(
on_chat_stream_complete,
[current_conversation_id, conversation_store, chatbot, system_prompt_textbox, model_dropdown, temperature_slider, current_lang_state],
[conversation_store, history_df]
)
new_chat_btn.click(handle_new_chat, inputs=[conversation_store, current_conversation_id, current_lang_state], outputs=[current_conversation_id, conversation_store, chatbot, history_df])
history_df.select(load_conversation_from_df, inputs=[history_df, conversation_store, current_lang_state], outputs=[current_conversation_id, chatbot, system_prompt_textbox, model_dropdown, temperature_slider, textbox])
return components
def update_language(lang: str, components: dict):
"""
Returns a dictionary mapping components to their gr.update calls for language change.
"""
updates = {
components["new_chat_btn"]: gr.update(value=get_text('chat_new_chat_button', lang)),
components["history_df"]: gr.update(headers=["ID", get_text('chat_history_dataframe_header', lang)]),
components["chatbot"]: gr.update(placeholder=get_text('chat_chatbot_placeholder', lang)),
components["textbox"]: gr.update(placeholder=get_text('chat_textbox_placeholder', lang)),
components["submit_btn"]: gr.update(value=get_text('chat_submit_button', lang)),
components["recommended_title"]: gr.update(value=get_text('chat_recommended_dialogues_title', lang)),
components["recommended_dataset"]: gr.update(
samples=[[item["task"]] for item in get_recommended_inputs(lang)],
label=get_text('chat_recommended_dataset_label', lang),
headers=[get_text('chat_recommended_dataset_header', lang)],
),
components["system_prompt_textbox"]: gr.update(label=get_text('chat_system_prompt_label', lang), placeholder=get_text('chat_system_prompt_placeholder', lang)),
components["temperature_slider"]: gr.update(label=get_text('chat_temperature_slider_label', lang)),
}
return updates