BF667-AI commited on
Commit
12fc364
·
verified ·
1 Parent(s): c69cc1a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +916 -328
app.py CHANGED
@@ -6,6 +6,10 @@ import threading
6
  from itertools import islice
7
  from datetime import datetime
8
  import re
 
 
 
 
9
  import gradio as gr
10
  import torch
11
  from transformers import pipeline, TextIteratorStreamer
@@ -17,54 +21,94 @@ import json
17
  import urllib.parse
18
  from config import MODELS
19
 
20
- # Global event to signal cancellation from the UI thread to the generation thread
21
- cancel_event = threading.Event()
 
22
 
23
- access_token = os.environ.get('HF_TOKEN', '')
 
24
 
25
- # Global cache for pipelines to avoid re-loading.
 
26
  PIPELINES = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
- def google_search_web(query, max_results=6, max_chars=50):
29
- """Search using Google web scraping with multiple approaches"""
 
 
 
 
 
30
 
31
- # Try multiple User-Agents
32
- user_agents = [
 
 
 
 
 
 
 
 
 
 
 
33
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
34
  'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
35
  'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
36
  ]
37
 
38
- for user_agent in user_agents:
39
- try:
40
- # Try different search URLs
41
- search_urls = [
42
- f"https://www.google.com/search?q={quote_plus(query)}&safe=off&num={max_results}",
43
- f"https://www.google.com/search?q={quote_plus(query)}&safe=off&num={max_results}&hl=en",
44
- f"https://www.google.com/webhp?safe=off&q={quote_plus(query)}&num={max_results}"
45
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  for search_url in search_urls:
48
  try:
49
- headers = {
50
- 'User-Agent': user_agent,
51
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
52
- 'Accept-Language': 'en-US,en;q=0.5',
53
- 'Accept-Encoding': 'gzip, deflate',
54
- 'Connection': 'keep-alive',
55
- 'Upgrade-Insecure-Requests': '1',
56
- 'Cache-Control': 'max-age=0'
57
- }
58
-
59
  response = requests.get(search_url, headers=headers, timeout=15, verify=True)
60
  response.raise_for_status()
61
 
62
  soup = BeautifulSoup(response.text, 'html.parser')
63
 
64
- # Find search result containers
65
- results = []
66
-
67
- # Try different selectors
68
  selectors = [
69
  ('div', 'g'),
70
  ('div', 'tF2Cxc'),
@@ -79,256 +123,225 @@ def google_search_web(query, max_results=6, max_chars=50):
79
  break
80
 
81
  if not search_results:
82
- # Try alternative parsing
83
  search_results = soup.find_all('div', class_=re.compile(r'^(g|tF2Cxc|MjjYud|yuRUbf)'))
84
 
 
85
  for result in search_results[:max_results]:
86
  try:
87
- # Get title
88
- title_elem = result.find('h3')
89
  if not title_elem:
90
- title_elem = result.find('h2')
91
- title = title_elem.text if title_elem else "No Title"
92
 
93
- # Get snippet
94
- snippet_elem = result.find('div', class_='VwiC3b')
95
- if not snippet_elem:
96
- snippet_elem = result.find('div', class_='IsZvec')
97
- if not snippet_elem:
98
- snippet_elem = result.find('div', class_='lEBKkf')
99
- snippet = snippet_elem.text if snippet_elem else ""
100
 
101
- # Get link
102
  link_elem = result.find('a')
103
- link = link_elem.get('href') if link_elem else ""
104
- if link and link.startswith('/url?q='):
 
 
 
105
  link = urllib.parse.unquote(link.split('/url?q=')[1].split('&')[0])
106
 
107
- if link and not link.startswith('http'):
108
  continue
109
 
110
- # Clean up snippet
 
111
  snippet = ' '.join(snippet.split())
112
- if len(snippet) > max_chars:
113
- snippet = snippet[:max_chars] + "..."
114
 
115
  if title and snippet:
116
- results.append(f"{len(results)+1}. {title} - {snippet}")
117
 
118
- except Exception:
 
119
  continue
120
 
121
  if results:
122
  return results
123
 
124
- except Exception:
 
125
  continue
126
-
127
- except Exception:
128
- continue
 
 
129
 
130
- return []
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
- def duckduckgo_search(query, max_results=6, max_chars=50):
133
- """Fallback to DuckDuckGo search"""
134
- try:
135
- from ddgs import DDGS
136
- with DDGS() as ddgs:
 
 
 
 
 
 
 
 
137
  results = []
138
- for r in islice(ddgs.text(query, region="wt-wt", safesearch="off", timelimit="y"), max_results):
139
- title = r.get('title', 'No Title')
140
- body = r.get('body', '')
141
- if len(body) > max_chars:
142
- body = body[:max_chars] + "..."
143
- results.append(f"{len(results)+1}. {title} - {body}")
 
 
 
 
 
 
 
 
 
144
  return results
145
- except Exception:
146
- return []
 
147
 
148
- def bing_search(query, max_results=6, max_chars=50):
149
- """Fallback to Bing search"""
150
- try:
151
- headers = {
152
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
153
- }
154
- search_url = f"https://www.bing.com/search?q={quote_plus(query)}&safeSearch=off&count={max_results}"
155
-
156
- response = requests.get(search_url, headers=headers, timeout=10)
157
- response.raise_for_status()
158
-
159
- soup = BeautifulSoup(response.text, 'html.parser')
160
  results = []
161
 
162
- # Find search results
163
- search_results = soup.find_all('li', class_='b_algo')
164
-
165
- for result in search_results[:max_results]:
166
  try:
167
- title_elem = result.find('h2')
168
- title = title_elem.text if title_elem else "No Title"
169
-
170
- snippet_elem = result.find('p')
171
- snippet = snippet_elem.text if snippet_elem else ""
172
-
173
- if len(snippet) > max_chars:
174
- snippet = snippet[:max_chars] + "..."
175
 
176
- if title and snippet:
177
- results.append(f"{len(results)+1}. {title} - {snippet}")
 
178
 
179
- except Exception:
 
180
  continue
181
 
182
  return results
183
- except Exception:
184
- return []
185
 
186
- def retrieve_context(query, max_results=6, max_chars=50):
187
- """
188
- Retrieve search snippets from multiple search engines.
189
- Returns a list of result strings.
190
- """
191
- # Try Google first
192
- results = google_search_web(query, max_results, max_chars)
193
- if results:
194
- print(f"✅ Google search successful: {len(results)} results")
195
- return results
196
 
197
- # Try DuckDuckGo
198
- results = duckduckgo_search(query, max_results, max_chars)
199
- if results:
200
- print(f"✅ DuckDuckGo search successful: {len(results)} results")
201
- return results
202
 
203
- # Try Bing
204
- results = bing_search(query, max_results, max_chars)
205
- if results:
206
- print(f"✅ Bing search successful: {len(results)} results")
207
- return results
208
-
209
- print("❌ All search engines failed")
210
- return []
211
-
212
- def load_pipeline(model_name):
213
- """
214
- Load and cache a transformers pipeline for text generation.
215
- Tries bfloat16, falls back to float16 or float32 if unsupported.
216
- """
217
- global PIPELINES
218
- if model_name in PIPELINES:
219
- return PIPELINES[model_name]
220
- repo = MODELS[model_name]["repo_id"]
221
- tokenizer = AutoTokenizer.from_pretrained(repo, token=access_token)
222
- for dtype in (torch.bfloat16, torch.float16, torch.float32):
223
- try:
 
 
 
 
 
 
 
 
 
224
  pipe = pipeline(
225
  task="text-generation",
226
  model=repo,
227
  tokenizer=tokenizer,
228
  trust_remote_code=True,
229
- dtype=dtype,
230
  device_map="auto",
231
- use_cache=True,
232
- token=access_token)
233
- PIPELINES[model_name] = pipe
234
  return pipe
235
- except Exception:
236
- continue
237
- # Final fallback
238
- pipe = pipeline(
239
- task="text-generation",
240
- model=repo,
241
- tokenizer=tokenizer,
242
- trust_remote_code=True,
243
- device_map="auto",
244
- use_cache=True
245
- )
246
- PIPELINES[model_name] = pipe
247
- return pipe
248
-
249
- def format_conversation(history, system_prompt, tokenizer):
250
- if hasattr(tokenizer, "chat_template") and tokenizer.chat_template:
251
- messages = [{"role": "system", "content": system_prompt.strip()}] + history
252
- return tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, enable_thinking=True)
253
- else:
254
- # Fallback for base LMs without chat template
255
- prompt = system_prompt.strip() + "\n"
256
- for msg in history:
257
- if msg['role'] == 'user':
258
- prompt += "User: " + msg['content'].strip() + "\n"
259
- elif msg['role'] == 'assistant':
260
- prompt += "Assistant: " + msg['content'].strip() + "\n"
261
- if not prompt.strip().endswith("Assistant:"):
262
- prompt += "Assistant: "
263
- return prompt
264
-
265
- def get_duration(user_msg, chat_history, system_prompt, enable_search, max_results, max_chars, model_name, max_tokens, temperature, top_k, top_p, repeat_penalty, search_timeout):
266
- # Get model size from the MODELS dict
267
- model_size = MODELS[model_name].get("params_b", 4.0)
268
-
269
- # Only use AOT for models >= 2B parameters
270
- use_aot = model_size >= 2
271
-
272
- # Adjusted for H200 performance
273
- base_duration = 20 if not use_aot else 40
274
- token_duration = max_tokens * 0.005
275
- search_duration = 10 if enable_search else 0
276
- aot_compilation_buffer = 20 if use_aot else 0
277
-
278
- return base_duration + token_duration + search_duration + aot_compilation_buffer
279
-
280
- def get_model_size(model_name):
281
- """Get model size from the MODELS dict."""
282
- return MODELS.get(model_name, {}).get("params_b", 4.0)
283
 
284
- def chat_response(user_msg, chat_history, system_prompt,
285
- enable_search, max_results, max_chars,
286
- model_name, max_tokens, temperature,
287
- top_k, top_p, repeat_penalty, search_timeout):
288
- """
289
- Generates streaming chat responses, optionally with background web search.
290
- This version includes cancellation support.
291
- """
292
- # Clear the cancellation event at the start of a new generation
293
- cancel_event.clear()
294
 
295
- history = list(chat_history or [])
296
- history.append({'role': 'user', 'content': user_msg})
297
-
298
- # Launch web search if enabled
299
- debug = ''
300
- search_results = []
301
- if enable_search:
302
- debug = '🔍 Searching (Google → DuckDuckGo → Bing)...'
303
- thread_search = threading.Thread(
304
- target=lambda: search_results.extend(
305
- retrieve_context(user_msg, int(max_results), int(max_chars))
306
- )
307
- )
308
- thread_search.daemon = True
309
- thread_search.start()
310
- else:
311
- debug = 'Web search disabled.'
312
-
313
- # Wait for search results if enabled
314
- if enable_search:
315
- thread_search.join(timeout=float(search_timeout))
316
- if search_results:
317
- debug = f"✅ Search completed - Found {len(search_results)} results\n\n" + "\n".join(
318
- f"- {r}" for r in search_results
319
  )
320
  else:
321
- debug = "❌ No search results found. Check internet connection or try again."
322
-
323
- try:
324
- cur_date = datetime.now().strftime('%Y-%m-%d')
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
- # Prepare enriched system prompt
327
- if search_results:
328
- enriched = system_prompt.strip() + f"""
329
  # SEARCH CONTEXT (TRUSTED SOURCES ONLY)
330
  Below are search results. Treat them as the ONLY source of truth for answering.
331
- {search_results}
332
 
333
  RULES (VERY IMPORTANT):
334
  - Do NOT use outside knowledge. Do NOT guess or fill missing information.
@@ -351,143 +364,470 @@ ANSWER POLICY:
351
  - If sources are insufficient, stop and ask for more data instead of guessing.
352
 
353
  DATE CONTEXT:
354
- - Today is {cur_date} (use only for time reference, not for assumptions).
355
 
356
  USER QUESTION:
357
- """
358
- else:
359
- enriched = system_prompt.strip()
360
-
361
- pipe = load_pipeline(model_name)
362
 
363
- prompt = format_conversation(history, enriched, pipe.tokenizer)
364
- prompt_debug = f"\n\n--- Prompt Preview ---\n```\n{prompt}\n```"
365
- streamer = TextIteratorStreamer(pipe.tokenizer,
366
- skip_prompt=True,
367
- skip_special_tokens=True)
368
- gen_thread = threading.Thread(
369
- target=pipe,
370
- args=(prompt,),
371
- kwargs={
372
- 'max_new_tokens': max_tokens,
373
- 'temperature': temperature,
374
- 'top_k': top_k,
375
- 'top_p': top_p,
376
- 'repetition_penalty': repeat_penalty,
377
- 'streamer': streamer,
378
- 'return_full_text': False,
379
- }
380
- )
381
- gen_thread.start()
382
-
383
- # Buffers for thought vs answer
384
  thought_buf = ''
385
  answer_buf = ''
386
  in_thought = False
387
  assistant_message_started = False
388
-
389
- # First yield contains the user message
390
- yield history, debug
391
-
392
- # Stream tokens
393
  for chunk in streamer:
394
- # Check for cancellation signal
395
  if cancel_event.is_set():
396
  if assistant_message_started and history and history[-1]['role'] == 'assistant':
397
  history[-1]['content'] += " [Generation Canceled]"
398
- yield history, debug
399
  break
400
 
401
  text = chunk
402
-
403
- # Detect start of thinking
404
  if not in_thought and '<think>' in text:
405
  in_thought = True
406
- history.append({'role': 'assistant', 'content': '', 'metadata': {'title': '💭 Thought'}})
407
- assistant_message_started = True
 
408
  after = text.split('<think>', 1)[1]
 
 
 
 
 
 
409
  thought_buf += after
 
410
  if '</think>' in thought_buf:
 
411
  before, after2 = thought_buf.split('</think>', 1)
412
- history[-1]['content'] = before.strip()
 
 
413
  in_thought = False
414
  answer_buf = after2
415
- history.append({'role': 'assistant', 'content': answer_buf})
 
 
 
416
  else:
417
- history[-1]['content'] = thought_buf
418
- yield history, debug
419
  continue
420
-
 
 
 
 
 
421
  if in_thought:
422
  thought_buf += text
423
  if '</think>' in thought_buf:
424
- before, after2 = thought_buf.split('</think>', 1)
 
 
 
425
  history[-1]['content'] = before.strip()
426
- in_thought = False
 
 
 
427
  answer_buf = after2
 
 
428
  history.append({'role': 'assistant', 'content': answer_buf})
429
  else:
430
- history[-1]['content'] = thought_buf
431
- yield history, debug
 
 
432
  continue
433
-
 
 
 
 
434
  # Stream answer
435
  if not assistant_message_started:
436
- history.append({'role': 'assistant', 'content': ''})
 
437
  assistant_message_started = True
438
-
 
 
439
  answer_buf += text
440
  history[-1]['content'] = answer_buf.strip()
441
- yield history, debug
 
 
442
 
443
- gen_thread.join()
444
- yield history, debug + prompt_debug
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  except GeneratorExit:
446
- # Handle cancellation gracefully
447
- print("Chat response cancelled.")
448
  return
449
  except Exception as e:
450
- history.append({'role': 'assistant', 'content': f"Error: {e}"})
451
- yield history, debug
 
 
 
 
 
452
  finally:
453
  gc.collect()
454
 
455
- def update_default_prompt(enable_search):
456
- return f"You are a helpful assistant."
 
 
 
 
457
 
458
- def update_duration_estimate(model_name, enable_search, max_results, max_chars, max_tokens, search_timeout):
459
- """Calculate and format the estimated GPU duration for current settings."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  try:
461
- dummy_msg, dummy_history, dummy_system_prompt = "", [], ""
462
- duration = get_duration(dummy_msg, dummy_history, dummy_system_prompt,
463
- enable_search, max_results, max_chars, model_name,
464
- max_tokens, 0.7, 40, 0.9, 1.2, search_timeout)
 
 
 
 
 
 
465
  model_size = get_model_size(model_name)
466
- return (f"⏱️ **Estimated GPU Time: {duration:.1f} seconds**\n\n"
467
- f"📊 **Model Size:** {model_size:.1f}B parameters\n"
468
- f"🔍 **Web Search:** {'Enabled (Multi-Engine)' if enable_search else 'Disabled'}")
 
 
 
 
 
 
469
  except Exception as e:
 
 
 
470
  return f"⚠️ Error calculating estimate: {e}"
471
 
 
 
 
 
 
 
 
 
472
  # ------------------------------
473
- # Gradio UI
 
 
 
 
 
474
  # ------------------------------
475
  with gr.Blocks(
 
476
  title="LLM Inference",
 
 
 
 
 
477
  theme=gr.themes.Soft(
478
  primary_hue="blue",
479
  secondary_hue="blue",
480
  neutral_hue="slate",
481
  radius_size="lg",
 
482
  font=[gr.themes.GoogleFont("Syne"), "Arial", "sans-serif"]
483
  ),
484
  css="""
485
- .duration-estimate { background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); border-left: 4px solid #667eea; padding: 12px; border-radius: 8px; margin: 16px 0; }
486
- .chatbot { border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }
487
- button.primary { font-weight: 600; }
 
 
 
 
 
 
 
 
488
  .gradio-accordion { margin-bottom: 12px; }
489
  """
490
  ) as demo:
 
 
491
  # Header
492
  gr.Markdown("""
493
  # 🧠 LLM Inference with Multi-Engine Search
@@ -495,67 +835,138 @@ with gr.Blocks(
495
 
496
  with gr.Row():
497
  # Left Panel - Configuration
498
- with gr.Column(scale=3):
 
 
 
 
 
 
499
  # Core Settings (Always Visible)
500
  with gr.Group():
501
- gr.Markdown("### ⚙️ Core Settings")
502
- model_dd = gr.Dropdown(
 
 
 
 
 
 
 
503
  label="🤖 Model",
504
  choices=list(MODELS.keys()),
505
  value="Qwen3-1.7B",
506
  info="Select the language model to use"
507
  )
 
 
 
508
  search_chk = gr.Checkbox(
509
  label="🔍 Enable Web Search",
510
  value=False,
511
- info="Search across Google, DuckDuckGo, and Bing (no API required)"
 
 
 
512
  )
513
  sys_prompt = gr.Textbox(label="📝 System Prompt", lines=3, value=update_default_prompt(False), placeholder="Define the assistant's behavior and personality...")
514
 
515
- # Duration Estimate
 
 
 
 
 
 
516
  duration_display = gr.Markdown(
517
- value=update_duration_estimate("Qwen3-1.7B", False, 4, 50, 1024, 5.0),
 
518
  elem_classes="duration-estimate"
 
519
  )
520
 
521
  # Advanced Settings (Collapsible)
522
  with gr.Accordion("🎛️ Advanced Generation Parameters", open=False):
 
 
 
 
 
 
523
  max_tok = gr.Slider(
524
  64, 16384, value=1024, step=32,
525
  label="Max Tokens",
 
526
  info="Maximum length of generated response"
527
  )
 
 
 
528
  temp = gr.Slider(
529
  0.1, 2.0, value=0.7, step=0.1,
 
530
  label="Temperature",
531
  info="Higher = more creative, Lower = more focused"
532
  )
 
 
 
533
  with gr.Row():
534
  k = gr.Slider(
535
- 1, 100, value=40, step=1,
 
 
 
 
 
 
536
  label="Top-K",
537
  info="Number of top tokens to consider"
538
  )
539
  p = gr.Slider(
 
540
  0.1, 1.0, value=0.9, step=0.05,
541
  label="Top-P",
542
  info="Nucleus sampling threshold"
543
  )
 
 
 
 
544
  rp = gr.Slider(
545
- 1.0, 2.0, value=1.2, step=0.1,
 
 
 
546
  label="Repetition Penalty",
547
- info="Penalize repeated tokens"
 
 
 
 
548
  )
549
 
550
  # Web Search Settings (Collapsible)
551
  with gr.Accordion("🌐 Web Search Settings", open=False, visible=False) as search_settings:
 
 
 
 
552
  mr = gr.Number(
553
  value=4, precision=0,
554
  label="Max Results",
555
  info="Number of search results to retrieve"
556
  )
557
  mc = gr.Number(
558
- value=50, precision=0,
 
 
 
 
 
 
 
 
559
  label="Max Chars/Result",
560
  info="Character limit per search result"
561
  )
@@ -565,11 +976,26 @@ with gr.Blocks(
565
  info="Maximum time to wait for search results"
566
  )
567
  gr.Markdown("""
568
- ⚠️ **Search Engines:**
 
 
 
 
569
  - Google (primary)
570
  - DuckDuckGo (fallback)
571
  - Bing (fallback)
572
 
 
 
 
 
 
 
 
 
 
 
 
573
  SafeSearch is **OFF** for comprehensive results.
574
  """)
575
 
@@ -579,15 +1005,29 @@ with gr.Blocks(
579
 
580
  # Right Panel - Chat Interface
581
  with gr.Column(scale=7):
582
- chat = gr.Chatbot(
 
 
 
 
 
 
 
 
583
  type="messages",
584
  height=600,
585
  label="💬 Conversation",
586
  show_copy_button=True,
 
587
  avatar_images=(
588
- "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Crect width='40' height='40' rx='20' fill='%23f093fb'/%3E%3Ctext x='20' y='28' text-anchor='middle' font-size='20' fill='white' font-family='Arial'%3E👤%3C/text%3E%3C/svg%3E",
589
- "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Crect width='40' height='40' rx='20' fill='%23667eea'/%3E%3Ctext x='20' y='28' text-anchor='middle' font-size='20' fill='white' font-family='Arial'%3E🤖%3C/text%3E%3C/svg%3E"
 
 
590
  ),
 
 
 
591
  bubble_full_width=False,
592
  render_markdown=True,
593
  sanitize_html=False
@@ -595,56 +1035,115 @@ with gr.Blocks(
595
 
596
  # Input Area
597
  with gr.Row():
 
 
 
 
 
 
 
598
  txt = gr.Textbox(
599
  placeholder="💭 Type your message here... (Press Enter to send)",
 
600
  scale=9,
601
  container=False,
602
  show_label=False,
603
  lines=1,
604
- max_lines=5
 
 
 
 
 
 
 
605
  )
606
  with gr.Column(scale=1, min_width=120):
607
  submit_btn = gr.Button("📤 Send", variant="primary", size="lg")
608
- cancel_btn = gr.Button("⏹️ Stop", variant="stop", visible=False, size="lg")
 
 
 
 
 
609
 
610
  # Example Prompts
611
  gr.Examples(
612
  examples=[
 
613
  ["Explain quantum computing in simple terms"],
614
  ["Write a Python function to calculate fibonacci numbers"],
615
- ["What are the latest developments in AI? (Enable web search)"],
 
 
 
 
616
  ["Tell me a creative story about a time traveler"],
617
- ["Help me debug this code: def add(a,b): return a+b+1"]
 
 
 
618
  ],
619
  inputs=txt,
620
- label="💡 Example Prompts"
 
 
 
 
 
621
  )
622
 
623
  # Debug/Status Info (Collapsible)
624
  with gr.Accordion("🔍 Debug Info", open=False):
625
  dbg = gr.Markdown()
626
 
 
 
627
  # Footer
628
  gr.Markdown("""
629
  ---
630
  💡 **Tips:**
 
 
 
 
631
  - Use **Advanced Parameters** to fine-tune creativity and response length
 
632
  - Enable **Web Search** for real-time information (uses multiple search engines)
 
633
  - SafeSearch is **OFF** for comprehensive results
634
- - Try different **models** for various tasks (reasoning, coding, general chat)
 
635
  - Click the **Copy** button on responses to save them to your clipboard
636
- """, elem_classes="footer")
 
 
 
 
 
 
637
 
638
  # --- Event Listeners ---
639
 
640
- # Group all inputs for cleaner event handling
641
  chat_inputs = [txt, chat, sys_prompt, search_chk, mr, mc, model_dd, max_tok, temp, k, p, rp, st]
 
 
 
642
  # Group all UI components that can be updated.
643
  ui_components = [chat, dbg, txt, submit_btn, cancel_btn]
644
 
 
 
645
  def submit_and_manage_ui(user_msg, chat_history, *args):
646
  """
647
- Orchestrator function that manages UI state and calls the backend chat function.
 
 
 
 
 
 
648
  """
649
  if not user_msg.strip():
650
  yield {}
@@ -657,86 +1156,175 @@ with gr.Blocks(
657
  cancel_btn: gr.update(visible=True),
658
  }
659
 
 
 
 
 
 
 
 
 
 
660
  cancelled = False
661
  try:
662
  backend_args = [user_msg, chat_history] + list(args)
663
  for response_chunk in chat_response(*backend_args):
664
  yield {
 
 
 
 
 
 
 
 
 
 
665
  chat: response_chunk[0],
666
  dbg: response_chunk[1],
667
  }
668
  except GeneratorExit:
669
  cancelled = True
670
- print("Generation cancelled by user.")
 
 
 
671
  raise
672
  except Exception as e:
673
  print(f"An error occurred during generation: {e}")
 
674
  error_history = (chat_history or []) + [
675
  {'role': 'user', 'content': user_msg},
676
- {'role': 'assistant', 'content': f"**An error occurred:** {str(e)}"}
 
 
 
 
 
 
 
677
  ]
678
  yield {chat: error_history}
679
  finally:
680
  if not cancelled:
681
  print("Resetting UI state.")
 
 
 
 
682
  yield {
683
  txt: gr.update(interactive=True),
684
  submit_btn: gr.update(interactive=True),
685
  cancel_btn: gr.update(visible=False),
686
  }
687
 
 
 
 
 
688
  def set_cancel_flag():
689
  """Called by the cancel button, sets the global event."""
690
  cancel_event.set()
691
  print("Cancellation signal sent.")
692
 
 
 
 
 
693
  def reset_ui_after_cancel():
 
694
  """Reset UI components after cancellation."""
695
  cancel_event.clear()
696
  print("UI reset after cancellation.")
 
 
 
 
697
  return {
698
  txt: gr.update(interactive=True),
 
699
  submit_btn: gr.update(interactive=True),
700
  cancel_btn: gr.update(visible=False),
701
  }
702
 
 
 
 
 
 
703
  # Event for submitting text via Enter key or Submit button
704
  submit_event = txt.submit(
 
705
  fn=submit_and_manage_ui,
706
  inputs=chat_inputs,
707
  outputs=ui_components,
708
  )
709
- submit_btn.click(
 
 
 
 
 
 
 
 
710
  fn=submit_and_manage_ui,
711
  inputs=chat_inputs,
712
  outputs=ui_components,
713
  )
714
 
 
 
715
  # Event for the "Cancel" button.
716
  cancel_btn.click(
717
  fn=set_cancel_flag,
718
  cancels=[submit_event]
719
  ).then(
720
- fn=reset_ui_after_cancel,
 
 
 
 
 
 
 
 
 
721
  outputs=ui_components
722
  )
723
 
724
  # Listeners for updating the duration estimate
 
725
  duration_inputs = [model_dd, search_chk, mr, mc, max_tok, st]
 
 
726
  for component in duration_inputs:
727
  component.change(fn=update_duration_estimate, inputs=duration_inputs, outputs=duration_display)
728
 
 
 
 
729
  # Toggle web search settings visibility
730
  def toggle_search_settings(enabled):
 
731
  return gr.update(visible=enabled)
732
 
 
 
 
733
  search_chk.change(
734
  fn=lambda enabled: (update_default_prompt(enabled), gr.update(visible=enabled)),
735
  inputs=search_chk,
736
  outputs=[sys_prompt, search_settings]
737
  )
738
 
739
- # Clear chat action
 
 
 
 
 
 
740
  clr.click(fn=lambda: ([], "", ""), outputs=[chat, txt, dbg])
741
 
742
  demo.launch(share=True)
 
6
  from itertools import islice
7
  from datetime import datetime
8
  import re
9
+ from typing import List, Dict, Any, Optional, Tuple, Generator
10
+ from dataclasses import dataclass
11
+ from functools import lru_cache
12
+ import logging
13
  import gradio as gr
14
  import torch
15
  from transformers import pipeline, TextIteratorStreamer
 
21
  import urllib.parse
22
  from config import MODELS
23
 
24
+ # Configure logging
25
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
26
+ logger = logging.getLogger(__name__)
27
 
28
+ # Global event for cancellation
29
+ cancel_event = threading.Event()
30
 
31
+ # Constants
32
+ ACCESS_TOKEN = os.environ.get('HF_TOKEN', '')
33
  PIPELINES = {}
34
+ SEARCH_TIMEOUT_DEFAULT = 5.0
35
+ MAX_RETRIES = 3
36
+ RETRY_DELAY = 1
37
+
38
+ # Data classes for better structure
39
+ @dataclass
40
+ class SearchResult:
41
+ title: str
42
+ snippet: str
43
+ url: Optional[str] = None
44
+
45
+ def format(self, max_chars: int = 50) -> str:
46
+ snippet = self.snippet[:max_chars] + "..." if len(self.snippet) > max_chars else self.snippet
47
+ return f"{self.title} - {snippet}"
48
 
49
+ @dataclass
50
+ class GenerationConfig:
51
+ max_tokens: int = 1024
52
+ temperature: float = 0.7
53
+ top_k: int = 40
54
+ top_p: float = 0.9
55
+ repetition_penalty: float = 1.2
56
 
57
+ def to_dict(self) -> Dict[str, Any]:
58
+ return {
59
+ 'max_new_tokens': self.max_tokens,
60
+ 'temperature': self.temperature,
61
+ 'top_k': self.top_k,
62
+ 'top_p': self.top_p,
63
+ 'repetition_penalty': self.repetition_penalty,
64
+ }
65
+
66
+ class SearchEngine:
67
+ """Base class for search engines with common functionality"""
68
+
69
+ USER_AGENTS = [
70
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
71
  'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
72
  'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
73
  ]
74
 
75
+ @staticmethod
76
+ def _get_headers() -> Dict[str, str]:
77
+ return {
78
+ 'User-Agent': SearchEngine.USER_AGENTS[0],
79
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
80
+ 'Accept-Language': 'en-US,en;q=0.5',
81
+ 'Accept-Encoding': 'gzip, deflate',
82
+ 'Connection': 'keep-alive',
83
+ 'Upgrade-Insecure-Requests': '1',
84
+ 'Cache-Control': 'max-age=0'
85
+ }
86
+
87
+ class GoogleSearch(SearchEngine):
88
+ """Google search implementation"""
89
+
90
+ @staticmethod
91
+ def search(query: str, max_results: int = 6, max_chars: int = 50) -> List[SearchResult]:
92
+ """Perform Google search with multiple fallback strategies"""
93
+ encoded_query = quote_plus(query)
94
+ search_urls = [
95
+ f"https://www.google.com/search?q={encoded_query}&safe=off&num={max_results}",
96
+ f"https://www.google.com/search?q={encoded_query}&safe=off&num={max_results}&hl=en",
97
+ f"https://www.google.com/webhp?safe=off&q={encoded_query}&num={max_results}"
98
+ ]
99
+
100
+ for user_agent in SearchEngine.USER_AGENTS:
101
+ headers = SearchEngine._get_headers()
102
+ headers['User-Agent'] = user_agent
103
 
104
  for search_url in search_urls:
105
  try:
 
 
 
 
 
 
 
 
 
 
106
  response = requests.get(search_url, headers=headers, timeout=15, verify=True)
107
  response.raise_for_status()
108
 
109
  soup = BeautifulSoup(response.text, 'html.parser')
110
 
111
+ # Try multiple selectors
 
 
 
112
  selectors = [
113
  ('div', 'g'),
114
  ('div', 'tF2Cxc'),
 
123
  break
124
 
125
  if not search_results:
 
126
  search_results = soup.find_all('div', class_=re.compile(r'^(g|tF2Cxc|MjjYud|yuRUbf)'))
127
 
128
+ results = []
129
  for result in search_results[:max_results]:
130
  try:
131
+ # Extract title
132
+ title_elem = result.find('h3') or result.find('h2')
133
  if not title_elem:
134
+ continue
 
135
 
136
+ # Extract snippet
137
+ snippet_elem = result.find('div', class_='VwiC3b') or \
138
+ result.find('div', class_='IsZvec') or \
139
+ result.find('div', class_='lEBKkf')
 
 
 
140
 
141
+ # Extract link
142
  link_elem = result.find('a')
143
+ if not link_elem:
144
+ continue
145
+
146
+ link = link_elem.get('href', '')
147
+ if link.startswith('/url?q='):
148
  link = urllib.parse.unquote(link.split('/url?q=')[1].split('&')[0])
149
 
150
+ if not link.startswith('http'):
151
  continue
152
 
153
+ title = title_elem.text.strip()
154
+ snippet = snippet_elem.text.strip() if snippet_elem else ""
155
  snippet = ' '.join(snippet.split())
 
 
156
 
157
  if title and snippet:
158
+ results.append(SearchResult(title=title, snippet=snippet, url=link))
159
 
160
+ except Exception as e:
161
+ logger.debug(f"Error parsing Google result: {e}")
162
  continue
163
 
164
  if results:
165
  return results
166
 
167
+ except Exception as e:
168
+ logger.debug(f"Google search attempt failed: {e}")
169
  continue
170
+
171
+ return []
172
+
173
+ class DuckDuckGoSearch(SearchEngine):
174
+ """DuckDuckGo search implementation"""
175
 
176
+ @staticmethod
177
+ def search(query: str, max_results: int = 6, max_chars: int = 50) -> List[SearchResult]:
178
+ try:
179
+ from ddgs import DDGS
180
+ with DDGS() as ddgs:
181
+ results = []
182
+ for r in islice(ddgs.text(query, region="wt-wt", safesearch="off", timelimit="y"), max_results):
183
+ title = r.get('title', 'No Title')
184
+ body = r.get('body', '')
185
+ results.append(SearchResult(title=title, snippet=body))
186
+ return results
187
+ except Exception as e:
188
+ logger.debug(f"DuckDuckGo search failed: {e}")
189
+ return []
190
 
191
+ class BingSearch(SearchEngine):
192
+ """Bing search implementation"""
193
+
194
+ @staticmethod
195
+ def search(query: str, max_results: int = 6, max_chars: int = 50) -> List[SearchResult]:
196
+ try:
197
+ headers = SearchEngine._get_headers()
198
+ search_url = f"https://www.bing.com/search?q={quote_plus(query)}&safeSearch=off&count={max_results}"
199
+
200
+ response = requests.get(search_url, headers=headers, timeout=10)
201
+ response.raise_for_status()
202
+
203
+ soup = BeautifulSoup(response.text, 'html.parser')
204
  results = []
205
+
206
+ for result in soup.find_all('li', class_='b_algo')[:max_results]:
207
+ try:
208
+ title_elem = result.find('h2')
209
+ snippet_elem = result.find('p')
210
+
211
+ if title_elem and snippet_elem:
212
+ title = title_elem.text.strip()
213
+ snippet = snippet_elem.text.strip()
214
+ results.append(SearchResult(title=title, snippet=snippet))
215
+
216
+ except Exception as e:
217
+ logger.debug(f"Error parsing Bing result: {e}")
218
+ continue
219
+
220
  return results
221
+ except Exception as e:
222
+ logger.debug(f"Bing search failed: {e}")
223
+ return []
224
 
225
+ class SearchManager:
226
+ """Manages multiple search engines with fallback mechanism"""
227
+
228
+ _engines = [
229
+ GoogleSearch,
230
+ DuckDuckGoSearch,
231
+ BingSearch
232
+ ]
233
+
234
+ @classmethod
235
+ def search(cls, query: str, max_results: int = 6, max_chars: int = 50, timeout: float = 5.0) -> List[SearchResult]:
236
+ """Search across all engines with timeout"""
237
  results = []
238
 
239
+ for engine_cls in cls._engines:
 
 
 
240
  try:
241
+ # Use threading with timeout
242
+ result_container = []
243
+ search_thread = threading.Thread(
244
+ target=lambda: result_container.extend(engine_cls.search(query, max_results, max_chars))
245
+ )
246
+ search_thread.daemon = True
247
+ search_thread.start()
248
+ search_thread.join(timeout=timeout)
249
 
250
+ if result_container:
251
+ logger.info(f"Search successful with {engine_cls.__name__}: {len(result_container)} results")
252
+ return result_container
253
 
254
+ except Exception as e:
255
+ logger.warning(f"Search engine {engine_cls.__name__} failed: {e}")
256
  continue
257
 
258
  return results
 
 
259
 
260
+ class ModelManager:
261
+ """Manages model loading and caching"""
 
 
 
 
 
 
 
 
262
 
263
+ _pipelines = {}
264
+ _lock = threading.Lock()
 
 
 
265
 
266
+ @classmethod
267
+ def load_pipeline(cls, model_name: str) -> pipeline:
268
+ """Load and cache pipeline with fallback for dtype"""
269
+ with cls._lock:
270
+ if model_name in cls._pipelines:
271
+ return cls._pipelines[model_name]
272
+
273
+ repo = MODELS[model_name]["repo_id"]
274
+ tokenizer = AutoTokenizer.from_pretrained(repo, token=ACCESS_TOKEN)
275
+
276
+ # Try different dtypes
277
+ for dtype in (torch.bfloat16, torch.float16, torch.float32):
278
+ try:
279
+ pipe = pipeline(
280
+ task="text-generation",
281
+ model=repo,
282
+ tokenizer=tokenizer,
283
+ trust_remote_code=True,
284
+ dtype=dtype,
285
+ device_map="auto",
286
+ use_cache=True,
287
+ token=ACCESS_TOKEN
288
+ )
289
+ cls._pipelines[model_name] = pipe
290
+ return pipe
291
+ except Exception as e:
292
+ logger.warning(f"Failed to load with {dtype}: {e}")
293
+ continue
294
+
295
+ # Final fallback
296
  pipe = pipeline(
297
  task="text-generation",
298
  model=repo,
299
  tokenizer=tokenizer,
300
  trust_remote_code=True,
 
301
  device_map="auto",
302
+ use_cache=True
303
+ )
304
+ cls._pipelines[model_name] = pipe
305
  return pipe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
+ class PromptBuilder:
308
+ """Builds prompts for different models"""
 
 
 
 
 
 
 
 
309
 
310
+ @staticmethod
311
+ def format_conversation(history: List[Dict], system_prompt: str, tokenizer) -> str:
312
+ """Format conversation with proper chat template"""
313
+ if hasattr(tokenizer, "chat_template") and tokenizer.chat_template:
314
+ messages = [{"role": "system", "content": system_prompt.strip()}] + history
315
+ return tokenizer.apply_chat_template(
316
+ messages,
317
+ tokenize=False,
318
+ add_generation_prompt=True,
319
+ enable_thinking=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  )
321
  else:
322
+ # Fallback for base LMs
323
+ prompt = f"{system_prompt.strip()}\n"
324
+ for msg in history:
325
+ role = "User" if msg['role'] == 'user' else "Assistant"
326
+ prompt += f"{role}: {msg['content'].strip()}\n"
327
+
328
+ if not prompt.strip().endswith("Assistant:"):
329
+ prompt += "Assistant: "
330
+ return prompt
331
+
332
+ @staticmethod
333
+ def build_search_context(search_results: List[SearchResult], system_prompt: str, user_query: str) -> str:
334
+ """Build enriched prompt with search context"""
335
+ if not search_results:
336
+ return system_prompt.strip()
337
+
338
+ formatted_results = "\n".join(f"[{i+1}] {r.format()}" for i, r in enumerate(search_results))
339
 
340
+ return f"""{system_prompt.strip()}
341
+
 
342
  # SEARCH CONTEXT (TRUSTED SOURCES ONLY)
343
  Below are search results. Treat them as the ONLY source of truth for answering.
344
+ {formatted_results}
345
 
346
  RULES (VERY IMPORTANT):
347
  - Do NOT use outside knowledge. Do NOT guess or fill missing information.
 
364
  - If sources are insufficient, stop and ask for more data instead of guessing.
365
 
366
  DATE CONTEXT:
367
+ - Today is {datetime.now().strftime('%Y-%m-%d')} (use only for time reference, not for assumptions).
368
 
369
  USER QUESTION:
370
+ {user_query}"""
 
 
 
 
371
 
372
+ class StreamProcessor:
373
+ """Handles streaming token processing"""
374
+
375
+ @staticmethod
376
+ def process_stream(streamer: TextIteratorStreamer, history: List[Dict]) -> Generator[Tuple[List[Dict], str], None, None]:
377
+ """Process streaming tokens and handle thinking tags"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  thought_buf = ''
379
  answer_buf = ''
380
  in_thought = False
381
  assistant_message_started = False
382
+
 
 
 
 
383
  for chunk in streamer:
 
384
  if cancel_event.is_set():
385
  if assistant_message_started and history and history[-1]['role'] == 'assistant':
386
  history[-1]['content'] += " [Generation Canceled]"
387
+ yield history, "Generation canceled by user."
388
  break
389
 
390
  text = chunk
391
+
392
+ # Handle thinking tags
393
  if not in_thought and '<think>' in text:
394
  in_thought = True
395
+ history.append({'role': 'assthought = True
396
+ history.append({'role': 'assistant', 'contentistant', 'content': '', 'metadata': {'title': '💭 Thought'}})
397
+ assistant_message_started': '', 'metadata': {'title': '💭 Thought' = True
398
  after = text.split('<think>', 1)[1]
399
+ thought_buf += after}})
400
+ assistant_message_started = True
401
+ after = text.split('<think>', 1)[1
402
+
403
+ if '</think>' in thought_buf:
404
+ before,]
405
  thought_buf += after
406
+
407
  if '</think>' in thought_buf:
408
+ after2 = thought_buf.split('</think>', 1)
409
  before, after2 = thought_buf.split('</think>', 1)
410
+ history[-1]['content'] history[-1]['content'] = before.strip()
411
+ in_thought = False
412
+ answer_buf = = before.strip()
413
  in_thought = False
414
  answer_buf = after2
415
+ history.append({'role': ' after2
416
+ history.append({'roleassistant', 'content': answer_buf})
417
+ else:
418
+ history[-1]['': 'assistant', 'content': answer_buf})
419
  else:
420
+ content'] = thought_buf
421
+ yield history, ""
422
  continue
423
+
424
+ if in_thought:
425
+ thought_b history[-1]['content'] = thought_buf
426
+ yield history, ""
427
+ continue
428
+
429
  if in_thought:
430
  thought_buf += text
431
  if '</think>' in thought_buf:
432
+ beforeuf += text
433
+ if '</think>' in thought_buf:
434
+ , after2 = thought_buf.split('</think>', 1)
435
+ before, after2 = thought_buf.split('</think>', 1)
436
  history[-1]['content'] = before.strip()
437
+ in_thought = history[-1]['content'] = before.strip()
438
+ in_thought False
439
+ answer_buf = after2
440
+ history.append({'role': = False
441
  answer_buf = after2
442
+ history 'assistant', 'content': answer_buf})
443
+ else:
444
  history.append({'role': 'assistant', 'content': answer_buf})
445
  else:
446
+ history[-1]['content[-1]['content'] = thought_buf
447
+ yield history, ""
448
+ '] = thought_buf
449
+ yield history, ""
450
  continue
451
+
452
+ # Stream answer
453
+ if not assistant_message_started:
454
+ history.append({'role continue
455
+
456
  # Stream answer
457
  if not assistant_message_started:
458
+ ': 'assistant', 'content': ''})
459
+ assistant_message_start history.append({'role': 'assistant', 'content': ''})
460
  assistant_message_started = True
461
+
462
+ answered = True
463
+
464
  answer_buf += text
465
  history[-1]['content'] = answer_buf.strip()
466
+ yield history, "_buf += text
467
+ history[-1]['content'] = answer_buf.strip()
468
+ yield history, ""
469
 
470
+ # Main chat function
471
+ def chat_response(
472
+ user_msg: str,
473
+ chat"
474
+
475
+ # Main chat function
476
+ def chat_response(
477
+ user_msg: str,
478
+ chat_history: List[Dict],
479
+ system_prompt: str,
480
+ _history: List[Dict],
481
+ system_prompt: str,
482
+ enable_search enable_search: bool,
483
+ max_results: int,
484
+ max_ch: bool,
485
+ max_results: int,
486
+ max_chars: int,
487
+ ars: int,
488
+ model_name: str,
489
+ max_tokens: int,
490
+ temperature: float,
491
+ top model_name: str,
492
+ max_tokens: int,
493
+ _k: int,
494
+ top_p temperature: float,
495
+ top_k: int,
496
+ top_p: float,
497
+ repeat: float,
498
+ repeat_penalty: float,
499
+ search_timeout: float
500
+ )_penalty: float,
501
+ search_timeout: float
502
+ ) -> Generator[Tuple[List[Dict], str], None, None]:
503
+ """Generate -> Generator[Tuple[List[Dict], str], None, None]:
504
+ """Generate streaming chat responses with search integration"""
505
+
506
+ cancel streaming chat responses with search integration"""
507
+
508
+ cancel_event.clear()
509
+ history = list(chat_history or [])
510
+ history.append({'role': 'user', 'content': user_msg})
511
+
512
+ #_event.clear()
513
+ history = list(chat_history or [])
514
+ history.append({'role': 'user', 'content': user_msg})
515
+
516
+ # Perform search if enabled
517
+ search_results: List[SearchResult] = []
518
+ search_debug = "Web search disabled."
519
+
520
+ if enable_search:
521
+ search_debug = "🔍 Searching across multiple engines..."
522
+ try:
523
+ search_results = SearchManager.search(
524
+ user_msg,
525
+ int(max_results),
526
+ Perform search if enabled
527
+ search_results: List[SearchResult] = []
528
+ search_debug = "Web search disabled."
529
+
530
+ if enable_search:
531
+ search_debug = "🔍 Searching across multiple engines..."
532
+ try:
533
+ search_results = SearchManager.search(
534
+ user_msg,
535
+ int(max int(max_chars),
536
+ float(search_timeout)
537
+ )
538
+
539
+ if search_results:
540
+ _results),
541
+ int(max_chars),
542
+ float(search_timeout)
543
+ )
544
+
545
+ if search_results:
546
+ search_debug = f"✅ Search completed - Found {len(search_results)} results\n\n" + "\n".join search_debug = f"✅ Search completed - Found {len(search_results)} results\n\n" + "\n".join(
547
+ f"- {r.format(int(max_chars))}" for r in(
548
+ f"- {r.format(int(max_chars))}" for r in search_results
549
+ )
550
+ else:
551
+ search_debug = "❌ No search search_results
552
+ )
553
+ else:
554
+ search_debug = "❌ No search results found. results found. Check internet connection or try again."
555
+ except Exception as e:
556
+ search_debug = f"❌ Check internet connection or try again."
557
+ except Exception as e:
558
+ search Search failed: {str(e)}"
559
+ logger.error(f"Search error: {e}")
560
+
561
+ try:
562
+ # Build prompt
563
+ if enable_debug = f"❌ Search failed: {str(e)}"
564
+ logger.error(f"Search error: {e}")
565
+
566
+ try:
567
+ # Build prompt
568
+ if enable_search and search_results:
569
+ enriched_prompt = PromptBuilder.build_search_context(
570
+ search_results,
571
+ _search and search_results:
572
+ enriched_prompt = PromptBuilder.build_search_context(
573
+ search_results,
574
+ system_prompt, system_prompt,
575
+ user_msg
576
+ )
577
+ else:
578
+ enriched_prompt = system_prompt
579
+ user_msg
580
+ )
581
+ else:
582
+ enriched_prompt = system.strip()
583
+
584
+ # Load model
585
+ pipe = ModelManager.load_pipeline(model_name)
586
+
587
+ # Format prompt
588
+ prompt = PromptBuilder.format_conversation_prompt.strip()
589
+
590
+ # Load model
591
+ pipe = ModelManager.load_pipeline(model_name)
592
+
593
+ # Format prompt
594
+ (history, enriched_prompt, pipe.tokenizer)
595
+ prompt_debug = f"\n\n--- Prompt prompt = PromptBuilder.format_conversation(history, enriched_prompt, pipe.tokenizer)
596
+ prompt_debug = f"\n\n--- Prompt Preview ---\n```\n{prompt[:500]}...\n``` Preview ---\n```\n{prompt[:500]}...\n" if len(prompt) > 500 else f"\n```" if len(prompt) > 500 else f"\n\n--- Prompt Preview\n--- Prompt Preview ---\n```\n{prompt}\n```"
597
+
598
+ # Configure generation
599
+ config = GenerationConfig(
600
+ max_tokens ---\n```\n{prompt}\n```"
601
+
602
+ # Configure generation
603
+ config = GenerationConfig(
604
+ max_tokens=max_t=max_tokens,
605
+ temperature=temperatureokens,
606
+ temperature=temperature,
607
+ top_k=top_k,
608
+ top_p=top_p,
609
+ repetition_penalty=repeat_penalty
610
+ )
611
+
612
+ # Setup,
613
+ top_k=top_k,
614
+ top_p=top_p,
615
+ repetition_penalty=repeat_penalty
616
+ )
617
+
618
+ # Setup streamer
619
+ streamer = TextIteratorStreamer(
620
+ pipe.tokenizer,
621
+ skip_prompt=True,
622
+ skip_special_tokens=True
623
+ )
624
+
625
+ # Start streamer
626
+ streamer = TextIteratorStreamer(
627
+ pipe.tokenizer,
628
+ skip_prompt=True,
629
+ skip_special_tokens=True
630
+ )
631
+
632
+ # Start generation in generation in background thread
633
+ gen_kwargs = config.to_dict()
634
+ gen_kwargs['stream background thread
635
+ gen_kwargs = config.to_dict()
636
+ gener'] = streamer
637
+ gen_kwargs['return_full_text_kwargs['streamer'] = streamer
638
+ gen_kwargs['return_full_text'] = False
639
+
640
+ '] = False
641
+
642
+ gen_thread = threading.Thread(
643
+ target= gen_thread = threading.Thread(
644
+ target=pipe,
645
+ args=(prompt,),
646
+ kwargs=gen_kwargs
647
+ )
648
+ gen_thread.start()
649
+
650
+ # Yield initialpipe,
651
+ args=(prompt,),
652
+ kwargs=gen_kwargs
653
+ )
654
+ gen_thread.start()
655
+
656
+ # Yield initial state state
657
+ yield history, search_debug
658
+
659
+ # Process stream
660
+ for
661
+ yield history, search_debug
662
+
663
+ # Process stream
664
+ for history_update, debug_update in StreamProcessor.process_stream(streamer, history):
665
+ yield history_update history_update, debug_update in StreamProcessor.process_stream(streamer, history):
666
+ yield history_update, debug_update
667
+
668
+ # Wait for completion
669
+ , debug_update
670
+
671
+ # Wait for completion
672
+ gen_thread.join(timeout=5.0)
673
+ yield history, search_debug + prompt_debug
674
+
675
+ gen_thread.join(timeout=5.0)
676
+ yield history, search_debug + prompt_debug
677
+
678
  except GeneratorExit:
679
+ logger.info("Generation except GeneratorExit:
680
+ logger.info("Generation cancelled by user")
681
  return
682
  except Exception as e:
683
+ logger.error(f" cancelled by user")
684
+ return
685
+ except Exception as e:
686
+ logger.error(f"Generation error: {e}")
687
+ history.append({'role': 'assistant', 'content': f"Error: {strGeneration error: {e}")
688
+ history.append({'role': 'assistant', 'content': f"(e)}"})
689
+ yield history, search_debug
690
  finally:
691
  gc.collect()
692
 
693
+ # Utility functions
694
+ def get_model_size(model_name: str) -> float:
695
+ """Error: {str(e)}"})
696
+ yield history, search_debug
697
+ finally:
698
+ gc.collect()
699
 
700
+ # Utility functions
701
+ def get_model_size(model_name: str) ->Get model size in billions of parameters"""
702
+ return MODELS.get(model_name, {}).get("params float:
703
+ """Get model size in billions of parameters"""
704
+ return MODELS.get(model_name, {}).get("params_b", 4_b", 4.0)
705
+
706
+ def get_duration_estimate(
707
+ model_name: str,
708
+ enable_search: bool,
709
+ max_t.0)
710
+
711
+ def get_duration_estimate(
712
+ model_name: str,
713
+ enable_search: bool,
714
+ max_tokens: int,
715
+ search_timeout: float
716
+ ) -> float:
717
+ """Calculateokens: int,
718
+ search_timeout: float
719
+ ) -> float:
720
+ """Calculate estimated GPU duration"""
721
+ model_size = get_model_size(model_name)
722
+ estimated GPU duration"""
723
+ model_size = get_model_size(model_name)
724
+ use_aot = model_size >= 2
725
+
726
+ base use_aot = model_size >= 2
727
+
728
+ _duration = 20 if not use_aot else 40
729
+ token_duration = max_tokens * base_duration = 20 if not use_aot else 40
730
+ token_duration = max_tokens * 0.005
731
+ search_duration = 10 if enable_search else 0
732
+ a0.005
733
+ search_duration = 10 if enable_search else 0
734
+ aot_compilation = 20 if use_aot else 0
735
+
736
+ return base_dot_compilation = 20 if use_aot else 0
737
+
738
+ return base_duration + token_duration + search_duration + aot_compilation
739
+
740
+ def update_duration_estimate(
741
+ modeluration + token_duration + search_duration + aot_compilation
742
+
743
+ def update_duration_estimate(
744
+ model_name: str,
745
+ enable_name: str,
746
+ enable_search: bool,
747
+ max_results: int,
748
+ max_chars: int_search: bool,
749
+ max_results: int,
750
+ max_chars: int,
751
+ max_tokens: int,
752
+ search_timeout: float
753
+ ) -> str:
754
+ """Format duration estimate for display"""
755
  try:
756
+ duration = get_duration_estimate(model_name, enable_search, max_tokens,
757
+ max_tokens: int,
758
+ search_timeout: float
759
+ ) -> str:
760
+ """Format duration estimate for display"""
761
+ try:
762
+ duration = get_duration_estimate(model_name, enable_search, max, search_timeout)
763
+ model_size = get_model_size(model_name)
764
+
765
+ return f"""⏱_tokens, search_timeout)
766
  model_size = get_model_size(model_name)
767
+
768
+ return f **Estimated GPU Time: {duration:.1f} seconds**
769
+
770
+ 📊 **Model Size:** {model_size:.1f}B parameters
771
+ 🔍"""⏱️ **Estimated GPU Time: {duration:.1f} seconds**
772
+
773
+ 📊 **Model Size:** {model_size:.1f}B parameters
774
+ 🔍 ** **Web Search:** {'Enabled (Multi-Engine)' if enable_search else 'Disabled'}"""
775
+ except ExceptionWeb Search:** {'Enabled (Multi-Engine)' if enable_search else 'Disabled'}"""
776
  except Exception as e:
777
+ logger.error(f"Error calculating estimate: {e}")
778
+ return f"⚠️ Error as e:
779
+ logger.error(f"Error calculating estimate: {e}")
780
  return f"⚠️ Error calculating estimate: {e}"
781
 
782
+ def update_default_prompt(enable_search: bool) -> str:
783
+ """Generate calculating estimate: {e}"
784
+
785
+ def update_default_prompt(enable_search: bool) -> str:
786
+ """ default system prompt"""
787
+ return "You are aGenerate default system prompt"""
788
+ return "You are a helpful assistant."
789
+
790
  # ------------------------------
791
+ # Gradio UI (unchanged)
792
+ # ------------------------------
793
+ with gr helpful assistant."
794
+
795
+ # ------------------------------
796
+ # Gradio UI (unchanged)
797
  # ------------------------------
798
  with gr.Blocks(
799
+ .Blocks(
800
  title="LLM Inference",
801
+ theme=gr.themes.Soft(
802
+ primary_hue="blue",
803
+ secondary_hue="blue",
804
+ neutral_hue="slate",
805
+ radius title="LLM Inference",
806
  theme=gr.themes.Soft(
807
  primary_hue="blue",
808
  secondary_hue="blue",
809
  neutral_hue="slate",
810
  radius_size="lg",
811
+ font=[gr_size="lg",
812
  font=[gr.themes.GoogleFont("Syne"), "Arial", "sans-serif"]
813
  ),
814
  css="""
815
+ .themes.GoogleFont("Syne"), "Arial", "sans-serif"]
816
+ ),
817
+ css="""
818
+ .duration-estimate { background: linear-gradient(135deg, #667eea15 .duration-estimate { background: linear-gradient(135deg, #667eea15 0%, #0%, #764ba215 100%); border-left: 4764ba215 px solid #667eea; padding: 12px; border-radius:100%); border-left: 4px solid #667eea; padding: 12px; border-radius: 8px; margin: 16px 0; }
819
+ . 8px; margin: 16px 0; }
820
+ .chatbot { border-radius: 12px; box-shadow: 0 4px 6px -chatbot { border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }
821
+ button.primary {1px rgba(0, 0, 0, 0.1); }
822
+ button.primary { font font-weight: 600; }
823
+ .gradio-accordion { margin-bottom: 12px; }
824
+ """
825
+ -weight: 600; }
826
  .gradio-accordion { margin-bottom: 12px; }
827
  """
828
  ) as demo:
829
+ # Header
830
+ gr.Markdown("") as demo:
831
  # Header
832
  gr.Markdown("""
833
  # 🧠 LLM Inference with Multi-Engine Search
 
835
 
836
  with gr.Row():
837
  # Left Panel - Configuration
838
+ with gr.Column(scale"
839
+ # 🧠 LLM Inference with Multi-Engine Search
840
+ """)
841
+
842
+ with gr.Row():
843
+ # Left Panel - Configuration
844
+ with gr.Column(=3):
845
  # Core Settings (Always Visible)
846
  with gr.Group():
847
+ gr.Markdown("scale=3):
848
+ # Core Settings (Always Visible)
849
+ with gr.Group():
850
+ gr.Markdown### ⚙️ Core Settings")
851
+ model_dd = gr.D("### ⚙️ Core Settings")
852
+ model_dd = grropdown(
853
+ label="🤖 Model",
854
+ choices=list(MODELS.keys()),
855
+ value="Qwen3.Dropdown(
856
  label="🤖 Model",
857
  choices=list(MODELS.keys()),
858
  value="Qwen3-1.7B",
859
  info="Select the language model to use"
860
  )
861
+ search_chk = gr.Check-1.7B",
862
+ info="Select the language model to use"
863
+ )
864
  search_chk = gr.Checkbox(
865
  label="🔍 Enable Web Search",
866
  value=False,
867
+ info="Search across Googlebox(
868
+ label="🔍 Enable Web Search",
869
+ value=False,
870
+ info="Search across Google,, DuckDuckGo, and Bing (no API required)"
871
  )
872
  sys_prompt = gr.Textbox(label="📝 System Prompt", lines=3, value=update_default_prompt(False), placeholder="Define the assistant's behavior and personality...")
873
 
874
+ # DuckDuckGo, and Bing (no API required)"
875
+ )
876
+ sys_prompt = gr.Textbox(label="📝 System Prompt", lines=3, value=update_default_prompt(False), placeholder="Define the assistant's behavior and personality...")
877
+
878
+ Duration Estimate
879
+ duration_display = gr.Markdown(
880
+ value=update_duration_estimate("Qwen3-1. # Duration Estimate
881
  duration_display = gr.Markdown(
882
+ value=update_duration_estimate("Qwen3-1.7B", False7B", False, 4, 50, 1024, 5.0),
883
+ elem_classes="duration-est, 4, 50, 1024, 5.0),
884
  elem_classes="duration-estimate"
885
+ imate"
886
  )
887
 
888
  # Advanced Settings (Collapsible)
889
  with gr.Accordion("🎛️ Advanced Generation Parameters", open=False):
890
+ )
891
+
892
+ # Advanced Settings (Collapsible)
893
+ with gr.Accordion("🎛️ Advanced Generation max_tok = gr.Slider(
894
+ 64, 16384, value=1024, step=32,
895
+ label="Max Tok Parameters", open=False):
896
  max_tok = gr.Slider(
897
  64, 16384, value=1024, step=32,
898
  label="Max Tokens",
899
+ infoens",
900
  info="Maximum length of generated response"
901
  )
902
+ temp = gr.Slider(
903
+ 0.1, ="Maximum length of generated response"
904
+ )
905
  temp = gr.Slider(
906
  0.1, 2.0, value=0.7, step=0.1,
907
+ label="Temperature2.0, value=0.7, step=0.1,
908
  label="Temperature",
909
  info="Higher = more creative, Lower = more focused"
910
  )
911
+ ",
912
+ info="Higher = more creative, Lower = more focused"
913
+ )
914
  with gr.Row():
915
  k = gr.Slider(
916
+ 1, 100, value=40, step=1 with gr.Row():
917
+ k = gr.Slider(
918
+ 1, 100, value=40,
919
+ label="Top-K",
920
+ info="Number of top tokens to consider"
921
+ )
922
+ p = gr.Slider, step=1,
923
  label="Top-K",
924
  info="Number of top tokens to consider"
925
  )
926
  p = gr.Slider(
927
+ (
928
  0.1, 1.0, value=0.9, step=0.05,
929
  label="Top-P",
930
  info="Nucleus sampling threshold"
931
  )
932
+ rp = gr.S0.1, 1.0, value=0.9, step=0.05,
933
+ label="Top-P",
934
+ info="Nucleus sampling threshold"
935
+ )
936
  rp = gr.Slider(
937
+ lider(
938
+ 1.0,1.0, 2.0, value=1.2, step=0.1,
939
+ label="Repetition Penalty",
940
+ info="Penalize 2.0, value=1.2, step=0.1,
941
  label="Repetition Penalty",
942
+ info repeated tokens"
943
+ )
944
+
945
+ # Web Search Settings (Collapsible)
946
+ with gr.Accordion("🌐 Web Search Settings", open=False,="Penalize repeated tokens"
947
  )
948
 
949
  # Web Search Settings (Collapsible)
950
  with gr.Accordion("🌐 Web Search Settings", open=False, visible=False) as search_settings:
951
+ mr = gr.Number(
952
+ value=4, precision=0,
953
+ label="Max Results",
954
+ info="Number of visible=False) as search_settings:
955
  mr = gr.Number(
956
  value=4, precision=0,
957
  label="Max Results",
958
  info="Number of search results to retrieve"
959
  )
960
  mc = gr.Number(
961
+ value=50, precision search results to retrieve"
962
+ )
963
+ mc = gr.Number(
964
+ value=50, precision==0,
965
+ label="Max Chars/Result",
966
+ info="Character limit per search result"
967
+ )
968
+ st = gr.Slider(
969
+ minimum=0.0, maximum=30.0, step=0.50,
970
  label="Max Chars/Result",
971
  info="Character limit per search result"
972
  )
 
976
  info="Maximum time to wait for search results"
977
  )
978
  gr.Markdown("""
979
+ , value=5.0,
980
+ label="Search Timeout (s)",
981
+ info="Maximum time to wait for search results"
982
+ )
983
+ gr.Markdown("" ⚠️ **Search Engines:**
984
  - Google (primary)
985
  - DuckDuckGo (fallback)
986
  - Bing (fallback)
987
 
988
+ "
989
+ ⚠️ **Search Engines:**
990
+ - Google (primary)
991
+ - DuckDuckGo (fallback)
992
+ - Bing ( SafeSearch is **OFF** for comprehensive results.
993
+ """)
994
+
995
+ # Actions
996
+ with gr.Row():
997
+ clr = gr.Button("🗑️ Clearfallback)
998
+
999
  SafeSearch is **OFF** for comprehensive results.
1000
  """)
1001
 
 
1005
 
1006
  # Right Panel - Chat Interface
1007
  with gr.Column(scale=7):
1008
+ chat = Chat", variant="secondary", scale=1)
1009
+
1010
+ # Right Panel - Chat Interface
1011
+ with gr.Column(scale=7):
1012
+ gr.Chatbot(
1013
+ type="messages",
1014
+ height=600,
1015
+ label="💬 Conversation",
1016
+ show chat = gr.Chatbot(
1017
  type="messages",
1018
  height=600,
1019
  label="💬 Conversation",
1020
  show_copy_button=True,
1021
+ _copy_button=True,
1022
  avatar_images=(
1023
+ "data:image avatar_images=(
1024
+ "data:image/svg+xml,%3/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Crect width='40' height='40' rx='20' fill='%23f093fb'/%3E%3Ctext x'%3E%3Crect width='40' height='40' rx='20' fill='%23f093fb'/%3E%3Ctext x='20' y='28' text-anchor='middle' font-size='20' fill='='20' y='28' text-anchor='middle' font-size='20' fill='white' font-family='Arial'%3E👤%3C/text%3E%3C/swhite' font-family='Arial'%3E👤%3C/text%3E%3C/svg%3E",
1025
+ "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'vg%3E",
1026
+ "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Crect width='40' height='40' rx='20' fill='%23667eea'/%3E%3Ctext x='20' y='28' text-anchor='middle' font-size='20' fill='white' font-family='Arial'%%3E%3Crect width='40' height='40' rx='20' fill='%23667eea'/%3E%3Ctext x='20' y='28' text-anchor='middle' font-size='20' fill='white' font-family='3E🤖%3C/text%3E%3C/svg%3E"
1027
  ),
1028
+ Arial'%3E🤖%3C/text%3E%3C/svg%3E"
1029
+ ),
1030
+ bubble_full_width=False,
1031
  bubble_full_width=False,
1032
  render_markdown=True,
1033
  sanitize_html=False
 
1035
 
1036
  # Input Area
1037
  with gr.Row():
1038
+ render_markdown=True,
1039
+ sanitize_html=False
1040
+ )
1041
+
1042
+ # Input Area
1043
+ with gr txt = gr.Textbox(
1044
+ placeholder="💭 Type your message here... (Press Enter to.Row():
1045
  txt = gr.Textbox(
1046
  placeholder="💭 Type your message here... (Press Enter to send)",
1047
+ send)",
1048
  scale=9,
1049
  container=False,
1050
  show_label=False,
1051
  lines=1,
1052
+ max scale=9,
1053
+ container=False,
1054
+ show_label=False,
1055
+ lines=1,
1056
+ max_lines=5_lines=5
1057
+ )
1058
+ with gr.Column(scale=1, min_width=120):
1059
+ submit_btn
1060
  )
1061
  with gr.Column(scale=1, min_width=120):
1062
  submit_btn = gr.Button("📤 Send", variant="primary", size="lg")
1063
+ cancel_btn = gr.Button("⏹️ Stop", variant=" = gr.Button("📤 Send", variant="primary", size="lg")
1064
+ cancel_btn = gr.Button("⏹️stop", visible=False, size="lg")
1065
+
1066
+ # Example Prompts
1067
+ gr.Examples(
1068
+ Stop", variant="stop", visible=False, size="lg")
1069
 
1070
  # Example Prompts
1071
  gr.Examples(
1072
  examples=[
1073
+ ["Explain examples=[
1074
  ["Explain quantum computing in simple terms"],
1075
  ["Write a Python function to calculate fibonacci numbers"],
1076
+ ["What are the latest developments in AI quantum computing in simple terms"],
1077
+ ["Write a Python function to calculate fibonacci numbers"],
1078
+ ["? (Enable web search)"],
1079
+ ["Tell me a creative story about a time traveler"],
1080
+ ["Help me debug this code: def add(a,bWhat are the latest developments in AI? (Enable web search)"],
1081
  ["Tell me a creative story about a time traveler"],
1082
+ ["Help me debug this code: def add(a,b):): return a+b+1"]
1083
+ ],
1084
+ inputs=txt,
1085
+ label="💡 Example Prom return a+b+1"]
1086
  ],
1087
  inputs=txt,
1088
+ label="pts"
1089
+ )
1090
+
1091
+ # Debug/Status Info (Collapsible)
1092
+ with gr.Accordion("🔍 Debug Info", open=False):
1093
+ dbg = gr💡 Example Prompts"
1094
  )
1095
 
1096
  # Debug/Status Info (Collapsible)
1097
  with gr.Accordion("🔍 Debug Info", open=False):
1098
  dbg = gr.Markdown()
1099
 
1100
+ # Footer.Markdown()
1101
+
1102
  # Footer
1103
  gr.Markdown("""
1104
  ---
1105
  💡 **Tips:**
1106
+ - Use **Advanced Parameters** to fine-tune creativity and response
1107
+ gr.Markdown("""
1108
+ ---
1109
+ 💡 **Tips:**
1110
  - Use **Advanced Parameters** to fine-tune creativity and response length
1111
+ length
1112
  - Enable **Web Search** for real-time information (uses multiple search engines)
1113
+ - SafeSearch is **OFF** for comprehensive results - Enable **Web Search** for real-time information (uses multiple search engines)
1114
  - SafeSearch is **OFF** for comprehensive results
1115
+ - Try different **
1116
+ - Try different **models** for various tasks (reasonmodels** for various tasks (reasoning, coding, general chat)
1117
  - Click the **Copy** button on responses to save them to your clipboard
1118
+ ing, coding, general chat)
1119
+ - Click the **Copy** button on responses to save them to your clipboard
1120
+ """, elem_classes="footer """, elem_classes="footer")
1121
+
1122
+ # --- Event Listeners ---
1123
+
1124
+ # Group all inputs")
1125
 
1126
  # --- Event Listeners ---
1127
 
1128
+ # Group all for cleaner event handling
1129
  chat_inputs = [txt, chat, sys_prompt, search_chk, mr, mc, model_dd, max_tok, temp, k, p, rp, st]
1130
+ # Group inputs for cleaner event handling
1131
+ chat_inputs = [txt, chat, sys_prompt, search_chk, mr, mc, model_dd, max_tok, temp, k, p, rp, st all UI components that can be updated.
1132
+ ui_components = [chat, dbg, txt, submit_btn, cancel_btn]
1133
  # Group all UI components that can be updated.
1134
  ui_components = [chat, dbg, txt, submit_btn, cancel_btn]
1135
 
1136
+ def submit]
1137
+
1138
  def submit_and_manage_ui(user_msg, chat_history, *args):
1139
  """
1140
+ Orchestrator_and_manage_ui(user_msg, chat_history, *args):
1141
+ """
1142
+ Orchestrator function that manages UI function that manages UI state and calls the backend chat function.
1143
+ """
1144
+ if not user_msg.strip():
1145
+ yield {}
1146
+ state and calls the backend chat function.
1147
  """
1148
  if not user_msg.strip():
1149
  yield {}
 
1156
  cancel_btn: gr.update(visible=True),
1157
  }
1158
 
1159
+ cancelled = return
1160
+
1161
+ # Update UI to "generating" state
1162
+ yield {
1163
+ txt: gr.update(value="", interactive=False),
1164
+ submit_btn: gr.update(interactive=False),
1165
+ cancel_btn: gr.update(visible=True),
1166
+ }
1167
+
1168
  cancelled = False
1169
  try:
1170
  backend_args = [user_msg, chat_history] + list(args)
1171
  for response_chunk in chat_response(*backend_args):
1172
  yield {
1173
+ chat False
1174
+ try:
1175
+ backend_args = [user_msg, chat_history] + list(args)
1176
+ for response_chunk in chat_response(*backend_args):
1177
+ yield: response_chunk[0],
1178
+ dbg: response_chunk[1],
1179
+ }
1180
+ except GeneratorExit:
1181
+ cancelled = True
1182
+ print("Generation {
1183
  chat: response_chunk[0],
1184
  dbg: response_chunk[1],
1185
  }
1186
  except GeneratorExit:
1187
  cancelled = True
1188
+ cancelled by user.")
1189
+ raise
1190
+ except Exception as e:
1191
+ print(f"An error occurred during generation: { print("Generation cancelled by user.")
1192
  raise
1193
  except Exception as e:
1194
  print(f"An error occurred during generation: {e}")
1195
+ e}")
1196
  error_history = (chat_history or []) + [
1197
  {'role': 'user', 'content': user_msg},
1198
+ error_history = (chat_history or []) + [
1199
+ {'role': 'user', 'content': {'role': 'assistant', 'content': f"**An error occurred:** {str user_msg},
1200
+ {'role': 'assistant', 'content': f"(e)}"}
1201
+ ]
1202
+ yield {chat: error_history}
1203
+ finally:
1204
+ if not cancelled:
1205
+ print("**An error occurred:** {str(e)}"}
1206
  ]
1207
  yield {chat: error_history}
1208
  finally:
1209
  if not cancelled:
1210
  print("Resetting UI state.")
1211
+ yield {
1212
+ txt: gr.update(interactive=True),
1213
+ submit_btn: gr.update(interactive=True),
1214
+ Resetting UI state.")
1215
  yield {
1216
  txt: gr.update(interactive=True),
1217
  submit_btn: gr.update(interactive=True),
1218
  cancel_btn: gr.update(visible=False),
1219
  }
1220
 
1221
+ def set_cancel_flag():
1222
+ """Called by the cancel button, cancel_btn: gr.update(visible=False),
1223
+ }
1224
+
1225
  def set_cancel_flag():
1226
  """Called by the cancel button, sets the global event."""
1227
  cancel_event.set()
1228
  print("Cancellation signal sent.")
1229
 
1230
+ def reset_ui_after sets the global event."""
1231
+ cancel_event.set()
1232
+ print("Cancellation signal sent.")
1233
+
1234
  def reset_ui_after_cancel():
1235
+ _cancel():
1236
  """Reset UI components after cancellation."""
1237
  cancel_event.clear()
1238
  print("UI reset after cancellation.")
1239
+ return {
1240
+ txt: gr.update """Reset UI components after cancellation."""
1241
+ cancel_event.clear()
1242
+ print("UI reset after cancellation.")
1243
  return {
1244
  txt: gr.update(interactive=True),
1245
+ submit_btn: gr.update(interactive=True(interactive=True),
1246
  submit_btn: gr.update(interactive=True),
1247
  cancel_btn: gr.update(visible=False),
1248
  }
1249
 
1250
+ # Event for submitting text via Enter key or Submit button
1251
+ submit),
1252
+ cancel_btn: gr.update(visible=False),
1253
+ }
1254
+
1255
  # Event for submitting text via Enter key or Submit button
1256
  submit_event = txt.submit(
1257
+ fn=submit_and_event = txt.submit(
1258
  fn=submit_and_manage_ui,
1259
  inputs=chat_inputs,
1260
  outputs=ui_components,
1261
  )
1262
+ _manage_ui,
1263
+ inputs=chat_inputs,
1264
+ outputs=ui_components,
1265
+ )
1266
+ submit_ submit_btn.click(
1267
+ fn=submit_and_manage_ui,
1268
+ inputs=chat_inputs,
1269
+ outputs=ui_components,
1270
+ btn.click(
1271
  fn=submit_and_manage_ui,
1272
  inputs=chat_inputs,
1273
  outputs=ui_components,
1274
  )
1275
 
1276
+ # Event )
1277
+
1278
  # Event for the "Cancel" button.
1279
  cancel_btn.click(
1280
  fn=set_cancel_flag,
1281
  cancels=[submit_event]
1282
  ).then(
1283
+ fn=reset for the "Cancel" button.
1284
+ cancel_btn.click(
1285
+ fn=set_cancel_flag,
1286
+ cancels=[submit_event]
1287
+ ).then(
1288
+ fn=reset_ui__ui_after_cancel,
1289
+ outputs=ui_components
1290
+ )
1291
+
1292
+ # Listeners forafter_cancel,
1293
  outputs=ui_components
1294
  )
1295
 
1296
  # Listeners for updating the duration estimate
1297
+ duration updating the duration estimate
1298
  duration_inputs = [model_dd, search_chk, mr, mc, max_tok, st]
1299
+ for component in duration_inputs:
1300
+ component_inputs = [model_dd, search_chk, mr, mc, max_tok, st]
1301
  for component in duration_inputs:
1302
  component.change(fn=update_duration_estimate, inputs=duration_inputs, outputs=duration_display)
1303
 
1304
+ # Toggle web search settings visibility
1305
+ def toggle_search.change(fn=update_duration_estimate, inputs=duration_inputs, outputs=duration_display)
1306
+
1307
  # Toggle web search settings visibility
1308
  def toggle_search_settings(enabled):
1309
+ _settings(enabled):
1310
  return gr.update(visible=enabled)
1311
 
1312
+ search_chk.change(
1313
+ fn=lambda enabled: (update_default_prompt(enabled), gr.update return gr.update(visible=enabled)
1314
+
1315
  search_chk.change(
1316
  fn=lambda enabled: (update_default_prompt(enabled), gr.update(visible=enabled)),
1317
  inputs=search_chk,
1318
  outputs=[sys_prompt, search_settings]
1319
  )
1320
 
1321
+ # Clear chat(visible=enabled)),
1322
+ inputs=search_chk,
1323
+ outputs=[sys_prompt, search_settings]
1324
+ )
1325
+
1326
+ # Clear chat action action
1327
+ clr.click(fn=lambda: ([], "", ""), outputs=[chat, txt
1328
  clr.click(fn=lambda: ([], "", ""), outputs=[chat, txt, dbg])
1329
 
1330
  demo.launch(share=True)