import os import re import time import threading import torch import pandas as pd import gradio as gr from difflib import SequenceMatcher from peft import PeftModel from huggingface_hub import snapshot_download from transformers import AutoModelForCausalLM, AutoTokenizer # --- 1. Configuration --- BASE_MODEL_ID = "unsloth/Meta-Llama-3.1-8B-Instruct" MAX_OPTIONS = 8 # Retained models after the literature-agent assessment: # Base, DPO-only, DA-DPO, and TuluCore. ADAPTER_REPO_ID = "starfriend/WaterScopeAI-Adapters" ADAPTER_SUBFOLDERS = { "da_it": "DA-IT", "dpo": "DPO", "da_dpo": "DA-DPO", "tulucore": "DA/Tulucore", } MODEL_DISPLAY_NAMES = { "base": "Base", "da_it": "DA-IT", "dpo": "DPO-only", "da_dpo": "DA-DPO", "tulucore": "TuluCore", } MCQA_MODEL_ORDER = ["da_it", "da_dpo"] CHAT_MODEL_ORDER = ["base", "dpo", "da_dpo", "tulucore"] AGENT_MODEL_ORDER = ["base", "dpo", "da_dpo", "tulucore"] DEFAULT_CHAT_MODEL = "da_dpo" DEFAULT_AGENT_MODEL = "base" DATA_PATH = os.path.join("Testing MCQA data", "Decarbonization_MCQA.csv") # --- 2. Load dataset --- try: MCQA_DF = pd.read_csv(DATA_PATH, encoding="utf-8") except UnicodeDecodeError: MCQA_DF = pd.read_csv(DATA_PATH, encoding="latin1") # Ensure only Question + A-D columns MCQA_DF = MCQA_DF[["Question", "A", "B", "C", "D"]] # --- 3. Lazy Loading for Models --- _model = None _tokenizer = None # Generation is serialized because PEFT adapters are model-global state. _generation_lock = threading.Lock() def download_required_adapters(): """Download only the four required PEFT adapters from the Hub.""" hf_token = os.getenv("HF_TOKEN") allow_patterns = [] for subfolder in ADAPTER_SUBFOLDERS.values(): allow_patterns.extend( [ f"{subfolder}/adapter_config.json", f"{subfolder}/adapter_model.safetensors", ] ) local_repo_path = snapshot_download( repo_id=ADAPTER_REPO_ID, repo_type="model", revision="main", token=hf_token, allow_patterns=allow_patterns, ) adapter_paths = {} for adapter_name, subfolder in ADAPTER_SUBFOLDERS.items(): adapter_path = os.path.join(local_repo_path, *subfolder.split("/")) config_path = os.path.join(adapter_path, "adapter_config.json") weights_path = os.path.join(adapter_path, "adapter_model.safetensors") if not os.path.isfile(config_path): raise FileNotFoundError( f"{adapter_name} adapter_config.json was not found at " f"{config_path}. Check the repository structure and HF_TOKEN." ) if not os.path.isfile(weights_path): raise FileNotFoundError( f"{adapter_name} adapter weights were not found at " f"{weights_path}." ) adapter_paths[adapter_name] = adapter_path return adapter_paths def load_model_and_tokenizer(): """Load the base model and the three retained adapters.""" global _model, _tokenizer if _model is not None and _tokenizer is not None: return _model, _tokenizer print("Initializing WaterScope-AI models...") if not torch.cuda.is_available(): raise RuntimeError("No CUDA GPU detected. This Space requires a GPU.") _tokenizer = AutoTokenizer.from_pretrained( BASE_MODEL_ID, use_fast=True, ) if _tokenizer.pad_token_id is None: _tokenizer.pad_token = _tokenizer.eos_token base_model = AutoModelForCausalLM.from_pretrained( BASE_MODEL_ID, torch_dtype=torch.bfloat16, device_map="auto", low_cpu_mem_usage=True, ) base_model.eval() print("Base model loaded.") adapter_paths = download_required_adapters() first_adapter = "da_it" _model = PeftModel.from_pretrained( base_model, adapter_paths[first_adapter], adapter_name=first_adapter, is_trainable=False, ) print(f"DA-IT adapter loaded from: {adapter_paths[first_adapter]}") for adapter_name in ("dpo", "da_dpo", "tulucore"): _model.load_adapter( adapter_paths[adapter_name], adapter_name=adapter_name, is_trainable=False, ) print( f"{MODEL_DISPLAY_NAMES[adapter_name]} adapter loaded from: " f"{adapter_paths[adapter_name]}" ) _model.set_adapter(DEFAULT_CHAT_MODEL) _model.eval() print("Available adapters:", list(_model.peft_config.keys())) print("MCQA models:", [MODEL_DISPLAY_NAMES[name] for name in MCQA_MODEL_ORDER]) print("Chat models:", [MODEL_DISPLAY_NAMES[name] for name in CHAT_MODEL_ORDER]) print("Agent models:", [MODEL_DISPLAY_NAMES[name] for name in AGENT_MODEL_ORDER]) return _model, _tokenizer def activate_model(model_name): """Activate a retained adapter or temporarily disable adapters for Base.""" all_models = ["base"] + list(ADAPTER_SUBFOLDERS.keys()) if model_name not in all_models: raise ValueError( f"Unknown model '{model_name}'. Choose from: {', '.join(all_models)}" ) if model_name == "base": return _model.disable_adapter() _model.set_adapter(model_name) return None # --- 4. Utility Functions --- def extract_letter(raw_answer: str) -> str: """Extract predicted option letter from model output""" # Priority 1: Look for explicit phrases like "answer is B" match = re.search(r"(?:answer|option) is\s+([A-H])", raw_answer, re.IGNORECASE) if match: return match.group(1).upper() # Priority 2: Look for formats like "B." or "B)" at the start match = re.search(r"^\s*([A-H])[\.\):]", raw_answer) if match: return match.group(1).upper() # Priority 3: Look for the first standalone letter in the text match = re.search(r"\b([A-H])\b", raw_answer) if match: return match.group(1).upper() return "N/A" def clean_repetitions(text: str) -> str: lines = [l.strip() for l in text.strip().splitlines() if l.strip()] if not lines: return "" # split into words (keep punctuation as part of word) def tokenize(line): return re.findall(r"\S+", line) result = tokenize(lines[0]) for line in lines[1:]: tokens = tokenize(line) # find overlap i = 0 while i < len(result) and i < len(tokens) and result[i].rstrip(".,!?") == tokens[i].rstrip(".,!?"): i += 1 # append only the non-overlapping part result.extend(tokens[i:]) return " ".join(result) # Global variable to track cancellation cancellation_requested = False def run_mcqa_comparison( question, opt_a, opt_b, opt_c, opt_d, opt_e, opt_f, opt_g, opt_h, generate_explanation, ): """Run the original MCQA comparison with DA-IT and DA-DPO.""" global _model, _tokenizer, cancellation_requested cancellation_requested = False if _model is None or _tokenizer is None: gr.Info("Initializing models for the first time...") load_model_and_tokenizer() options = [opt_a, opt_b, opt_c, opt_d, opt_e, opt_f, opt_g, opt_h] active_options = [opt for opt in options if opt and opt.strip()] if not question or len(active_options) < 2: yield ( "Error", "Please enter a question and at least two options.", "Error", "Please enter a question and at least two options.", ) return option_labels = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" option_text = "\n".join( f"{option_labels[i]}. {value}" for i, value in enumerate(active_options) ) if generate_explanation: instruction = ( "Provide the letter first, followed by a concise expert explanation " "in the form: 'The answer is [LETTER]. Because ...'" ) max_tokens = 220 else: instruction = "Return only the letter of the best answer." max_tokens = 30 messages = [ { "role": "system", "content": ( "You are an expert in water and wastewater treatment, " "decarbonization, emissions, resource recovery, and " "environmental sustainability. Answer the multiple-choice " f"question accurately. {instruction}" ), }, { "role": "user", "content": f"Question: {question}\nCandidate options:\n{option_text}", }, ] chat_input = _tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, ) inputs = safe_tokenize( chat_input, _tokenizer, _model, max_new_tokens=max_tokens, ) def generate_for(model_name): if cancellation_requested: raise gr.Error("Processing cancelled by user") generation_kwargs = dict( **inputs, max_new_tokens=max_tokens, eos_token_id=_tokenizer.eos_token_id, pad_token_id=_tokenizer.pad_token_id, do_sample=False, use_cache=True, ) if model_name == "da_dpo": generation_kwargs.update( repetition_penalty=1.10, no_repeat_ngram_size=4, ) with torch.inference_mode(): _model.set_adapter(model_name) outputs = _model.generate(**generation_kwargs) generated_ids = outputs[0][inputs["input_ids"].shape[1]:] response = _tokenizer.decode( generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False, ).strip() return clean_repetitions(response) try: with _generation_lock: yield "", "Running DA-IT...", "", "" da_it_raw = generate_for("da_it") da_it_letter = extract_letter(da_it_raw) yield da_it_letter, da_it_raw, "", "Running DA-DPO..." da_dpo_raw = generate_for("da_dpo") da_dpo_letter = extract_letter(da_dpo_raw) yield da_it_letter, da_it_raw, da_dpo_letter, da_dpo_raw except gr.Error as exc: if "cancelled" in str(exc).lower(): gr.Info("Processing cancelled by user") return raise # Function to handle cancellation def cancel_processing(): global cancellation_requested cancellation_requested = True return "Cancellation requested" # Safe tokenization wrapper def safe_tokenize(chat_input, _tokenizer, _model, max_new_tokens=1600): # 1. Validate input type if not isinstance(chat_input, str) or len(chat_input.strip()) == 0: raise ValueError("chat_input must be a non-empty string") # 2. Sanitize weird characters (e.g., emojis, zero-width spaces) clean_input = re.sub(r"[^\x00-\x7F]+", " ", chat_input) # 3. Tokenize with truncation to avoid position limit issues max_input_tokens = ( _model.config.max_position_embeddings - max_new_tokens - 100 ) tokens = _tokenizer( clean_input, return_tensors="pt", truncation=True, max_length=max_input_tokens ) # 4. Validate token IDs vocab_size = _model.get_input_embeddings().weight.shape[0] max_id = tokens["input_ids"].max().item() min_id = tokens["input_ids"].min().item() print(f"[DEBUG] chat_input: {repr(chat_input)}") print(f"[DEBUG] sanitized_input: {repr(clean_input)}") print(f"[DEBUG] token IDs min: {min_id}, max: {max_id}, vocab size: {vocab_size}") if max_id >= vocab_size or min_id < 0: raise ValueError(f"Token IDs out of range: min {min_id}, max {max_id}, vocab size {vocab_size}") # 5. Move tokens to model device tokens = {k: v.to(_model.device) for k, v in tokens.items() if isinstance(v, torch.Tensor)} return tokens # General chat function def chat_with_model( message, history, selected_model=DEFAULT_CHAT_MODEL, max_new_tokens=900, ): """General conversational QA while preserving multi-turn history.""" global _model, _tokenizer if _model is None or _tokenizer is None: gr.Info("Initializing models for the first time...") load_model_and_tokenizer() if not isinstance(message, str) or not message.strip(): return "Please provide a non-empty message." selected_model = selected_model or DEFAULT_CHAT_MODEL if selected_model not in CHAT_MODEL_ORDER: raise gr.Error( "Chat supports Base, DPO-only, DA-DPO, and TuluCore." ) max_new_tokens = max(64, min(int(max_new_tokens), 1600)) system_prompt = ( """ You are an expert AI assistant in water and wastewater engineering. When answering a question: - First, write down all relevant facts or values. - Next, identify which one is correct based on those facts for factual comparisons or multiple options. - Next, provide a clear description for conceptual definition questions. - Finally, clearly state your conclusion in this format: [Main answer]. [one or two sentences explaining the reasoning]. [appropriate values, equations to support the reasoning]. """ ) messages = [{"role": "system", "content": system_prompt}] messages.extend(normalize_chat_history(history)) messages.append({"role": "user", "content": message.strip()}) return generate_chat_response( messages=messages, selected_model=selected_model, max_new_tokens=max_new_tokens, ) AGENT_APPLICATIONS = { "general_qa": { "label": "General environmental QA", "instruction": ( "Answer the technical question directly. Distinguish facts, " "assumptions, and recommendations." ), }, "document_qa": { "label": "Document-grounded QA", "instruction": ( "Use the supplied context as the only source for document-specific " "claims. State clearly when the context does not support an answer." ), }, "paper_synthesis": { "label": "Multi-paper synthesis", "instruction": ( "Compare the supplied papers with explicit source attribution. " "Preserve key methods and quantitative findings and do not mix " "evidence among papers." ), }, "method_recommendation": { "label": "Method recommendation", "instruction": ( "Recommend a method using explicit evidence, implementation " "constraints, limitations, and uncertainty. Do not claim superiority " "unless the supplied evidence directly supports it." ), }, "research_gaps": { "label": "Research-gap identification", "instruction": ( "Identify evidence-grounded research gaps. Link every gap to a " "specific limitation or unresolved issue in the supplied context." ), }, "claim_check": { "label": "Unsupported-claim check", "instruction": ( "Evaluate whether the requested claim is supported. Refuse to invent " "a paper, method, value, causal relationship, or percentage." ), }, } def normalize_chat_history(history): """Convert Gradio messages or legacy tuples into chat-template messages.""" normalized = [] for item in history or []: if isinstance(item, dict): role = item.get("role") content = item.get("content") if isinstance(content, list): parts = [] for part in content: if isinstance(part, dict) and part.get("type") == "text": if part.get("text"): parts.append(str(part["text"])) elif isinstance(part, str): parts.append(part) content = "\n".join(parts) if role in {"user", "assistant"} and content: normalized.append({"role": role, "content": str(content)}) elif isinstance(item, (list, tuple)) and len(item) >= 2: user_message, assistant_message = item[0], item[1] if user_message: normalized.append( {"role": "user", "content": str(user_message)} ) if assistant_message: normalized.append( {"role": "assistant", "content": str(assistant_message)} ) return normalized def generate_chat_response(messages, selected_model, max_new_tokens): """Shared generation path for Chat and Agent.""" chat_input = _tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, ) inputs = safe_tokenize( chat_input, _tokenizer, _model, max_new_tokens=max_new_tokens, ) generation_kwargs = dict( **inputs, max_new_tokens=max_new_tokens, do_sample=False, eos_token_id=_tokenizer.eos_token_id, pad_token_id=_tokenizer.pad_token_id, use_cache=True, ) # The benchmark showed that DA-DPO and TuluCore benefit from modest # repetition control for longer, open-ended generations. if selected_model in {"da_dpo", "tulucore"}: generation_kwargs.update( repetition_penalty=1.10, no_repeat_ngram_size=4, ) with _generation_lock: try: if selected_model == "base": with _model.disable_adapter(): outputs = _model.generate(**generation_kwargs) else: _model.set_adapter(selected_model) outputs = _model.generate(**generation_kwargs) except Exception as exc: print( f"[ERROR] Generation failed for {selected_model}: {exc}", flush=True, ) raise gr.Error( f"{MODEL_DISPLAY_NAMES.get(selected_model, selected_model)} " f"could not generate a response: {exc}" ) input_length = inputs["input_ids"].shape[1] generated_ids = outputs[0][input_length:] return _tokenizer.decode( generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False, ).strip() def agent_run( selected_model, application, context, question, max_new_tokens=900, ): """Run a selectable WaterScope agent application.""" global _model, _tokenizer if _model is None or _tokenizer is None: gr.Info("Initializing models for the first time...") load_model_and_tokenizer() if selected_model not in AGENT_MODEL_ORDER: raise gr.Error( "Agent applications support Base, DPO-only, DA-DPO, and TuluCore." ) if application not in AGENT_APPLICATIONS: raise gr.Error("Select a valid agent application.") if not isinstance(question, str) or not question.strip(): raise gr.Error("Enter a question or task.") max_new_tokens = max(64, min(int(max_new_tokens), 1600)) application_config = AGENT_APPLICATIONS[application] system_prompt = f""" You are WaterScope-AI, an expert scientific agent for water, wastewater, environmental engineering, and sustainability. Application: {application_config["label"]} Task behavior: {application_config["instruction"]} Core requirements: - Answer the user's actual task directly. - Preserve important quantitative values and units. - Attribute document-specific evidence clearly. - Separate evidence from inference and recommendation. - Never invent papers, citations, numerical values, standards, or findings. - When evidence is insufficient, say exactly what cannot be concluded. - Avoid repeated sentences or sections and stop when complete. """.strip() context = (context or "").strip() user_content = question.strip() if context: user_content = ( "SUPPLIED CONTEXT\n" "================\n" f"{context}\n\n" "USER TASK\n" "=========\n" f"{question.strip()}" ) messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_content}, ] started = time.time() response = generate_chat_response( messages=messages, selected_model=selected_model, max_new_tokens=max_new_tokens, ) elapsed = time.time() - started diagnostics = ( f"Model: {MODEL_DISPLAY_NAMES[selected_model]}\n" f"Application: {application_config['label']}\n" f"Context characters: {len(context):,}\n" f"Response characters: {len(response):,}\n" f"Generation time: {elapsed:.1f} s" ) return response, diagnostics # Backward-compatible API endpoint used by prior local scripts. def agent_chat_with_model( message, chat_history=None, max_new_tokens=600, selected_model=DEFAULT_AGENT_MODEL, ): messages = [ { "role": "system", "content": ( "You are WaterScope-AI, an expert assistant in water and " "wastewater engineering. Answer accurately, remain grounded, " "and do not invent evidence." ), } ] messages.extend(normalize_chat_history(chat_history)) messages.append({"role": "user", "content": str(message)}) return generate_chat_response( messages=messages, selected_model=selected_model, max_new_tokens=max(64, min(int(max_new_tokens), 1200)), ) # Custom CSS for website-like appearance with lighter blue header custom_css = """ .gradio-container { max-width: 1200px !important; margin: 0 auto !important; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important; } .header { text-align: center; padding: 20px; background: linear-gradient(135deg, #6eb1ff 0%, #88d3fe 100%); color: white; border-radius: 8px; margin-bottom: 20px; } .header h1 { margin: 0; font-size: 2.5em; font-weight: 600; } .header p { margin: 10px 0 0; font-size: 1.2em; opacity: 0.9; } .section { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); margin-bottom: 20px; } .nav-bar { margin-bottom: 20px; display: flex; justify-content: center; gap: 10px; } .footer { text-align: center; padding: 15px; margin-top: 30px; color: #666; font-size: 0.9em; border-top: 1px solid #eee; } .dataframe-container { margin-top: 20px; } .model-output { background: #f8f9fa; padding: 15px; border-radius: 8px; border-left: 4px solid #6eb1ff; } .model-output h4 { margin-top: 0; color: #6eb1ff; } .option-controls { margin-top: 15px; display: flex; gap: 10px; } .cancel-btn { background: #f39c12 !important; color: white !important; } .cancel-btn:hover { background: #e67e22 !important; } .status-message { padding: 10px; border-radius: 4px; margin: 10px 0; } .status-info { background-color: #e3f2fd; border-left: 4px solid #2196f3; } .status-warning { background-color: #fff3e0; border-left: 4px solid #ff9800; } .status-error { background-color: #ffebee; border-left: 4px solid #f44336; } .status-success { background-color: #e8f5e9; border-left: 4px solid #4caf50; } /* Chat specific styles */ .chat-container { display: flex; flex-direction: column; height: 500px; } .chat-messages { flex: 1; overflow-y: auto; padding: 15px; background: var(--light); border-radius: 6px; margin-bottom: 15px; display: flex; flex-direction: column; gap: 15px; } .message { display: flex; max-width: 80%; } .user-message { align-self: flex-end; } .bot-message { align-self: flex-start; } .message-content { padding: 12px 16px; border-radius: 18px; line-height: 1.4; } .user-message .message-content { background: var(--accent); color: white; border-bottom-right-radius: 4px; } .bot-message .message-content { background: var(--light-gray); color: var(--dark); border-bottom-left-radius: 4px; } .chat-input-container { display: flex; gap: 10px; } .chat-input-container textarea { flex: 1; padding: 12px; border: 1px solid var(--border); border-radius: 6px; resize: vertical; font-family: inherit; font-size: 14px; } """ # --- 5. Gradio UI --- with gr.Blocks( title="WaterScope-AI", fill_width=True, ) as demo: # Custom Header with lighter blue with gr.Column(elem_classes="header"): gr.Markdown("WaterScope-AI") gr.Markdown("Domain-Specific Language Models and Agent Applications for Water Sustainability") # Navigation Bar with gr.Row(elem_classes="nav-bar"): gr.Button("Home", variant="secondary", size="sm") gr.Button("About", variant="secondary", size="sm") gr.Button("Documentation", variant="secondary", size="sm") gr.Button("Contact", variant="secondary", size="sm") # Create tabs for different functionalities with gr.Tabs(): # MCQA Demo Tab with gr.TabItem("MCQA Demo"): # Status message area status_message = gr.HTML("", elem_classes="status-message") # Main content in a styled section with gr.Column(elem_classes="section"): # State for tracking number of visible options num_options_state = gr.State(4) # Top row with input and output panels with gr.Row(): # Left panel with inputs with gr.Column(scale=1): with gr.Group(): question_box = gr.Textbox(label="Question", lines=2, interactive=True) gr.Markdown("#### Options") # Create option boxes using a list (like in the working version) option_boxes = [] for i in range(MAX_OPTIONS): option_boxes.append(gr.Textbox( label=f"Option {chr(ord('A') + i)}", visible=(i < 4), interactive=True )) with gr.Row(): add_option_btn = gr.Button("Add Option") clear_btn = gr.Button("Clear") explanation_checkbox = gr.Checkbox(label="Generate Explanation", value=False) with gr.Row(): run_btn = gr.Button("Run Comparison", variant="primary") cancel_btn = gr.Button("Cancel", variant="stop", visible=False, elem_classes="cancel-btn") # Right panel with outputs with gr.Column(scale=1): gr.Markdown("### Model Outputs") with gr.Row(): with gr.Column(elem_classes="model-output"): gr.Markdown("#### DA-IT Model") da_it_letter_box = gr.Textbox( label="Predicted Letter", interactive=False, ) da_it_raw_box = gr.Textbox( label="Raw Answer", lines=3, interactive=False, ) with gr.Column(elem_classes="model-output"): gr.Markdown("#### DA-DPO Model") da_dpo_letter_box = gr.Textbox( label="Predicted Letter", interactive=False, ) da_dpo_raw_box = gr.Textbox( label="Raw Answer", lines=3, interactive=False, ) # Table section with custom styling with gr.Column(elem_classes="section dataframe-container"): gr.Markdown("### Browse 777 MCQAs (click a row to autofill)") mcqa_table = gr.Dataframe( value=MCQA_DF.values.tolist(), headers=["Question", "A", "B", "C", "D"], datatype=["str"]*5, interactive=False, wrap=True, max_height=400 ) # Chat Tab with gr.TabItem("Chat"): with gr.Column(elem_classes="section"): gr.Markdown( "### General Chat\n" "Use any retained model for conversational water and " "environmental engineering questions." ) with gr.Row(): chat_model = gr.Dropdown( choices=[ (MODEL_DISPLAY_NAMES[name], name) for name in CHAT_MODEL_ORDER ], value=DEFAULT_CHAT_MODEL, label="Chat model", ) chat_max_tokens = gr.Slider( minimum=128, maximum=1600, value=900, step=64, label="Maximum new tokens", ) with gr.Row(): system_status = gr.Textbox( value="Ready", label="System status", interactive=False, ) api_status = gr.Textbox( value="Ready", label="Generation status", interactive=False, ) chatbot = gr.Chatbot( label="Conversation", elem_classes="chat-messages", height=430, ) with gr.Row(): msg = gr.Textbox( label="Your message", placeholder="Ask a water or environmental engineering question...", lines=3, scale=5, ) send_btn = gr.Button( "Send", variant="primary", scale=1, ) clear_chat = gr.Button("Clear conversation") # Agent Applications Tab with gr.TabItem("Agent Applications"): with gr.Column(elem_classes="section"): gr.Markdown( "### Test WaterScope-AI across agent applications\n" "Select a model and application, provide optional source " "material, and submit a task." ) with gr.Row(): agent_model = gr.Dropdown( choices=[ (MODEL_DISPLAY_NAMES[name], name) for name in AGENT_MODEL_ORDER ], value=DEFAULT_AGENT_MODEL, label="Agent model", ) agent_application = gr.Dropdown( choices=[ (config["label"], key) for key, config in AGENT_APPLICATIONS.items() ], value="document_qa", label="Application", ) agent_max_tokens = gr.Slider( minimum=128, maximum=1600, value=900, step=64, label="Maximum new tokens", ) agent_context = gr.Textbox( label="Optional context or documents", placeholder=( "Paste one or more paper summaries, regulatory passages, " "process data, or other source material here." ), lines=14, ) agent_question = gr.Textbox( label="Question or task", placeholder=( "Example: Compare the treatment methods and recommend " "the most defensible option based only on the context." ), lines=4, ) with gr.Row(): agent_run_button = gr.Button( "Run Agent", variant="primary", ) agent_clear_button = gr.Button("Clear") agent_output = gr.Textbox( label="Agent response", lines=18, interactive=False, ) agent_diagnostics = gr.Textbox( label="Run diagnostics", lines=5, interactive=False, ) # Backward-compatible hidden endpoint: /agent_chat agent_api_message = gr.Textbox(visible=False) agent_api_history = gr.JSON(value=[], visible=False) agent_api_max_tokens = gr.Number(value=600, precision=0, visible=False) agent_api_model = gr.Dropdown( choices=AGENT_MODEL_ORDER, value=DEFAULT_AGENT_MODEL, visible=False, ) agent_api_output = gr.Textbox(visible=False) agent_api_trigger = gr.Button("Agent Chat API", visible=False) agent_api_trigger.click( fn=agent_chat_with_model, inputs=[ agent_api_message, agent_api_history, agent_api_max_tokens, agent_api_model, ], outputs=agent_api_output, api_name="agent_chat", ) # Structured agent endpoint: /agent_run agent_run_button.click( fn=agent_run, inputs=[ agent_model, agent_application, agent_context, agent_question, agent_max_tokens, ], outputs=[agent_output, agent_diagnostics], api_name="agent_run", ) agent_clear_button.click( fn=lambda: ("", "", "", ""), inputs=None, outputs=[ agent_context, agent_question, agent_output, agent_diagnostics, ], queue=False, ) # Footer with gr.Column(elem_classes="footer"): gr.Markdown("© 2025 WaterScope-AI | Built with Gradio") # Function to add more options def add_option(current_count): if current_count < MAX_OPTIONS: current_count += 1 updates = [gr.update(visible=i < current_count) for i in range(MAX_OPTIONS)] return current_count, *updates # Function to clear all inputs and outputs (from working version) def clear_all(): """Clear all MCQA inputs, outputs, and option visibility states.""" option_visibility_updates = [ gr.update(visible=(i < 4), value="") for i in range(MAX_OPTIONS) ] return ( 4, # Reset the visible-option count "", # Clear the question *[""] * MAX_OPTIONS, # Clear option values False, # Uncheck explanation "", "", "", "", # Clear DA-IT and DA-DPO outputs *option_visibility_updates, ) # Fixed function to load row data def load_row(evt: gr.SelectData): """Load a selected row from the dataframe into the input fields""" if evt.index[0] >= len(MCQA_DF): return ["", ""] + [""] * MAX_OPTIONS row = MCQA_DF.iloc[evt.index[0]] # Return question and first 4 options (A-D), and empty for the rest return_values = [ row["Question"] if pd.notna(row["Question"]) else "", row["A"] if pd.notna(row["A"]) else "", row["B"] if pd.notna(row["B"]) else "", row["C"] if pd.notna(row["C"]) else "", row["D"] if pd.notna(row["D"]) else "" ] # Add empty values for any additional options return_values += [""] * (MAX_OPTIONS - 4) return return_values # Function to toggle cancel button visibility def toggle_cancel_button(): return gr.update(visible=True) # Function to hide cancel button def hide_cancel_button(): return gr.update(visible=False) # Function to update status message def update_status(message, type="info"): if type == "info": cls = "status-info" elif type == "warning": cls = "status-warning" elif type == "error": cls = "status-error" elif type == "success": cls = "status-success" else: cls = "status-info" return f'
{message}
' # Connect the table selection event mcqa_table.select( fn=load_row, inputs=None, outputs=[question_box, *option_boxes] ) # Connect the add option button add_option_btn.click( fn=add_option, inputs=[num_options_state], outputs=[num_options_state, *option_boxes] ) # Define the MCQA components cleared by the Clear button. outputs_to_clear = [ num_options_state, question_box, *option_boxes, explanation_checkbox, da_it_letter_box, da_it_raw_box, da_dpo_letter_box, da_dpo_raw_box, *option_boxes, ] # Connect the clear button (from working version) clear_btn.click( fn=clear_all, inputs=None, outputs=outputs_to_clear, queue=False ).then( fn=lambda: update_status("Form cleared", "info"), inputs=None, outputs=[status_message], queue=False ) # Connect the run button run_event = run_btn.click( fn=lambda: update_status("Initializing processing...", "info"), inputs=None, outputs=[status_message], queue=False ).then( fn=toggle_cancel_button, inputs=None, outputs=[cancel_btn], queue=False ).then( fn=run_mcqa_comparison, inputs=[question_box, *option_boxes, explanation_checkbox], outputs=[ da_it_letter_box, da_it_raw_box, da_dpo_letter_box, da_dpo_raw_box, ] ).then( fn=lambda: update_status("Processing completed successfully", "success"), inputs=None, outputs=[status_message], queue=False ).then( fn=hide_cancel_button, inputs=None, outputs=[cancel_btn], queue=False ) # Connect the cancel button cancel_btn.click( fn=cancel_processing, inputs=None, outputs=None, queue=False ).then( fn=lambda: update_status("Processing cancelled by user", "warning"), inputs=None, outputs=[status_message], queue=False ).then( fn=hide_cancel_button, inputs=None, outputs=[cancel_btn], queue=False ) # Chat functionality def respond( message, chat_history, selected_model, max_new_tokens, ): chat_history = list(chat_history or []) if not isinstance(message, str) or not message.strip(): return "", chat_history, "Ready", "No message submitted" message = message.strip() try: bot_message = chat_with_model( message=message, history=chat_history, selected_model=selected_model, max_new_tokens=max_new_tokens, ) chat_history.extend( [ {"role": "user", "content": message}, {"role": "assistant", "content": bot_message}, ] ) return "", chat_history, "Ready", ( f"Response generated with " f"{MODEL_DISPLAY_NAMES[selected_model]}" ) except Exception as exc: error_message = f"Sorry, I encountered an error: {exc}" chat_history.extend( [ {"role": "user", "content": message}, {"role": "assistant", "content": error_message}, ] ) return "", chat_history, "Error", str(exc) # Connect the chat send button chat_inputs = [msg, chatbot, chat_model, chat_max_tokens] chat_outputs = [msg, chatbot, system_status, api_status] msg.submit( respond, chat_inputs, chat_outputs, api_name="respond", ) send_btn.click( respond, chat_inputs, chat_outputs, api_name="respond_button", ) # Connect the clear chat button def clear_chat_func(): system_status.value = "Ready" api_status.value = "Ready" return [] clear_chat.click(clear_chat_func, None, chatbot, queue=False) # Pre-load the model when the app starts print("Pre-loading models...") load_model_and_tokenizer() print("Models loaded successfully!") demo.queue(default_concurrency_limit=1).launch( debug=True, show_error=True, theme=gr.themes.Glass(primary_hue="blue"), css=custom_css, )