cstr commited on
Commit
37f5ab3
·
verified ·
1 Parent(s): 5e9023b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +361 -154
app.py CHANGED
@@ -6,6 +6,8 @@ import base64
6
  from PIL import Image
7
  import io
8
  import logging
 
 
9
 
10
  # Configure logging
11
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
@@ -14,82 +16,80 @@ logger = logging.getLogger(__name__)
14
  # API key
15
  OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "")
16
 
17
- # Model list with context sizes
18
  MODELS = [
19
  # Vision Models
20
- ("Meta: Llama 3.2 11B Vision Instruct (free)", "meta-llama/llama-3.2-11b-vision-instruct:free", 131072),
21
- ("Qwen: Qwen2.5 VL 72B Instruct (free)", "qwen/qwen2.5-vl-72b-instruct:free", 131072),
22
- ("Qwen: Qwen2.5 VL 32B Instruct (free)", "qwen/qwen2.5-vl-32b-instruct:free", 8192),
23
- ("Qwen: Qwen2.5 VL 7B Instruct (free)", "qwen/qwen-2.5-vl-7b-instruct:free", 64000),
24
- ("Qwen: Qwen2.5 VL 3B Instruct (free)", "qwen/qwen2.5-vl-3b-instruct:free", 64000),
 
 
25
 
26
  # Gemini Models
27
- ("Google: Gemini Pro 2.0 Experimental (free)", "google/gemini-2.0-pro-exp-02-05:free", 2000000),
28
- ("Google: Gemini Pro 2.5 Experimental (free)", "google/gemini-2.5-pro-exp-03-25:free", 1000000),
29
- ("Google: Gemini 2.0 Flash Thinking Experimental 01-21 (free)", "google/gemini-2.0-flash-thinking-exp:free", 1048576),
30
- ("Google: Gemini Flash 2.0 Experimental (free)", "google/gemini-2.0-flash-exp:free", 1048576),
31
- ("Google: Gemini Flash 1.5 8B Experimental", "google/gemini-flash-1.5-8b-exp", 1000000),
32
- ("Google: Gemini 2.0 Flash Thinking Experimental (free)", "google/gemini-2.0-flash-thinking-exp-1219:free", 40000),
33
- ("Google: LearnLM 1.5 Pro Experimental (free)", "google/learnlm-1.5-pro-experimental:free", 40960),
 
34
 
35
  # Llama Models
36
- ("Meta: Llama 3.3 70B Instruct (free)", "meta-llama/llama-3.3-70b-instruct:free", 8000),
37
- ("Meta: Llama 3.2 3B Instruct (free)", "meta-llama/llama-3.2-3b-instruct:free", 20000),
38
- ("Meta: Llama 3.2 1B Instruct (free)", "meta-llama/llama-3.2-1b-instruct:free", 131072),
39
- ("Meta: Llama 3.1 8B Instruct (free)", "meta-llama/llama-3.1-8b-instruct:free", 131072),
40
- ("Meta: Llama 3 8B Instruct (free)", "meta-llama/llama-3-8b-instruct:free", 8192),
41
- ("NVIDIA: Llama 3.1 Nemotron 70B Instruct (free)", "nvidia/llama-3.1-nemotron-70b-instruct:free", 131072),
 
 
42
 
43
  # DeepSeek Models
44
- ("DeepSeek: DeepSeek R1 Zero (free)", "deepseek/deepseek-r1-zero:free", 163840),
45
- ("DeepSeek: R1 (free)", "deepseek/deepseek-r1:free", 163840),
46
- ("DeepSeek: DeepSeek V3 Base (free)", "deepseek/deepseek-v3-base:free", 131072),
47
- ("DeepSeek: DeepSeek V3 0324 (free)", "deepseek/deepseek-v3-0324:free", 131072),
48
- ("DeepSeek: DeepSeek V3 (free)", "deepseek/deepseek-chat:free", 131072),
49
- ("DeepSeek: R1 Distill Qwen 14B (free)", "deepseek/deepseek-r1-distill-qwen-14b:free", 64000),
50
- ("DeepSeek: R1 Distill Qwen 32B (free)", "deepseek/deepseek-r1-distill-qwen-32b:free", 16000),
51
- ("DeepSeek: R1 Distill Llama 70B (free)", "deepseek/deepseek-r1-distill-llama-70b:free", 8192),
 
 
52
 
53
- # Gemma Models
54
- ("Google: Gemma 3 27B (free)", "google/gemma-3-27b-it:free", 96000),
55
- ("Google: Gemma 3 12B (free)", "google/gemma-3-12b-it:free", 131072),
56
- ("Google: Gemma 3 4B (free)", "google/gemma-3-4b-it:free", 131072),
57
- ("Google: Gemma 3 1B (free)", "google/gemma-3-1b-it:free", 32768),
58
- ("Google: Gemma 2 9B (free)", "google/gemma-2-9b-it:free", 8192),
 
 
 
 
59
 
60
- # Mistral Models
61
- ("Mistral: Mistral Nemo (free)", "mistralai/mistral-nemo:free", 128000),
62
- ("Mistral: Mistral Small 3.1 24B (free)", "mistralai/mistral-small-3.1-24b-instruct:free", 96000),
63
- ("Mistral: Mistral Small 3 (free)", "mistralai/mistral-small-24b-instruct-2501:free", 32768),
64
- ("Mistral: Mistral 7B Instruct (free)", "mistralai/mistral-7b-instruct:free", 8192),
65
-
66
- # Qwen Models
67
- ("Qwen: Qwen2.5 72B Instruct (free)", "qwen/qwen-2.5-72b-instruct:free", 32768),
68
- ("Qwen: QwQ 32B (free)", "qwen/qwq-32b:free", 40000),
69
- ("Qwen: QwQ 32B Preview (free)", "qwen/qwq-32b-preview:free", 16384),
70
- ("Qwen2.5 Coder 32B Instruct (free)", "qwen/qwen-2.5-coder-32b-instruct:free", 32768),
71
- ("Qwen 2 7B Instruct (free)", "qwen/qwen-2-7b-instruct:free", 8192),
72
-
73
- # Other Models
74
- ("Nous: DeepHermes 3 Llama 3 8B Preview (free)", "nousresearch/deephermes-3-llama-3-8b-preview:free", 131072),
75
- ("Moonshot AI: Moonlight 16B A3B Instruct (free)", "moonshotai/moonlight-16b-a3b-instruct:free", 8192),
76
- ("Microsoft: Phi-3 Mini 128K Instruct (free)", "microsoft/phi-3-mini-128k-instruct:free", 8192),
77
- ("Microsoft: Phi-3 Medium 128K Instruct (free)", "microsoft/phi-3-medium-128k-instruct:free", 8192),
78
- ("OpenChat 3.5 7B (free)", "openchat/openchat-7b:free", 8192),
79
- ("Reka: Flash 3 (free)", "rekaai/reka-flash-3:free", 32768),
80
- ("Dolphin3.0 R1 Mistral 24B (free)", "cognitivecomputations/dolphin3.0-r1-mistral-24b:free", 32768),
81
- ("Dolphin3.0 Mistral 24B (free)", "cognitivecomputations/dolphin3.0-mistral-24b:free", 32768),
82
- ("Bytedance: UI-TARS 72B (free)", "bytedance-research/ui-tars-72b:free", 32768),
83
- ("Qwerky 72b (free)", "featherless/qwerky-72b:free", 32768),
84
- ("OlympicCoder 7B (free)", "open-r1/olympiccoder-7b:free", 32768),
85
- ("OlympicCoder 32B (free)", "open-r1/olympiccoder-32b:free", 32768),
86
- ("Rogue Rose 103B v0.2 (free)", "sophosympatheia/rogue-rose-103b-v0.2:free", 4096),
87
- ("Toppy M 7B (free)", "undi95/toppy-m-7b:free", 4096),
88
- ("Hugging Face: Zephyr 7B (free)", "huggingfaceh4/zephyr-7b-beta:free", 4096),
89
- ("MythoMax 13B (free)", "gryphe/mythomax-l2-13b:free", 4096),
90
- ("AllenAI: Molmo 7B D (free)", "allenai/molmo-7b-d:free", 4096),
91
  ]
