diff --git a/app/app.py b/app/app.py index caab331..82849ca 100644 --- a/app/app.py +++ b/app/app.py @@ -6,34 +6,19 @@ from mimetypes import guess_type from pathlib import Path from typing import Optional +import json +import spaces import spaces import gradio as gr from feedback import save_feedback, scheduler from gradio.components.chatbot import Option from huggingface_hub import InferenceClient from pandas import DataFrame -from transformers import pipeline, AutoTokenizer, CohereForCausalLM - - -LANGUAGES: dict[str, str] = { - "English": "You are a helpful assistant. Always respond to requests in fluent and natural English, regardless of the language used by the user.", - "Dutch": "Je bent een behulpzame assistent die uitsluitend in het Nederlands communiceert. Beantwoord alle vragen en verzoeken in vloeiend en natuurlijk Nederlands, ongeacht de taal waarin de gebruiker schrijft.", - "Italian": "Sei un assistente utile e rispondi sempre in italiano in modo naturale e fluente, indipendentemente dalla lingua utilizzata dall'utente.", - "Spanish": "Eres un asistente útil que siempre responde en español de manera fluida y natural, independientemente del idioma utilizado por el usuario.", - "French": "Tu es un assistant utile qui répond toujours en français de manière fluide et naturelle, quelle que soit la langue utilisée par l'utilisateur.", - "German": "Du bist ein hilfreicher Assistent, der stets auf Deutsch in einer natürlichen und fließenden Weise antwortet, unabhängig von der Sprache des Benutzers.", - "Portuguese": "Você é um assistente útil que sempre responde em português de forma natural e fluente, independentemente do idioma utilizado pelo usuário.", - "Russian": "Ты полезный помощник, который всегда отвечает на русском языке плавно и естественно, независимо от языка пользователя.", - "Chinese": "你是一个有用的助手,总是用流畅自然的中文回答问题,无论用户使用哪种语言。", - "Japanese": "あなたは役に立つアシスタントであり、常に流暢で自然な日本語で応答します。ユーザーが使用する言語に関係なく、日本語で対応してください。", - "Korean": "당신은 유용한 도우미이며, 항상 유창하고 자연스러운 한국어로 응답합니다. 사용자가 어떤 언어를 사용하든 한국어로 대답하세요.", - "Hebrew": " אתה עוזר טוב ומועיל שמדבר בעברית ועונה בעברית.", - "Hindi" : "आप एक मददगार सहायक हैं। उपयोगकर्ता द्वारा इस्तेमाल की गई भाषा की परवाह किए बिना हमेशा धाराप्रवाह और स्वाभाविक अंग्रेजी में अनुरोधों का जवाब दें।" -} +from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM -BASE_MODEL = os.getenv("MODEL", "meta-llama/Llama-3.2-11B-Vision-Instruct") +BASE_MODEL = os.getenv("MODEL", "CohereForAI/aya-expanse-8b") ZERO_GPU = ( bool(os.getenv("ZERO_GPU", False)) or True if str(os.getenv("ZERO_GPU")).lower() == "true" @@ -48,24 +33,30 @@ def create_inference_client( model: Optional[str] = None, base_url: Optional[str] = None -) -> InferenceClient: +) -> InferenceClient | dict: """Create an InferenceClient instance with the given model or environment settings. This function will run the model locally if ZERO_GPU is set to True. + This function will run the model locally if ZERO_GPU is set to True. Args: model: Optional model identifier to use. If not provided, will use environment settings. + base_url: Optional base URL for the inference API. Returns: - InferenceClient: Configured client instance + Either an InferenceClient instance or a dictionary with pipeline and tokenizer """ if ZERO_GPU: tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL) - model = CohereForCausalLM.from_pretrained(BASE_MODEL, load_in_8bit=True) - return pipeline( - "text-generation", - model=model, - tokenizer=tokenizer, - ) + model = AutoModelForCausalLM.from_pretrained(BASE_MODEL, load_in_8bit=True) + return { + "pipeline": pipeline( + "text-generation", + model=model, + tokenizer=tokenizer, + max_new_tokens=2000, + ), + "tokenizer": tokenizer + } else: return InferenceClient( token=os.getenv("HF_TOKEN"), @@ -77,6 +68,77 @@ def create_inference_client( CLIENT = create_inference_client() +def get_persistent_storage_path(filename: str) -> tuple[Path, bool]: + """Check if persistent storage is available and return the appropriate path. + + Args: + filename: The name of the file to check/create + + Returns: + A tuple containing (file_path, is_persistent) + """ + persistent_path = Path("/data") / filename + local_path = Path(__file__).parent / filename + + # Check if persistent storage is available and writable + use_persistent = False + if Path("/data").exists() and Path("/data").is_dir(): + try: + # Test if we can write to the directory + test_file = Path("/data/write_test.tmp") + test_file.touch() + test_file.unlink() # Remove the test file + use_persistent = True + except (PermissionError, OSError): + print("Persistent storage exists but is not writable, falling back to local storage") + use_persistent = False + + return (persistent_path if use_persistent else local_path, use_persistent) + + +def load_languages() -> dict[str, str]: + """Load languages from JSON file or persistent storage""" + languages_path, use_persistent = get_persistent_storage_path("languages.json") + local_path = Path(__file__).parent / "languages.json" + + # If persistent storage is available but file doesn't exist yet, copy the local file to persistent storage + if use_persistent and not languages_path.exists(): + try: + if local_path.exists(): + import shutil + shutil.copy(local_path, languages_path) + print(f"Copied languages to persistent storage at {languages_path}") + else: + with open(languages_path, "w", encoding="utf-8") as f: + json.dump({"English": "You are a helpful assistant."}, f, ensure_ascii=False, indent=2) + print(f"Created new languages file in persistent storage at {languages_path}") + except Exception as e: + print(f"Error setting up persistent storage: {e}") + languages_path = local_path # Fall back to local path if any error occurs + + if not languages_path.exists() and local_path.exists(): + languages_path = local_path + + if languages_path.exists(): + with open(languages_path, "r", encoding="utf-8") as f: + return json.load(f) + else: + default_languages = {"English": "You are a helpful assistant."} + return default_languages + +LANGUAGES = load_languages() + +USER_AGREEMENT = """ +You have been asked to participate in a research study conducted by Lingo Lab from the Computer Science and Artificial Intelligence Laboratory at the Massachusetts Institute of Technology (M.I.T.), together with huggingface. + +The purpose of this study is the collection of multilingual human feedback to improve language models. As part of this study you will interat with a language model in a langugage of your choice, and provide indication to wether its reponses are helpful or not. + +Your name and personal data will never be recorded. You may decline further participation, at any time, without adverse consequences.There are no foreseeable risks or discomforts for participating in this study. Note participating in the study may pose risks that are currently unforeseeable. If you have questions or concerns about the study, you can contact the researchers at leshem@mit.edu. If you have any questions about your rights as a participant in this research (E-6610), feel you have been harmed, or wish to discuss other study-related concerns with someone who is not part of the research team, you can contact the M.I.T. Committee on the Use of Humans as Experimental Subjects (COUHES) by phone at (617) 253-8420, or by email at couhes@mit.edu. + +Clicking on the next button at the bottom of this page indicates that you are at least 18 years of age and willingly agree to participate in the research voluntarily. +""" + + def add_user_message(history, message): if isinstance(message, dict) and "files" in message: for x in message["files"]: @@ -89,15 +151,14 @@ def add_user_message(history, message): def format_system_message(language: str, history: list): - if history: - if history[0]["role"] == "system": - history = history[1:] system_message = [ { "role": "system", - "content": LANGUAGES[language], + "content": LANGUAGES.get(language, LANGUAGES["English"]), } ] + if history and history[0]["role"] == "system": + history = history[1:] history = system_message + history return history @@ -107,6 +168,11 @@ def format_history_as_messages(history: list): current_role = None current_message_content = [] + if TEXT_ONLY: + for entry in history: + messages.append({"role": entry["role"], "content": entry["content"]}) + return messages + if TEXT_ONLY: for entry in history: messages.append({"role": entry["role"], "content": entry["content"]}) @@ -198,13 +264,29 @@ def add_fake_like_data( @spaces.GPU def call_pipeline(messages: list, language: str): - response = CLIENT( - messages, - clean_up_tokenization_spaces=False, - max_length=2000, - ) - content = response[0]["generated_text"][-1]["content"] - return content + """Call the appropriate model pipeline based on configuration""" + if ZERO_GPU: + tokenizer = CLIENT["tokenizer"] + formatted_prompt = tokenizer.apply_chat_template( + messages, + tokenize=False, + ) + + response = CLIENT["pipeline"]( + formatted_prompt, + clean_up_tokenization_spaces=False, + max_length=2000, + return_full_text=False, + ) + + return response[0]["generated_text"] + else: + response = CLIENT( + messages, + clean_up_tokenization_spaces=False, + max_length=2000, + ) + return response[0]["generated_text"][-1]["content"] def respond( @@ -212,11 +294,12 @@ def respond( language: str, temperature: Optional[float] = None, seed: Optional[int] = None, -) -> list: # -> list: +) -> list: """Respond to the user message with a system message Return the history with the new message""" messages = format_history_as_messages(history) + if ZERO_GPU: content = call_pipeline(messages, language) else: @@ -228,6 +311,7 @@ def respond( temperature=temperature, ) content = response.choices[0].message.content + message = gr.ChatMessage(role="assistant", content=content) history.append(message) return history @@ -339,7 +423,6 @@ def wrangle_edit_data( ) return history else: - # Add feedback on original and corrected message add_fake_like_data( history=history[: index + 1], conversation_id=conversation_id, @@ -354,7 +437,6 @@ def wrangle_edit_data( language=language, ) history = history[: index + 1] - # add chosen and rejected options history[-1]["options"] = [ Option(label="chosen", value=x.value), Option(label="rejected", value=original_message["content"]), @@ -410,6 +492,41 @@ def submit_conversation(dataframe, conversation_id, session_id, language): return (gr.Dataframe(value=None, interactive=False), []) +def open_add_language_modal(): + return gr.Group(visible=True) + +def close_add_language_modal(): + return gr.Group(visible=False) + +def save_new_language(lang_name, system_prompt): + """Save the new language and system prompt to persistent storage if available, otherwise to local file.""" + global LANGUAGES + + languages_path, use_persistent = get_persistent_storage_path("languages.json") + local_path = Path(__file__).parent / "languages.json" + + if languages_path.exists(): + with open(languages_path, "r", encoding="utf-8") as f: + data = json.load(f) + else: + data = {} + + data[lang_name] = system_prompt + + with open(languages_path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + if use_persistent and local_path != languages_path: + try: + with open(local_path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + except Exception as e: + print(f"Error updating local backup: {e}") + + LANGUAGES.update({lang_name: system_prompt}) + return gr.Group(visible=False), gr.HTML(""), gr.Dropdown(choices=list(LANGUAGES.keys())) + + css = """ .options.svelte-pcaovb { display: none !important; @@ -420,85 +537,130 @@ def submit_conversation(dataframe, conversation_id, session_id, language): .retry-btn { display: none !important; } +/* Style for the add language button */ +button#add-language-btn { + padding: 0 !important; + font-size: 30px !important; + font-weight: bold !important; +} +/* Style for the user agreement container */ +.user-agreement-container { + box-shadow: 0 2px 5px rgba(0,0,0,0.1) !important; +} """ with gr.Blocks(css=css) as demo: - ############################## - # Chatbot - ############################## - gr.Markdown(""" - # ♾️ FeeL - a real-time Feedback Loop for LMs - """) + # State variable to track if user has consented + user_consented = gr.State(False) + + # Landing page with user agreement + with gr.Group(visible=True) as landing_page: + gr.Markdown("# Welcome to FeeL") + with gr.Group(elem_classes=["user-agreement-container"]): + gr.Markdown(USER_AGREEMENT) + consent_btn = gr.Button("I agree") + + # Main application interface (initially hidden) + with gr.Group(visible=False) as main_app: + ############################## + # Chatbot + ############################## + gr.Markdown(""" + # ♾️ FeeL - a real-time Feedback Loop for LMs + """) - with gr.Accordion("Explanation") as explanation: - gr.Markdown(f""" - FeeL is a collaboration between Hugging Face and MIT. - It is a community-driven project to provide a real-time feedback loop for VLMs, where your feedback is continuously used to fine-tune the underlying models. - The [dataset](https://huggingface.co/datasets/{scheduler.repo_id}), [code](https://github.com/huggingface/feel) and [models](https://huggingface.co/collections/feel-fl/feel-models-67a9b6ef0fdd554315e295e8) are public. + with gr.Accordion("About") as explanation: + gr.Markdown(f""" + FeeL is a collaboration between Hugging Face and MIT. + It is a community-driven project to provide a real-time feedback loop for VLMs, where your feedback is continuously used to fine-tune the underlying models. + The [dataset](https://huggingface.co/datasets/{scheduler.repo_id}), [code](https://github.com/huggingface/feel) and [models](https://huggingface.co/collections/feel-fl/feel-models-67a9b6ef0fdd554315e295e8) are public. + + Start by selecting your language, chat with the model with text and images and provide feedback in different ways. + + - ✏️ Edit a message + - 👍/👎 Like or dislike a message + - 🔄 Regenerate a message + + """) + + with gr.Column(): + gr.Markdown("Select your language or add a new one:") + with gr.Row(): + language = gr.Dropdown( + choices=list(load_languages().keys()), + container=False, + show_label=False, + scale=8 + ) + add_language_btn = gr.Button( + "+", + elem_id="add-language-btn", + size="sm" + ) + + + # Create a hidden group instead of a modal + with gr.Group(visible=False) as add_language_modal: + gr.Markdown(" Add New Language") + new_lang_name = gr.Textbox(label="Language Name", lines=1) + new_system_prompt = gr.Textbox(label="System Prompt", lines=4) + with gr.Row(): + with gr.Column(scale=1): + save_language_btn = gr.Button("Save") + with gr.Column(scale=1): + cancel_language_btn = gr.Button("Cancel") + + refresh_html = gr.HTML(visible=False) + + session_id = gr.Textbox( + interactive=False, + value=str(uuid.uuid4()), + visible=False, + ) - Start by selecting your language, chat with the model with text and images and provide feedback in different ways. + conversation_id = gr.Textbox( + interactive=False, + value=str(uuid.uuid4()), + visible=False, + ) - - ✏️ Edit a message - - 👍/👎 Like or dislike a message - - 🔄 Regenerate a message + chatbot = gr.Chatbot( + elem_id="chatbot", + editable="all", + bubble_full_width=False, + value=[ + { + "role": "system", + "content": LANGUAGES[language.value], + } + ], + type="messages", + feedback_options=["Like", "Dislike"], + ) - Feedback is automatically submitted allowing you to continue chatting, but you can also submit and reset the conversation by clicking "💾 Submit conversation" (under the chat) or trash the conversation by clicking "🗑️" (upper right corner). - """) - language = gr.Dropdown( - choices=list(LANGUAGES.keys()), label="Language", interactive=True + chat_input = gr.Textbox( + interactive=True, + placeholder="Enter message or upload file...", + show_label=False, + submit_btn=True, ) - with gr.Blocks(css=""" - #add-language-btn { - background: url('os.path.abspath("app/feel-add-icon.png")') no-repeat center; - background-size: contain; - width: 50px; - height: 50px; - border: none; - cursor: pointer; - } -""") as demo: - add_button = gr.Button("", elem_id="add-language-btn") - output = gr.Textbox(label="Status") - - session_id = gr.Textbox( - interactive=False, - value=str(uuid.uuid4()), - visible=False, - ) + with gr.Accordion("Collected feedback", open=False): + dataframe = gr.Dataframe(wrap=True, label="Collected feedback") - conversation_id = gr.Textbox( - interactive=False, - value=str(uuid.uuid4()), - visible=False, - ) + submit_btn = gr.Button(value="💾 Submit conversation", visible=False) - chatbot = gr.Chatbot( - elem_id="chatbot", - editable="all", - bubble_full_width=False, - value=[ - { - "role": "system", - "content": LANGUAGES[language.value], - } - ], - type="messages", - feedback_options=["Like", "Dislike"], - ) + # Function to show main app after consent + def show_main_app(): + return gr.Group(visible=False), gr.Group(visible=True), True - chat_input = gr.Textbox( - interactive=True, - placeholder="Enter message or upload file...", - show_label=False, - submit_btn=True, + # Connect consent button to show main app + consent_btn.click( + fn=show_main_app, + inputs=[], + outputs=[landing_page, main_app, user_consented] ) - with gr.Accordion("Collected feedback", open=False): - dataframe = gr.Dataframe(wrap=True, label="Collected feedback") - - submit_btn = gr.Button(value="💾 Submit conversation", visible=False) - ############################## # Deal with feedback ############################## @@ -553,10 +715,33 @@ def submit_conversation(dataframe, conversation_id, session_id, language): outputs=[conversation_id], ) + def on_app_load(): + global LANGUAGES + LANGUAGES = load_languages() + language_choices = list(LANGUAGES.keys()) + + return str(uuid.uuid4()), gr.Dropdown(choices=language_choices, value=language_choices[0]) + demo.load( - lambda: str(uuid.uuid4()), - inputs=[], - outputs=[session_id], + fn=on_app_load, + inputs=None, + outputs=[session_id, language] + ) + + add_language_btn.click( + fn=lambda: gr.Group(visible=True), + outputs=[add_language_modal] + ) + + cancel_language_btn.click( + fn=lambda: gr.Group(visible=False), + outputs=[add_language_modal] + ) + + save_language_btn.click( + fn=save_new_language, + inputs=[new_lang_name, new_system_prompt], + outputs=[add_language_modal, refresh_html, language] ) demo.launch() diff --git a/app/languages.json b/app/languages.json new file mode 100644 index 0000000..eb608e0 --- /dev/null +++ b/app/languages.json @@ -0,0 +1,12 @@ +{ + "English": "You are a helpful assistant. Always respond to requests in fluent and natural English, regardless of the language used by the user.", + "Dutch": "Je bent een behulpzame assistent die uitsluitend in het Nederlands communiceert. Beantwoord alle vragen en verzoeken in vloeiend en natuurlijk Nederlands, ongeacht de taal waarin de gebruiker schrijft.", + "Italian": "Sei un assistente utile e rispondi sempre in italiano in modo naturale e fluente, indipendentemente dalla lingua utilizzata dall'utente.", + "Spanish": "Eres un asistente útil que siempre responde en español de manera fluida y natural, independientemente del idioma utilizado por el usuario.", + "French": "Tu es un assistant utile qui répond toujours en français de manière fluide et naturelle, quelle que soit la langue utilisée par l'utilisateur.", + "German": "Du bist ein hilfreicher Assistent, der stets auf Deutsch in einer natürlichen und fließenden Weise antwortet, unabhängig von der Sprache des Benutzers.", + "Portuguese": "Você é um assistente útil que sempre responde em português de forma natural e fluente, independentemente do idioma utilizado pelo usuário.", + "Russian": "Ты полезный помощник, который всегда отвечает на русском языке плавно и естественно, независимо от языка пользователя.", + "Chinese": "你是一个有用的助手,总是用流畅自然的中文回答问题,无论用户使用哪种语言。", + "Japanese": "あなたは役に立つアシスタントであり、常に流暢で自然な日本語で応答します。ユーザーが使用する言語に関係なく、日本語で対応してください。" +} \ No newline at end of file