sdafd commited on
Commit
e59f7e5
·
verified ·
1 Parent(s): 6aba49b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +621 -0
app.py ADDED
@@ -0,0 +1,621 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import google.generativeai as genai
3
+ import os
4
+ import mimetypes
5
+ from PIL import Image
6
+ import io
7
+ import magic # python-magic library
8
+ from dotenv import load_dotenv
9
+
10
+ # (Optional) Load environment variables for local testing if you have a .env file
11
+ # load_dotenv()
12
+ # TEST_API_KEY = os.getenv("GEMINI_API_KEY") # Use this ONLY for your local testing
13
+
14
+ # --- Constants ---
15
+ # Define available models (expand this list as needed)
16
+ # Include models supporting different modalities and versions
17
+ AVAILABLE_MODELS = [
18
+ "gemini-1.5-flash-latest",
19
+ "gemini-1.5-pro-latest",
20
+ "gemini-1.0-pro",
21
+ "gemini-pro-vision", # Example vision model
22
+ # "gemini-experimental", # Add other relevant models
23
+ ]
24
+
25
+ # Define parameters for each model (Example structure)
26
+ # This needs meticulous mapping based on official Gemini documentation
27
+ MODEL_PARAMS = {
28
+ "gemini-1.5-flash-latest": {
29
+ "temperature": {"type": "slider", "min": 0.0, "max": 2.0, "step": 0.1, "default": 1.0},
30
+ "top_p": {"type": "slider", "min": 0.0, "max": 1.0, "step": 0.01, "default": 0.95},
31
+ "top_k": {"type": "slider", "min": 1, "max": 100, "step": 1, "default": 40},
32
+ "max_output_tokens": {"type": "number", "min": 1, "step": 1, "default": 8192},
33
+ "stop_sequences": {"type": "textbox", "lines": 1, "placeholder": "e.g., END,STOP", "default": ""},
34
+ # Safety settings could be added here too (as dropdowns or checkboxes)
35
+ },
36
+ "gemini-1.5-pro-latest": {
37
+ # Similar params, possibly different defaults or ranges
38
+ "temperature": {"type": "slider", "min": 0.0, "max": 2.0, "step": 0.1, "default": 1.0},
39
+ "top_p": {"type": "slider", "min": 0.0, "max": 1.0, "step": 0.01, "default": 0.95},
40
+ "top_k": {"type": "slider", "min": 1, "max": 100, "step": 1, "default": 40},
41
+ "max_output_tokens": {"type": "number", "min": 1, "step": 1, "default": 8192},
42
+ "stop_sequences": {"type": "textbox", "lines": 1, "placeholder": "e.g., END,STOP", "default": ""},
43
+ },
44
+ "gemini-1.0-pro": {
45
+ # Params for older model might differ slightly
46
+ "temperature": {"type": "slider", "min": 0.0, "max": 1.0, "step": 0.1, "default": 0.9}, # Different max/default maybe
47
+ "top_p": {"type": "slider", "min": 0.0, "max": 1.0, "step": 0.01, "default": 0.95},
48
+ "top_k": {"type": "slider", "min": 1, "max": 100, "step": 1, "default": 40},
49
+ "max_output_tokens": {"type": "number", "min": 1, "step": 1, "default": 2048}, # Different default
50
+ "stop_sequences": {"type": "textbox", "lines": 1, "placeholder": "e.g., END,STOP", "default": ""},
51
+ },
52
+ "gemini-pro-vision": {
53
+ # Vision models might have fewer text-generation params or different ones
54
+ "temperature": {"type": "slider", "min": 0.0, "max": 1.0, "step": 0.1, "default": 0.4},
55
+ "top_p": {"type": "slider", "min": 0.0, "max": 1.0, "step": 0.01, "default": 0.95},
56
+ "top_k": {"type": "slider", "min": 1, "max": 100, "step": 1, "default": 32},
57
+ "max_output_tokens": {"type": "number", "min": 1, "step": 1, "default": 2048},
58
+ # No stop sequences typically needed here? Check docs.
59
+ }
60
+ }
61
+
62
+ # --- Helper Functions ---
63
+
64
+ def get_mime_type(file_path):
65
+ """Get MIME type using python-magic for reliability."""
66
+ try:
67
+ mime = magic.Magic(mime=True)
68
+ return mime.from_file(file_path)
69
+ except Exception:
70
+ # Fallback to mimetypes if magic fails
71
+ return mimetypes.guess_type(file_path)[0]
72
+
73
+ def convert_file_to_text(file_obj):
74
+ """
75
+ Attempts to convert various file types to text.
76
+ Returns (text_content, original_filename) or (None, original_filename) if conversion fails.
77
+ """
78
+ file_path = file_obj.name
79
+ filename = os.path.basename(file_path)
80
+ mime_type = get_mime_type(file_path)
81
+ print(f"Processing file: {filename}, MIME type: {mime_type}") # Debugging
82
+
83
+ try:
84
+ if mime_type is None:
85
+ # If MIME type is unknown, try reading as text
86
+ print(f"Warning: Unknown MIME type for {filename}. Attempting to read as text.")
87
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
88
+ return f.read(), filename
89
+ elif mime_type.startswith("text/"):
90
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
91
+ return f.read(), filename
92
+ elif mime_type == "application/pdf":
93
+ # Placeholder for PDF conversion (requires pypdf or similar)
94
+ print(f"PDF conversion not implemented yet for {filename}.")
95
+ # from pypdf import PdfReader # Example
96
+ # reader = PdfReader(file_path)
97
+ # text = ""
98
+ # for page in reader.pages:
99
+ # text += page.extract_text() + "\n"
100
+ # return text, filename
101
+ return f"[Unsupported PDF: {filename} - Conversion not implemented]", filename # Temporary
102
+ elif mime_type in ["application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"]:
103
+ # Placeholder for DOCX conversion (requires python-docx or similar)
104
+ print(f"DOCX conversion not implemented yet for {filename}.")
105
+ # import docx # Example
106
+ # doc = docx.Document(file_path)
107
+ # text = "\n".join([para.text for para in doc.paragraphs])
108
+ # return text, filename
109
+ return f"[Unsupported Word Doc: {filename} - Conversion not implemented]", filename # Temporary
110
+ else:
111
+ # For other unsupported types, return a marker
112
+ print(f"Unsupported file type: {mime_type} for {filename}. Skipping content.")
113
+ return f"[Unsupported file type: {mime_type} - {filename}]", filename
114
+
115
+ except Exception as e:
116
+ print(f"Error converting file {filename}: {e}")
117
+ return f"[Error converting file: {filename}]", filename
118
+
119
+ def prepare_gemini_input(prompt, files):
120
+ """Prepares the input list for Gemini, handling text and images."""
121
+ gemini_parts = []
122
+ if prompt:
123
+ gemini_parts.append(prompt)
124
+
125
+ if files:
126
+ for file_obj in files:
127
+ file_path = file_obj.name
128
+ mime_type = get_mime_type(file_path)
129
+ filename = os.path.basename(file_path)
130
+
131
+ print(f"Preparing file for Gemini: {filename}, MIME: {mime_type}")
132
+
133
+ if mime_type and mime_type.startswith("image/"):
134
+ try:
135
+ img = Image.open(file_path)
136
+ # Convert image to bytes (e.g., PNG or JPEG)
137
+ # Gemini API directly accepts PIL Images usually
138
+ gemini_parts.append(img)
139
+ print(f"Added image: {filename}")
140
+ except Exception as e:
141
+ print(f"Error processing image {filename}: {e}")
142
+ gemini_parts.append(f"[Error processing image: {filename}]")
143
+ elif mime_type and mime_type.startswith("video/"): # Gemini 1.5 Pro can handle video
144
+ # Upload file via File API first (more complex, needs google.ai.generativelanguage)
145
+ # For simplicity here, we'll just note it's a video
146
+ # or provide a basic text representation if conversion isn't implemented
147
+ print(f"Video file detected: {filename}. Full video processing requires File API.")
148
+ gemini_parts.append(f"[Video file: {filename} - Requires File API upload]")
149
+ # Placeholder: Add text conversion if feasible for your use case
150
+ # text_content, _ = convert_file_to_text(file_obj)
151
+ # if text_content:
152
+ # gemini_parts.append(f"--- Content of video file {filename} (extracted as text) ---\n{text_content}")
153
+
154
+ elif mime_type and mime_type.startswith("audio/"): # Gemini 1.5 Pro can handle audio
155
+ print(f"Audio file detected: {filename}. Full audio processing requires File API.")
156
+ gemini_parts.append(f"[Audio file: {filename} - Requires File API upload]")
157
+ # Placeholder: Add text conversion if feasible (e.g. transcript)
158
+ # text_content, _ = convert_file_to_text(file_obj) # Needs specific audio-to-text logic
159
+ # if text_content:
160
+ # gemini_parts.append(f"--- Content of audio file {filename} (extracted as text) ---\n{text_content}")
161
+
162
+ else: # Assume text or convertible to text
163
+ text_content, original_filename = convert_file_to_text(file_obj)
164
+ if text_content:
165
+ # Add context marker
166
+ gemini_parts.append(f"\n--- Content from file: {original_filename} ---\n{text_content}\n--- End of file: {original_filename} ---")
167
+ else:
168
+ gemini_parts.append(f"[Could not process file: {original_filename}]")
169
+
170
+ # Ensure there's at least one part (maybe an empty string if only files were given?)
171
+ if not gemini_parts:
172
+ gemini_parts.append("") # Avoid sending empty list
173
+
174
+ return gemini_parts
175
+
176
+
177
+ # --- Gradio UI Functions ---
178
+
179
+ def validate_api_key(api_key):
180
+ """Checks if the API key is potentially valid by trying to list models."""
181
+ if not api_key:
182
+ return "<p style='color: orange;'>Please enter an API Key.</p>"
183
+ try:
184
+ genai.configure(api_key=api_key)
185
+ models = genai.list_models()
186
+ # Check if at least one desired model is available with this key
187
+ available_core_models = [m.name for m in models if 'generateContent' in m.supported_generation_methods]
188
+ if any(model_name.split('/')[-1] in AVAILABLE_MODELS for model_name in available_core_models):
189
+ return "<p style='color: green;'>API Key seems valid (can list models).</p>"
190
+ else:
191
+ return "<p style='color: orange;'>API Key is valid but might not have access to the required Gemini models.</p>"
192
+
193
+ except Exception as e:
194
+ print(f"API Key validation error: {e}")
195
+ # Be careful not to leak too much error detail
196
+ if "API key not valid" in str(e):
197
+ return "<p style='color: red;'>API Key is invalid.</p>"
198
+ else:
199
+ return f"<p style='color: red;'>API Key validation failed. Error: {str(e)}</p>"
200
+
201
+
202
+ def update_parameter_visibility(model_name):
203
+ """Updates visibility and values of parameter controls based on selected model."""
204
+ updates = {}
205
+ params_for_model = MODEL_PARAMS.get(model_name, {})
206
+
207
+ # Define ALL possible parameter components used across models
208
+ all_param_keys = set(k for params in MODEL_PARAMS.values() for k in params)
209
+
210
+ for key in all_param_keys:
211
+ param_config = params_for_model.get(key)
212
+ if param_config:
213
+ # Parameter exists for this model: make visible and set defaults
214
+ updates[param_elements[key]] = gr.update(
215
+ visible=True,
216
+ label=key.replace("_", " ").title(), # Nicer label
217
+ value=param_config.get("default") # Set default value
218
+ # Add specific updates for slider ranges etc. if needed
219
+ # minimum=param_config.get("min"),
220
+ # maximum=param_config.get("max"),
221
+ # step=param_config.get("step")
222
+ )
223
+ else:
224
+ # Parameter does NOT exist for this model: hide it
225
+ updates[param_elements[key]] = gr.update(visible=False, value=None) # Reset value when hiding
226
+
227
+ return updates
228
+
229
+
230
+ def handle_chat(api_key, model_name, history, message, files, *params_tuple):
231
+ """Handles the chat interaction."""
232
+ # 1. Basic Validation
233
+ if not api_key:
234
+ gr.Warning("Gemini API Key is missing!")
235
+ return history, "" # Return unchanged history and empty textbox
236
+ if not message and not files:
237
+ gr.Warning("Please enter a message or upload files.")
238
+ return history, ""
239
+
240
+ # 2. Configure API Key
241
+ try:
242
+ genai.configure(api_key=api_key)
243
+ except Exception as e:
244
+ gr.Error(f"Failed to configure API Key: {e}")
245
+ return history, message # Keep message in textbox for retry
246
+
247
+ # 3. Prepare Generation Config from *params_tuple
248
+ param_keys = [key for key, config in MODEL_PARAMS.get(model_name, {}).items()]
249
+ generation_config_dict = {}
250
+ if len(params_tuple) == len(param_keys):
251
+ generation_config_dict = {key: val for key, val in zip(param_keys, params_tuple) if val is not None}
252
+ # Handle stop sequences (expecting comma-separated string)
253
+ if 'stop_sequences' in generation_config_dict and isinstance(generation_config_dict['stop_sequences'], str):
254
+ sequences = [s.strip() for s in generation_config_dict['stop_sequences'].split(',') if s.strip()]
255
+ if sequences:
256
+ generation_config_dict['stop_sequences'] = sequences
257
+ else:
258
+ del generation_config_dict['stop_sequences'] # Remove if empty/invalid
259
+ print(f"Using Generation Config: {generation_config_dict}") # Debug
260
+ else:
261
+ print(f"Warning: Mismatch between expected params ({len(param_keys)}) and received params ({len(params_tuple)})")
262
+
263
+
264
+ # 4. Prepare Model Input
265
+ gemini_input_parts = prepare_gemini_input(message, files)
266
+ print(f"Prepared Gemini Input Parts: {gemini_input_parts}") # Debugging
267
+
268
+ # 5. Initialize Model and Chat
269
+ try:
270
+ # Add safety settings if needed/configured
271
+ # safety_settings = {...}
272
+ model = genai.GenerativeModel(model_name)#, safety_settings=safety_settings)
273
+
274
+ # Convert Gradio history (list of lists) to Gemini format (list of Content objects)
275
+ gemini_history = []
276
+ for user_msg, model_msg in history:
277
+ # Simple text history for now. Need enhancement for multimodal history.
278
+ if user_msg: gemini_history.append({'role': 'user', 'parts': [user_msg]})
279
+ if model_msg: gemini_history.append({'role': 'model', 'parts': [model_msg]})
280
+
281
+ chat = model.start_chat(history=gemini_history)
282
+ print(f"Starting chat with history (simplified): {gemini_history}") # Debugging
283
+
284
+ except Exception as e:
285
+ gr.Error(f"Failed to initialize model or chat: {e}")
286
+ return history, message # Keep message in textbox
287
+
288
+ # 6. Send Message and Get Response
289
+ response_text = ""
290
+ try:
291
+ # Use streaming for better UX in chat
292
+ response = chat.send_message(gemini_input_parts,
293
+ generation_config=genai.types.GenerationConfig(**generation_config_dict),
294
+ stream=True)
295
+
296
+ full_response_content = ""
297
+ for chunk in response:
298
+ # Check if the chunk has text content
299
+ if hasattr(chunk, 'text'):
300
+ chunk_text = chunk.text
301
+ print(f"Stream chunk: {chunk_text}") # Debug stream
302
+ full_response_content += chunk_text
303
+ # Yield intermediate updates to the chatbot
304
+ current_history = history + [[message or "[Input files only]", full_response_content]]
305
+ yield current_history, "" # Update chatbot, clear input
306
+ # Check for image data if model supports it (more complex parsing needed)
307
+ # elif chunk.parts and chunk.parts[0].inline_data:
308
+ # # Handle potential image output - requires modification
309
+ # pass
310
+
311
+ response_text = full_response_content # Final text response
312
+
313
+ # Check for blocked prompts or safety issues
314
+ if not response_text and response.prompt_feedback.block_reason:
315
+ block_reason = response.prompt_feedback.block_reason
316
+ safety_ratings = response.prompt_feedback.safety_ratings
317
+ gr.Warning(f"Request blocked. Reason: {block_reason}. Ratings: {safety_ratings}")
318
+ # Append a notice to history instead of an empty response
319
+ history.append([message or "[Input files only]", f"[Request blocked due to: {block_reason}]"])
320
+ return history, "" # Clear input box
321
+
322
+
323
+ except Exception as e:
324
+ gr.Error(f"Error during generation: {e}")
325
+ # Optionally add the error to history for context
326
+ history.append([message or "[Input files only]", f"[Error during generation: {str(e)}]"])
327
+ return history, "" # Clear input box
328
+
329
+ # 7. Update History and Clear Input
330
+ # The yielding above handles intermediate updates. This is the final state.
331
+ final_history = history + [[message or "[Input files only]", response_text or "[No text content received]"]]
332
+ return final_history, "" # Final update, clear input
333
+
334
+
335
+ def handle_single_response(api_key, model_name, prompt, files, *params_tuple):
336
+ """Handles the single response interaction."""
337
+ # 1. Validations
338
+ if not api_key:
339
+ gr.Warning("Gemini API Key is missing!")
340
+ return "[Error: API Key Missing]", None # Text output, Image output
341
+ if not prompt and not files:
342
+ gr.Warning("Please enter a prompt or upload files.")
343
+ return "[Error: No input provided]", None
344
+
345
+ # 2. Configure API Key
346
+ try:
347
+ genai.configure(api_key=api_key)
348
+ except Exception as e:
349
+ gr.Error(f"Failed to configure API Key: {e}")
350
+ return f"[Error: API Key Config Failed: {e}]", None
351
+
352
+ # 3. Prepare Generation Config
353
+ param_keys = [key for key, config in MODEL_PARAMS.get(model_name, {}).items()]
354
+ generation_config_dict = {}
355
+ if len(params_tuple) == len(param_keys):
356
+ generation_config_dict = {key: val for key, val in zip(param_keys, params_tuple) if val is not None}
357
+ # Handle stop sequences
358
+ if 'stop_sequences' in generation_config_dict and isinstance(generation_config_dict['stop_sequences'], str):
359
+ sequences = [s.strip() for s in generation_config_dict['stop_sequences'].split(',') if s.strip()]
360
+ if sequences:
361
+ generation_config_dict['stop_sequences'] = sequences
362
+ else:
363
+ del generation_config_dict['stop_sequences']
364
+ print(f"Using Generation Config: {generation_config_dict}") # Debug
365
+ else:
366
+ print(f"Warning: Mismatch between expected params ({len(param_keys)}) and received params ({len(params_tuple)})")
367
+
368
+
369
+ # 4. Prepare Model Input
370
+ gemini_input_parts = prepare_gemini_input(prompt, files)
371
+ print(f"Prepared Gemini Input Parts: {gemini_input_parts}") # Debugging
372
+
373
+
374
+ # 5. Initialize Model
375
+ try:
376
+ # Add safety settings if needed/configured
377
+ model = genai.GenerativeModel(model_name)
378
+ except Exception as e:
379
+ gr.Error(f"Failed to initialize model: {e}")
380
+ return f"[Error: Model Initialization Failed: {e}]", None
381
+
382
+ # 6. Generate Content (Non-streaming for single response usually)
383
+ output_text = "[No text content generated]"
384
+ output_image = None # Placeholder for image output
385
+ try:
386
+ response = model.generate_content(
387
+ gemini_input_parts,
388
+ generation_config=genai.types.GenerationConfig(**generation_config_dict),
389
+ stream=False # Simpler for single turn unless very long output expected
390
+ )
391
+
392
+ # Check for blocked prompts or safety issues
393
+ if response.prompt_feedback.block_reason:
394
+ block_reason = response.prompt_feedback.block_reason
395
+ safety_ratings = response.prompt_feedback.safety_ratings
396
+ gr.Warning(f"Request blocked. Reason: {block_reason}. Ratings: {safety_ratings}")
397
+ return f"[Request blocked due to: {block_reason}]", None
398
+
399
+ # Process response parts (could contain text and/or images)
400
+ # This part needs refinement based on how Gemini API returns mixed content
401
+ # For now, prioritize text and assume first image part if present
402
+ response_text_parts = []
403
+ for part in response.parts:
404
+ if hasattr(part, 'text'):
405
+ response_text_parts.append(part.text)
406
+ elif hasattr(part, 'inline_data') and part.inline_data.mime_type.startswith('image/'):
407
+ if output_image is None: # Display the first image found
408
+ try:
409
+ image_data = part.inline_data.data
410
+ img = Image.open(io.BytesIO(image_data))
411
+ output_image = img
412
+ print("Image received in response.")
413
+ except Exception as img_err:
414
+ print(f"Error decoding image from response: {img_err}")
415
+ response_text_parts.append("[Error decoding image in response]")
416
+
417
+ if response_text_parts:
418
+ output_text = "\n".join(response_text_parts)
419
+ elif hasattr(response, 'text'): # Fallback if parts parsing fails but text attribute exists
420
+ output_text = response.text
421
+
422
+ # Check if only an image was returned (or intended)
423
+ if not response_text_parts and output_image is not None:
424
+ output_text = "[Image generated - see output below]"
425
+
426
+
427
+ except Exception as e:
428
+ gr.Error(f"Error during generation: {e}")
429
+ output_text = f"[Error during generation: {str(e)}]"
430
+
431
+ # 7. Return results
432
+ return output_text, output_image
433
+
434
+
435
+ # --- Build Gradio Interface ---
436
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
437
+ gr.Markdown("# Gemini API Interface")
438
+ gr.Markdown("Interact with Google Gemini models using your own API key. Supports chat, single responses, file uploads, and model-specific parameters.")
439
+
440
+ # API Key Section
441
+ with gr.Row():
442
+ api_key_input = gr.Textbox(
443
+ label="Gemini API Key",
444
+ placeholder="Enter your Gemini API Key here",
445
+ type="password",
446
+ scale=3
447
+ )
448
+ validate_button = gr.Button("Validate Key", scale=1)
449
+ api_key_status = gr.Markdown("<p style='color: gray;'>Enter your key and click Validate.</p>")
450
+
451
+ # Model Selection
452
+ model_dropdown = gr.Dropdown(
453
+ label="Select Gemini Model",
454
+ choices=AVAILABLE_MODELS,
455
+ value=AVAILABLE_MODELS[0], # Default model
456
+ )
457
+
458
+ # Dynamic Parameters Section (Initially hidden, updated by model selection)
459
+ param_elements = {} # Dictionary to hold parameter UI components
460
+ with gr.Accordion("Model Parameters", open=False) as params_accordion:
461
+ # Create UI elements for ALL possible parameters defined in MODEL_PARAMS
462
+ # They will be shown/hidden by the update_parameter_visibility function
463
+ all_possible_params = set(k for params in MODEL_PARAMS.values() for k in params)
464
+ for param_name in sorted(list(all_possible_params)): # Sort for consistent order
465
+ # Determine control type based on the first model that defines it (can be refined)
466
+ control_type = "textbox" # Default
467
+ config = {}
468
+ for model_cfg in MODEL_PARAMS.values():
469
+ if param_name in model_cfg:
470
+ config = model_cfg[param_name]
471
+ control_type = config.get("type", "textbox")
472
+ break # Found config for this param
473
+
474
+ if control_type == "slider":
475
+ param_elements[param_name] = gr.Slider(
476
+ label=param_name.replace("_", " ").title(),
477
+ minimum=config.get("min", 0),
478
+ maximum=config.get("max", 1),
479
+ step=config.get("step", 0.1),
480
+ value=config.get("default"),
481
+ visible=False, # Initially hidden
482
+ interactive=True
483
+ )
484
+ elif control_type == "number":
485
+ param_elements[param_name] = gr.Number(
486
+ label=param_name.replace("_", " ").title(),
487
+ minimum=config.get("min", 1),
488
+ step=config.get("step", 1),
489
+ value=config.get("default"),
490
+ visible=False,
491
+ interactive=True
492
+ )
493
+ else: # Default to Textbox for stop_sequences etc.
494
+ param_elements[param_name] = gr.Textbox(
495
+ label=param_name.replace("_", " ").title(),
496
+ lines=config.get("lines", 1),
497
+ placeholder=config.get("placeholder", ""),
498
+ value=config.get("default", ""),
499
+ visible=False,
500
+ interactive=True
501
+ )
502
+
503
+ # Pack the parameter components into a list for function inputs/outputs
504
+ # IMPORTANT: The order here MUST match the order expected by handle_chat/handle_single_response
505
+ ordered_param_components = [param_elements[key] for key in sorted(param_elements.keys())]
506
+
507
+
508
+ # Main Interaction Area (Tabs)
509
+ with gr.Tabs():
510
+ # --- Chat Interface Tab ---
511
+ with gr.TabItem("Chat Interface"):
512
+ gr.Markdown("Have a conversation with the selected model. Upload files to include their content.")
513
+ chat_history_state = gr.State([]) # Holds the conversation history
514
+ chatbot_display = gr.Chatbot(label="Conversation", height=500)
515
+ with gr.Row():
516
+ chat_file_upload = gr.File(label="Upload Files (Text, Images, etc.)", file_count="multiple")
517
+ with gr.Row():
518
+ chat_message_input = gr.Textbox(label="Your Message", placeholder="Type your message here...", scale=4, lines=3)
519
+ chat_submit_button = gr.Button("Send", variant="primary", scale=1)
520
+ clear_chat_button = gr.Button("Clear Chat History")
521
+
522
+
523
+ # --- Single Response Tab ---
524
+ with gr.TabItem("Single Response"):
525
+ gr.Markdown("Send a prompt (and optionally files) to get a single response from the model.")
526
+ with gr.Row():
527
+ with gr.Column(scale=2):
528
+ single_prompt_input = gr.Textbox(label="Your Prompt", placeholder="Enter your prompt...", lines=5)
529
+ single_file_upload = gr.File(label="Upload Files (Text, Images, etc.)", file_count="multiple")
530
+ single_submit_button = gr.Button("Generate Response", variant="primary")
531
+ with gr.Column(scale=2):
532
+ gr.Markdown("**Output:**")
533
+ single_output_text = gr.Textbox(label="Text Response", lines=10, interactive=False)
534
+ single_output_image = gr.Image(label="Image Response", type="pil", interactive=False) # Display PIL images
535
+
536
+
537
+ # --- Event Wiring ---
538
+
539
+ # 1. API Key Validation
540
+ validate_button.click(
541
+ fn=validate_api_key,
542
+ inputs=[api_key_input],
543
+ outputs=[api_key_status]
544
+ )
545
+
546
+ # 2. Update Parameters UI when Model Changes
547
+ model_dropdown.change(
548
+ fn=update_parameter_visibility,
549
+ inputs=[model_dropdown],
550
+ outputs=list(param_elements.values()) # Pass the actual components
551
+ )
552
+
553
+ # Trigger initial parameter visibility update on load
554
+ demo.load(
555
+ fn=update_parameter_visibility,
556
+ inputs=[model_dropdown],
557
+ outputs=list(param_elements.values())
558
+ )
559
+
560
+ # 3. Chat Submission Logic (using .then() for streaming if possible, or standard submit)
561
+ # Note: Gradio streaming with gr.Chatbot often uses yields
562
+ chat_submit_button.click(
563
+ fn=handle_chat,
564
+ inputs=[
565
+ api_key_input,
566
+ model_dropdown,
567
+ chat_history_state,
568
+ chat_message_input,
569
+ chat_file_upload
570
+ ] + ordered_param_components, # Add dynamic params
571
+ outputs=[chatbot_display, chat_message_input] # Update chatbot, clear input box
572
+ ).then(
573
+ # Update the state *after* the response is fully generated
574
+ lambda history: history, # Simple pass-through to get final history
575
+ inputs=chatbot_display,
576
+ outputs=chat_history_state
577
+ )
578
+ # Allow submitting chat by pressing Enter in the textbox
579
+ chat_message_input.submit(
580
+ fn=handle_chat,
581
+ inputs=[
582
+ api_key_input,
583
+ model_dropdown,
584
+ chat_history_state,
585
+ chat_message_input,
586
+ chat_file_upload
587
+ ] + ordered_param_components,
588
+ outputs=[chatbot_display, chat_message_input]
589
+ ).then(
590
+ lambda history: history,
591
+ inputs=chatbot_display,
592
+ outputs=chat_history_state
593
+ )
594
+
595
+
596
+ # 4. Clear Chat Logic
597
+ def clear_chat_history_func():
598
+ return [], [] # Clears chatbot display and history state
599
+
600
+ clear_chat_button.click(
601
+ fn=clear_chat_history_func,
602
+ inputs=[],
603
+ outputs=[chatbot_display, chat_history_state]
604
+ )
605
+
606
+ # 5. Single Response Submission Logic
607
+ single_submit_button.click(
608
+ fn=handle_single_response,
609
+ inputs=[
610
+ api_key_input,
611
+ model_dropdown,
612
+ single_prompt_input,
613
+ single_file_upload
614
+ ] + ordered_param_components, # Add dynamic params
615
+ outputs=[single_output_text, single_output_image]
616
+ )
617
+
618
+
619
+ # Launch the Gradio app
620
+ if __name__ == "__main__":
621
+ demo.launch(debug=True) # Set debug=False for deployment