92
 
 
 
 
 
 
 
93
  def format_to_message_dict(history):
94
  """Convert history to proper message format"""
95
  messages = []
@@ -122,35 +122,95 @@ def encode_image_to_base64(image_path):
122
  logger.error(f"Error encoding image: {str(e)}")
123
  return None
124
 
125
- def prepare_message_with_images(text, images):
126
- """Prepare a message with text and images"""
127
- if not images:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  return text
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  content = [{"type": "text", "text": text}]
131
 
132
- for img in images:
133
- if img is None:
134
- continue
135
-
136
- encoded_image = encode_image_to_base64(img)
137
- if encoded_image:
138
- content.append({
139
- "type": "image_url",
140
- "image_url": {"url": encoded_image}
141
- })
 
 
142
 
143
  return content
144
 
145
- def ask_ai(message, chatbot, model_choice, temperature, max_tokens, uploaded_files):
146
- """Enhanced AI query function with file upload support and detailed logging"""
147
- if not message.strip() and not uploaded_files:
 
148
  return chatbot, ""
149
 
150
  # Get model ID and context size
151
  model_id = None
152
  context_size = 0
153
- for name, model_id_value, ctx_size in MODELS:
154
  if name == model_choice:
155
  model_id = model_id_value
156
  context_size = ctx_size
