import gradio as gr import torch from transformers import AutoProcessor, AutoModelForCausalLM from PIL import Image import os import spaces # --- Configuration --- hf_token = os.environ.get("HF_TOKEN") model_id = "google/medgemma-4b-it" # --- Model Loading --- if torch.cuda.is_available() and torch.cuda.get_device_capability()[0] >= 8: dtype = torch.bfloat16 else: dtype = torch.float16 model_loaded = False try: processor = AutoProcessor.from_pretrained(model_id, token=hf_token) model = AutoModelForCausalLM.from_pretrained( model_id, token=hf_token, torch_dtype=dtype, device_map="auto", ) model_loaded = True print("Model loaded successfully on device:", model.device) except Exception as e: print(f"Error loading model: {e}") # --- Core Chatbot Function --- @spaces.GPU def symptom_checker_chat(user_input, history_state, new_image_upload, image_state): """ Manages the conversational flow, persisting the image across turns. Args: user_input (str): Text from the user. history_state (list): Stateful conversation history. new_image_upload (PIL.Image): A new image uploaded in this turn. image_state (PIL.Image): The image persisted from a previous turn. Returns: tuple: Updated values for all relevant Gradio components. """ if not model_loaded: history_state.append((user_input, "Error: The model could not be loaded.")) return history_state, history_state, None, None, "" # FIX: Determine which image to use. A new upload takes precedence. # This is the key to solving the "image amnesia" problem. current_image = new_image_upload if new_image_upload is not None else image_state # If this is the *first* turn with an image, add the token. # Don't add it again if the image is just being carried over in the state. if new_image_upload is not None: model_input_text = f"\n{user_input}" else: model_input_text = user_input system_prompt = "You are an expert, empathetic AI medical assistant..." # Keep your detailed prompt # Construct the full conversation history conversation = [{"role": "system", "content": system_prompt}] for turn_input, assistant_output in history_state: conversation.append({"role": "user", "content": turn_input}) if assistant_output: conversation.append({"role": "assistant", "content": assistant_output}) conversation.append({"role": "user", "content": model_input_text}) prompt = processor.apply_chat_template( conversation, tokenize=False, add_generation_prompt=True ) # Process inputs. Crucially, pass `current_image` to the processor. try: if current_image: inputs = processor(text=prompt, images=current_image, return_tensors="pt").to(model.device, dtype) else: inputs = processor(text=prompt, return_tensors="pt").to(model.device, dtype) outputs = model.generate(**inputs, max_new_tokens=512, do_sample=True, temperature=0.7) input_token_len = inputs["input_ids"].shape[1] generated_tokens = outputs[:, input_token_len:] clean_response = processor.decode(generated_tokens[0], skip_special_tokens=True).strip() except Exception as e: print(f"Error during model generation: {e}") clean_response = "An error occurred during generation. Please check the logs." # Update the text history with the model-aware input history_state.append((model_input_text, clean_response)) # Create the display history without the special tokens display_history = [(turn.replace("\n", ""), resp) for turn, resp in history_state] # Return everything: # 1. display_history -> to the chatbot UI # 2. history_state -> to the text history state # 3. current_image -> to the image state to be persisted # 4. None -> to the image upload box to clear it # 5. "" -> to the text box to clear it return display_history, history_state, current_image, None, "" # --- Gradio Interface --- with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}") as demo: gr.Markdown("# AI Symptom Checker powered by MedGemma\n...") # Keep your intro markdown # FIX: Add a new state component to hold the image across turns image_state = gr.State(None) chatbot = gr.Chatbot(label="Conversation", height=500, bubble_full_width=False) chat_history = gr.State([]) with gr.Row(): image_box = gr.Image(type="pil", label="Upload Image of Symptom (Optional)") with gr.Row(): text_box = gr.Textbox(label="Describe your symptoms...", scale=4) submit_btn = gr.Button("Send", variant="primary", scale=1) # FIX: Update the clear function to also clear the new image_state def clear_all(): return [], [], None, None, "" clear_btn = gr.Button("Start New Conversation") clear_btn.click( fn=clear_all, outputs=[chatbot, chat_history, image_state, image_box, text_box], queue=False ) # Combine submit actions into a single function for DRY principle def on_submit(user_input, history, new_image, persisted_image): return symptom_checker_chat(user_input, history, new_image, persisted_image) # FIX: Update inputs and outputs to include the new image_state submit_btn.click( fn=on_submit, inputs=[text_box, chat_history, image_box, image_state], outputs=[chatbot, chat_history, image_state, image_box, text_box] ) text_box.submit( fn=on_submit, inputs=[text_box, chat_history, image_box, image_state], outputs=[chatbot, chat_history, image_state, image_box, text_box] ) if __name__ == "__main__": demo.launch(debug=True)