@@ -163,11 +223,8 @@ def ask_ai(message, chatbot, model_choice, temperature, max_tokens, uploaded_fil
163
  # Create messages from chatbot history
164
  messages = format_to_message_dict(chatbot)
165
 
166
- # Prepare message with images if any
167
- if uploaded_files:
168
- content = prepare_message_with_images(message, uploaded_files)
169
- else:
170
- content = message
171
 
172
  # Add current message
173
  messages.append({"role": "user", "content": content})
@@ -175,16 +232,25 @@ def ask_ai(message, chatbot, model_choice, temperature, max_tokens, uploaded_fil
175
  # Call API
176
  try:
177
  logger.info(f"Sending request to model: {model_id}")
178
- logger.info(f"Messages: {json.dumps(messages)}")
179
 
 
180
  payload = {
181
  "model": model_id,
182
  "messages": messages,
183
  "temperature": temperature,
184
- "max_tokens": max_tokens
 
 
 
185
  }
186
 
187
- logger.info(f"Request payload: {json.dumps(payload)}")
 
 
 
 
 
 
188
 
189
  response = requests.post(
190
  "https://openrouter.ai/api/v1/chat/completions",
@@ -194,11 +260,10 @@ def ask_ai(message, chatbot, model_choice, temperature, max_tokens, uploaded_fil
194
  "HTTP-Referer": "https://huggingface.co/spaces"
195
  },
196
  json=payload,
197
- timeout=60
198
  )
199
 
200
  logger.info(f"Response status: {response.status_code}")
201
- logger.info(f"Response headers: {response.headers}")
202
 
203
  response_text = response.text
204
  logger.info(f"Response body: {response_text}")
@@ -221,20 +286,68 @@ def ask_ai(message, chatbot, model_choice, temperature, max_tokens, uploaded_fil
221
  return chatbot, ""
222
 
223
  def clear_chat():
224
- return [], "", [], 0.7, 1000
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
  # Create enhanced interface
227
- with gr.Blocks(css="footer {visibility: hidden}") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  gr.Markdown("""
229
  # Enhanced AI Chat
230
 
231
- This interface allows you to chat with various free AI models from OpenRouter.
232
- You can upload images for vision-capable models and adjust parameters.
233
  """)
234
 
235
  with gr.Row():
236
  with gr.Column(scale=2):
237
- chatbot = gr.Chatbot(height=500, show_copy_button=True, show_label=False)
 
 
 
 
 
238
 
239
  with gr.Row():
240
  message = gr.Textbox(
@@ -251,93 +364,187 @@ with gr.Blocks(css="footer {visibility: hidden}") as demo:
251
  clear_btn = gr.Button("Clear Chat", variant="secondary")
252
 
253
  with gr.Row():
254
- uploaded_files = gr.Gallery(
255
- label="Uploaded Images",
256
- show_label=True,
257
- elem_id="gallery",
258
- columns=4,
259
- height=150,
260
- visible=False
261
- )
262
-
263
- with gr.Row():
264
- upload_btn = gr.UploadButton(
265
- label="Upload Images (for vision models)",
266
- file_types=["image"],
267
- file_count="multiple"
268
- )
 
 
 
 
 
 
 
 
269
 
270
  with gr.Column(scale=1):
271
  with gr.Group():
272
  gr.Markdown("### Model Selection")
273
- model_names = [name for name, _, _ in MODELS]
274
- model_choice = gr.Radio(
275
- model_names,
276
- value=model_names[0],
277
- label="Choose a Model"
278
- )
279
 
280
- with gr.Accordion("Model Context", open=False):
281
- context_info = gr.HTML(value="<p>Select a model to see its context window</p>")
282
-
283
- with gr.Accordion("Parameters", open=False):
284
- temperature = gr.Slider(
285
- minimum=0.1,
286
- maximum=2.0,
287
- value=0.7,
288
- step=0.1,
289
- label="Temperature"
290
- )
291
 
292
- max_tokens = gr.Slider(
293
- minimum=100,
294
- maximum=4000,
295
- value=1000,
296
- step=100,
297
- label="Max Tokens"
298
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
 
300
- # Set up context window display
301
- def update_context_info(model_name):
302
- for name, _, ctx_size in MODELS:
303
- if name == model_name:
304
- return f"<p><b>Context window:</b> {ctx_size:,} tokens</p>"
305
- return "<p>Model information not found</p>"
306
 
 
307
  model_choice.change(
308
- fn=update_context_info,
309
  inputs=[model_choice],
310
- outputs=[context_info]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  )
312
 
313
- # Process uploaded files
314
- def process_uploaded_files(files):
315
- file_paths = [file.name for file in files]
316
- return file_paths, gr.update(visible=True)
317
 
318
- upload_btn.upload(
319
- fn=process_uploaded_files,
320
- inputs=[upload_btn],
321
- outputs=[uploaded_files, uploaded_files]
322
  )
323
 
324
  # Set up events
325
  submit_btn.click(
326
  fn=ask_ai,
327
- inputs=[message, chatbot, model_choice, temperature, max_tokens, uploaded_files],
 
 
 
 
328
  outputs=[chatbot, message]
329
  )
330
 
331
  message.submit(
332
  fn=ask_ai,
333
- inputs=[message, chatbot, model_choice, temperature, max_tokens, uploaded_files],
 
 
 
 
334
  outputs=[chatbot, message]
335
  )
336
 
337
  clear_btn.click(
338
  fn=clear_chat,
339
  inputs=[],
340
- outputs=[chatbot, message, uploaded_files, temperature, max_tokens]
 
 
 
341
  )
342
 
343
  # Launch directly with Gradio's built-in server
 
6
  from PIL import Image
7
  import io
8
  import logging
9
+ import PyPDF2
10
+ import markdown
11
 
12
  # Configure logging
13
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 
16
  # API key
17
  OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "")
18
 
19
+ # Model list with context sizes - organized by category
20
  MODELS = [
21
  # Vision Models
22
+ {"category": "Vision", "models": [
23
+ ("Meta: Llama 3.2 11B Vision Instruct", "meta-llama/llama-3.2-11b-vision-instruct:free", 131072),
24
+ ("Qwen2.5 VL 72B Instruct", "qwen/qwen2.5-vl-72b-instruct:free", 131072),
25
+ ("Qwen2.5 VL 32B Instruct", "qwen/qwen2.5-vl-32b-instruct:free", 8192),
26
+ ("Qwen2.5 VL 7B Instruct", "qwen/qwen-2.5-vl-7b-instruct:free", 64000),
27
+ ("Qwen2.5 VL 3B Instruct", "qwen/qwen2.5-vl-3b-instruct:free", 64000),
28
+ ]},
29
 
30
  # Gemini Models
31
+ {"category": "Gemini", "models": [
32
+ ("Gemini Pro 2.0 Experimental", "google/gemini-2.0-pro-exp-02-05:free", 2000000),
33
+ ("Gemini Pro 2.5 Experimental", "google/gemini-2.5-pro-exp-03-25:free", 1000000),
34
+ ("Gemini 2.0 Flash Thinking Experimental", "google/gemini-2.0-flash-thinking-exp:free", 1048576),
35
+ ("Gemini Flash 2.0 Experimental", "google/gemini-2.0-flash-exp:free", 1048576),
36
+ ("Gemini Flash 1.5 8B Experimental", "google/gemini-flash-1.5-8b-exp", 1000000),
37
+ ("LearnLM 1.5 Pro Experimental", "google/learnlm-1.5-pro-experimental:free", 40960),
38
+ ]},
39
 
40
  # Llama Models
41
+ {"category": "Llama", "models": [
42
+ ("Llama 3.3 70B Instruct", "meta-llama/llama-3.3-70b-instruct:free", 8000),
43
+ ("Llama 3.2 3B Instruct", "meta-llama/llama-3.2-3b-instruct:free", 20000),
44
+ ("Llama 3.2 1B Instruct", "meta-llama/llama-3.2-1b-instruct:free", 131072),
45
+ ("Llama 3.1 8B Instruct", "meta-llama/llama-3.1-8b-instruct:free", 131072),
46
+ ("Llama 3 8B Instruct", "meta-llama/llama-3-8b-instruct:free", 8192),
47
+ ("Llama 3.1 Nemotron 70B Instruct", "nvidia/llama-3.1-nemotron-70b-instruct:free", 131072),
48
+ ]},
49
 
50
  # DeepSeek Models
51
+ {"category": "DeepSeek", "models": [
52
+ ("DeepSeek R1 Zero", "deepseek/deepseek-r1-zero:free", 163840),
53
+ ("DeepSeek R1", "deepseek/deepseek-r1:free", 163840),
54
+ ("DeepSeek V3 Base", "deepseek/deepseek-v3-base:free", 131072),
55
+ ("DeepSeek V3 0324", "deepseek/deepseek-v3-0324:free", 131072),
56
+ ("DeepSeek V3", "deepseek/deepseek-chat:free", 131072),
57
+ ("DeepSeek R1 Distill Qwen 14B", "deepseek/deepseek-r1-distill-qwen-14b:free", 64000),
58
+ ("DeepSeek R1 Distill Qwen 32B", "deepseek/deepseek-r1-distill-qwen-32b:free", 16000),
59
+ ("DeepSeek R1 Distill Llama 70B", "deepseek/deepseek-r1-distill-llama-70b:free", 8192),
60
+ ]},
61
 
62
+ # Other Popular Models
63
+ {"category": "Other Popular Models", "models": [
64
+ ("Mistral Nemo", "mistralai/mistral-nemo:free", 128000),
65
+ ("Mistral Small 3.1 24B", "mistralai/mistral-small-3.1-24b-instruct:free", 96000),
66
+ ("Gemma 3 27B", "google/gemma-3-27b-it:free", 96000),
67
+ ("Gemma 3 12B", "google/gemma-3-12b-it:free", 131072),
68
+ ("Gemma 3 4B", "google/gemma-3-4b-it:free", 131072),
69
+ ("DeepHermes 3 Llama 3 8B Preview", "nousresearch/deephermes-3-llama-3-8b-preview:free", 131072),
70
+ ("Qwen2.5 72B Instruct", "qwen/qwen-2.5-72b-instruct:free", 32768),
71
+ ]},
72
 
73
+ # Smaller Models (<50B params)
74
+ {"category": "Smaller Models", "models": [
75
+ ("Gemma 3 1B", "google/gemma-3-1b-it:free", 32768),
76
+ ("Gemma 2 9B", "google/gemma-2-9b-it:free", 8192),
77
+ ("Mistral 7B Instruct", "mistralai/mistral-7b-instruct:free", 8192),
78
+ ("Qwen 2 7B Instruct", "qwen/qwen-2-7b-instruct:free", 8192),
79
+ ("Phi-3 Mini 128K Instruct", "microsoft/phi-3-mini-128k-instruct:free", 8192),
80
+ ("Phi-3 Medium 128K Instruct", "microsoft/phi-3-medium-128k-instruct:free", 8192),
81
+ ("OpenChat 3.5 7B", "openchat/openchat-7b:free", 8192),
82
+ ("Zephyr 7B", "huggingfaceh4/zephyr-7b-beta:free", 4096),
83
+ ("MythoMax 13B", "gryphe/mythomax-l2-13b:free", 4096),
84
+ ]},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  ]
86
 
87
+ # Flatten model list for easy searching
88
+ ALL_MODELS = []
89
+ for category in MODELS:
90
+ for model in category["models"]:
91
+ ALL_MODELS.append(model)
92
+
93
  def format_to_message_dict(history):
94
  """Convert history to proper message format"""
95
  messages = []
 
122
  logger.error(f"Error encoding image: {str(e)}")
123
  return None
124
 
125
+ def extract_text_from_file(file_path):
126
+ """Extract text from various file types"""
127
+ try:
128
+ file_extension = file_path.split('.')[-1].lower()
129
+
130
+ if file_extension == 'pdf':
131
+ text = ""
132
+ with open(file_path, 'rb') as file:
133
+ pdf_reader = PyPDF2.PdfReader(file)
134
+ for page_num in range(len(pdf_reader.pages)):
135
+ page = pdf_reader.pages[page_num]
136
+ text += page.extract_text() + "\n\n"
137
+ return text
138
+
139
+ elif file_extension == 'md':
140
+ with open(file_path, 'r', encoding='utf-8') as file:
141
+ md_text = file.read()
142
+ # You can convert markdown to plain text if needed
143
+ return md_text
144
+
145
+ elif file_extension == 'txt':
146
+ with open(file_path, 'r', encoding='utf-8') as file:
147
+ return file.read()
148
+
149
+ else:
150
+ return f"Unsupported file type: {file_extension}"
151
+
152
+ except Exception as e:
153
+ logger.error(f"Error extracting text from file: {str(e)}")
154
+ return f"Error processing file: {str(e)}"
155
+
156
+ def prepare_message_with_media(text, images=None, documents=None):
157
+ """Prepare a message with text, images, and document content"""
158
+ # If no media, return text only
159
+ if not images and not documents:
160
  return text
161
 
162
+ # Start with text content
163
+ if documents and len(documents) > 0:
164
+ # If there are documents, append their content to the text
165
+ document_texts = []
166
+ for doc in documents:
167
+ if doc is None:
168
+ continue
169
+ doc_text = extract_text_from_file(doc)
170
+ if doc_text:
171
+ document_texts.append(doc_text)
172
+
173
+ # Add document content to text
174
+ if document_texts:
175
+ if not text:
176
+ text = "Please analyze these documents:"
177
+ else:
178
+ text = f"{text}\n\nDocument content:\n\n"
179
+
180
+ text += "\n\n".join(document_texts)
181
+
182
+ # If no images, return text only
183
+ if not images:
184
+ return text
185
+
186
+ # If we have images, create a multimodal content array
187
  content = [{"type": "text", "text": text}]
188
 
189
+ # Add images if any
190
+ if images:
191
+ for img in images:
192
+ if img is None:
193
+ continue
194
+
195
+ encoded_image = encode_image_to_base64(img)
196
+ if encoded_image:
197
+ content.append({
198
+ "type": "image_url",
199
+ "image_url": {"url": encoded_image}
200
+ })
201
 
202
  return content
203
 
204
+ def ask_ai(message, chatbot, model_choice, temperature, max_tokens, top_p, frequency_penalty,
205
+ presence_penalty, images, documents, reasoning_effort):
206
+ """Enhanced AI query function with comprehensive options"""
207
+ if not message.strip() and not images and not documents:
208
  return chatbot, ""
209
 
210
  # Get model ID and context size
211
  model_id = None
212
  context_size = 0
213
+ for name, model_id_value, ctx_size in ALL_MODELS:
214
  if name == model_choice:
215
  model_id = model_id_value
216
  context_size = ctx_size
 
223
  # Create messages from chatbot history
224
  messages = format_to_message_dict(chatbot)
225
 
226
+ # Prepare message with images and documents if any
227
+ content = prepare_message_with_media(message, images, documents)
 
 
 
228
 
229
  # Add current message
230
  messages.append({"role": "user", "content": content})
 
232
  # Call API
233
  try:
234
  logger.info(f"Sending request to model: {model_id}")
 
235
 
236
+ # Build the payload with all parameters
237
  payload = {
238
  "model": model_id,
239
  "messages": messages,
240
  "temperature": temperature,
241
+ "max_tokens": max_tokens,
242
+ "top_p": top_p,
243
+ "frequency_penalty": frequency_penalty,
244
+ "presence_penalty": presence_penalty
245
  }
246
 
247
+ # Add reasoning if selected
248
+ if reasoning_effort != "none":
249
+ payload["reasoning"] = {
250
+ "effort": reasoning_effort
251
+ }
252
+
253
+ logger.info(f"Request payload: {json.dumps(payload, default=str)}")
254
 
255
  response = requests.post(
256
  "https://openrouter.ai/api/v1/chat/completions",
 
260
  "HTTP-Referer": "https://huggingface.co/spaces"
261
  },
262
  json=payload,
263
+ timeout=120 # Longer timeout for document processing
264
  )
265
 
266
  logger.info(f"Response status: {response.status_code}")
 
267
 
268
  response_text = response.text
269
  logger.info(f"Response body: {response_text}")
 
286
  return chatbot, ""
287
 
288
  def clear_chat():
289
+ return [], "", [], [], 0.7, 1000, 0.8, 0.0, 0.0, "none"
290
+
291
+ def filter_models(search_term):
292
+ """Filter models based on search term"""
293
+ if not search_term:
294
+ return gr.Dropdown.update(choices=[model[0] for model in ALL_MODELS], value=ALL_MODELS[0][0])
295
+
296
+ filtered_models = [model[0] for model in ALL_MODELS if search_term.lower() in model[0].lower()]
297
+
298
+ if filtered_models:
299
+ return gr.Dropdown.update(choices=filtered_models, value=filtered_models[0])
300
+ else:
301
+ return gr.Dropdown.update(choices=[model[0] for model in ALL_MODELS], value=ALL_MODELS[0][0])
302
+
303
+ def get_model_info(model_name):
304
+ """Get model information by name"""
305
+ for model in ALL_MODELS:
306
+ if model[0] == model_name:
307
+ return model
308
+ return None
309
+
310
+ def update_context_display(model_name):
311
+ """Update the context size display based on the selected model"""
312
+ model_info = get_model_info(model_name)
313
+ if model_info:
314
+ name, model_id, context_size = model_info
315
+ context_formatted = f"{context_size:,}"
316
+ return f"{context_formatted} tokens"
317
+ return "Unknown"
318
 
319
  # Create enhanced interface
320
+ with gr.Blocks(css="""
321
+ .context-size {
322
+ font-size: 0.9em;
323
+ color: #666;
324
+ margin-left: 10px;
325
+ }
326
+ footer { display: none !important; }
327
+ .model-selection-row {
328
+ display: flex;
329
+ align-items: center;
330
+ }
331
+ .parameter-grid {
332
+ display: grid;
333
+ grid-template-columns: 1fr 1fr;
334
+ gap: 10px;
335
+ }
336
+ """) as demo:
337
  gr.Markdown("""
338
  # Enhanced AI Chat
339
 
340
+ Chat with various AI models from OpenRouter with support for images and documents.
 
341
  """)
342
 
343
  with gr.Row():
344
  with gr.Column(scale=2):
345
+ chatbot = gr.Chatbot(
346
+ height=500,
347
+ show_copy_button=True,
348
+ show_label=False,
349
+ avatar_images=(None, "https://upload.wikimedia.org/wikipedia/commons/0/04/ChatGPT_logo.svg")
350
+ )
351
 
352
  with gr.Row():
353
  message = gr.Textbox(
 
364
  clear_btn = gr.Button("Clear Chat", variant="secondary")
365
 
366
  with gr.Row():
367
+ # Image upload
368
+ with gr.Accordion("Upload Images (for vision models)", open=False):
369
+ images = gr.Gallery(
370
+ label="Uploaded Images",
371
+ show_label=True,
372
+ columns=4,
373
+ height="auto",
374
+ object_fit="contain"
375
+ )
376
+
377
+ image_upload_btn = gr.UploadButton(
378
+ label="Upload Images",
379
+ file_types=["image"],
380
+ file_count="multiple"
381
+ )
382
+
383
+ # Document upload
384
+ with gr.Accordion("Upload Documents (PDF, MD, TXT)", open=False):
385
+ documents = gr.File(
386
+ label="Uploaded Documents",
387
+ file_types=[".pdf", ".md", ".txt"],
388
+ file_count="multiple"
389
+ )
390
 
391
  with gr.Column(scale=1):
392
  with gr.Group():
393
  gr.Markdown("### Model Selection")
 
 
 
 
 
 
394
 
395
+ with gr.Row(elem_classes="model-selection-row"):
396
+ model_search = gr.Textbox(
397
+ placeholder="Search models...",
398
+ label="",
399
+ show_label=False
400
+ )
 
 
 
 
 
401
 
402
+ with gr.Row(elem_classes="model-selection-row"):
403
+ model_choice = gr.Dropdown(
404
+ [model[0] for model in ALL_MODELS],
405
+ value=ALL_MODELS[0][0],
406
+ label="Model"
407
+ )
408
+ context_display = gr.Textbox(
409
+ value=update_context_display(ALL_MODELS[0][0]),
410
+ label="Context",
411
+ interactive=False,
412
+ elem_classes="context-size"
413
+ )
414
+
415
+ # Model category selection
416
+ with gr.Accordion("Browse by Category", open=False):
417
+ model_categories = gr.Radio(
418
+ [category["category"] for category in MODELS],
419
+ label="Categories",
420
+ value=MODELS[0]["category"]
421
+ )
422
+
423
+ category_models = gr.Radio(
424
+ [model[0] for model in MODELS[0]["models"]],
425
+ label="Models in Category"
426
+ )
427
+
428
+ with gr.Accordion("Generation Parameters", open=False):
429
+ with gr.Group(elem_classes="parameter-grid"):
430
+ temperature = gr.Slider(
431
+ minimum=0.0,
432
+ maximum=2.0,
433
+ value=0.7,
434
+ step=0.1,
435
+ label="Temperature"
436
+ )
437
+
438
+ max_tokens = gr.Slider(
439
+ minimum=100,
440
+ maximum=4000,
441
+ value=1000,
442
+ step=100,
443
+ label="Max Tokens"
444
+ )
445
+
446
+ top_p = gr.Slider(
447
+ minimum=0.1,
448
+ maximum=1.0,
449
+ value=0.8,
450
+ step=0.1,
451
+ label="Top P"
452
+ )
453
+
454
+ frequency_penalty = gr.Slider(
455
+ minimum=-2.0,
456
+ maximum=2.0,
457
+ value=0.0,
458
+ step=0.1,
459
+ label="Frequency Penalty"
460
+ )
461
+
462
+ presence_penalty = gr.Slider(
463
+ minimum=-2.0,
464
+ maximum=2.0,
465
+ value=0.0,
466
+ step=0.1,
467
+ label="Presence Penalty"
468
+ )
469
+
470
+ reasoning_effort = gr.Radio(
471
+ ["none", "low", "medium", "high"],
472
+ value="none",
473
+ label="Reasoning Effort"
474
+ )
475
 
476
+ # Connect model search to dropdown filter
477
+ model_search.change(
478
+ fn=filter_models,
479
+ inputs=[model_search],
480
+ outputs=[model_choice]
481
+ )
482
 
483
+ # Update context display when model changes
484
  model_choice.change(
485
+ fn=update_context_display,
486
  inputs=[model_choice],
487
+ outputs=[context_display]
488
+ )
489
+
490
+ # Update model list when category changes
491
+ def update_category_models(category):
492
+ for cat in MODELS:
493
+ if cat["category"] == category:
494
+ return gr.Radio.update(choices=[model[0] for model in cat["models"]], value=cat["models"][0][0])
495
+ return gr.Radio.update(choices=[], value=None)
496
+
497
+ model_categories.change(
498
+ fn=update_category_models,
499
+ inputs=[model_categories],
500
+ outputs=[category_models]
501
+ )
502
+
503
+ # Update main model choice when category model is selected
504
+ category_models.change(
505
+ fn=lambda x: x,
506
+ inputs=[category_models],
507
+ outputs=[model_choice]
508
  )
509
 
510
+ # Process uploaded images
511
+ def process_uploaded_images(files):
512
+ return [file.name for file in files]
 
513
 
514
+ image_upload_btn.upload(
515
+ fn=process_uploaded_images,
516
+ inputs=[image_upload_btn],
517
+ outputs=[images]
518
  )
519
 
520
  # Set up events
521
  submit_btn.click(
522
  fn=ask_ai,
523
+ inputs=[
524
+ message, chatbot, model_choice, temperature, max_tokens,
525
+ top_p, frequency_penalty, presence_penalty, images,
526
+ documents, reasoning_effort
527
+ ],
528
  outputs=[chatbot, message]
529
  )
530
 
531
  message.submit(
532
  fn=ask_ai,
533
+ inputs=[
534
+ message, chatbot, model_choice, temperature, max_tokens,
535
+ top_p, frequency_penalty, presence_penalty, images,
536
+ documents, reasoning_effort
537
+ ],
538
  outputs=[chatbot, message]
539
  )
540
 
541
  clear_btn.click(
542
  fn=clear_chat,
543
  inputs=[],
544
+ outputs=[
545
+ chatbot, message, images, documents, temperature,
546
+ max_tokens, top_p, frequency_penalty, presence_penalty, reasoning_effort
547
+ ]
548
  )
549
 
550
  # Launch directly with Gradio's built-in server