BF667-AI commited on
Commit
6c87f61
·
verified ·
1 Parent(s): 713d2ba

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +672 -115
app.py CHANGED
@@ -8,7 +8,6 @@ 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
@@ -28,12 +27,13 @@ logger = logging.getLogger(__name__)
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
@@ -271,21 +271,34 @@ class ModelManager:
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:
@@ -293,14 +306,18 @@ class ModelManager:
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
 
@@ -320,179 +337,355 @@ class PromptBuilder:
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.
348
- - If the answer is not clearly supported by the search results, say: "Not enough information in the provided sources."
349
- - Every factual statement must be directly supported by at least one citation [citation:X].
350
- - Do NOT add explanations, examples, or background that are not explicitly present in the sources.
351
- - Do NOT paraphrase beyond what is necessary for clarity.
 
 
 
 
 
 
352
  - If sources conflict, mention the conflict and cite both.
353
- - If multiple sources are used, distribute citations per sentence, not only at the end.
 
 
 
354
 
355
  CITATION RULES:
356
  - Use inline citations like this: [citation:1]
 
 
357
  - If multiple sources support a sentence: [citation:1][citation:3]
 
358
  - Never place all citations only at the end.
359
 
360
  ANSWER POLICY:
361
  - Be concise and strictly grounded.
 
 
 
 
 
362
  - No speculation, no assumptions, no "likely", no "probably".
363
- - If the user requests a list, only include items explicitly found in sources.
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': 'assistant', 'content': '', 'metadata': {'title': '💭 Thought'}})
 
 
 
 
 
 
 
 
396
  assistant_message_started = True
397
  after = text.split('<think>', 1)[1]
398
  thought_buf += after
399
 
 
 
 
 
 
400
  if '</think>' in thought_buf:
401
  before, after2 = thought_buf.split('</think>', 1)
402
  history[-1]['content'] = before.strip()
 
 
403
  in_thought = False
 
404
  answer_buf = after2
405
- history.append({'role': 'assistant', 'content': answer_buf})
 
 
406
  else:
 
407
  history[-1]['content'] = thought_buf
408
  yield history, ""
 
 
409
  continue
410
 
 
 
411
  if in_thought:
412
  thought_buf += text
413
- if '</think>' in thought_buf:
414
- before, after2 = thought_buf.split('</think>', 1)
 
 
 
 
415
  history[-1]['content'] = before.strip()
 
416
  in_thought = False
 
417
  answer_buf = after2
418
- history.append({'role': 'assistant', 'content': answer_buf})
 
419
  else:
 
 
420
  history[-1]['content'] = thought_buf
 
421
  yield history, ""
 
422
  continue
423
 
 
 
424
  # Stream answer
425
- if not assistant_message_started:
426
- history.append({'role': 'assistant', 'content': ''})
427
- assistant_message_started = True
 
 
 
 
 
428
 
429
  answer_buf += text
430
  history[-1]['content'] = answer_buf.strip()
 
 
 
 
431
  yield history, ""
432
 
433
  # Main chat function
 
 
434
  def chat_response(
435
  user_msg: str,
 
436
  chat_history: List[Dict],
 
437
  system_prompt: str,
 
438
  enable_search: bool,
439
  max_results: int,
 
 
440
  max_chars: int,
 
441
  model_name: str,
 
442
  max_tokens: int,
443
  temperature: float,
444
  top_k: int,
 
 
 
445
  top_p: float,
446
  repeat_penalty: float,
 
 
447
  search_timeout: float
 
448
  ) -> Generator[Tuple[List[Dict], str], None, None]:
 
449
  """Generate streaming chat responses with search integration"""
450
 
 
 
451
  cancel_event.clear()
452
  history = list(chat_history or [])
453
  history.append({'role': 'user', 'content': user_msg})
454
 
 
 
 
 
455
  # Perform search if enabled
 
456
  search_results: List[SearchResult] = []
 
457
  search_debug = "Web search disabled."
458
 
 
 
459
  if enable_search:
460
- search_debug = "🔍 Searching across multiple engines..."
 
 
 
461
  try:
462
  search_results = SearchManager.search(
463
  user_msg,
 
 
464
  int(max_results),
 
465
  int(max_chars),
466
  float(search_timeout)
467
  )
468
 
469
- if search_results:
470
- search_debug = f"✅ Search completed - Found {len(search_results)} results\n\n" + "\n".join(
471
- f"- {r.format(int(max_chars))}" for r in search_results
 
 
 
 
 
 
 
 
 
472
  )
473
  else:
474
  search_debug = "❌ No search results found. Check internet connection or try again."
 
 
475
  except Exception as e:
476
  search_debug = f"❌ Search failed: {str(e)}"
 
477
  logger.error(f"Search error: {e}")
478
 
 
 
 
479
  try:
480
  # Build prompt
481
  if enable_search and search_results:
 
 
482
  enriched_prompt = PromptBuilder.build_search_context(
483
  search_results,
484
- system_prompt,
 
 
 
 
 
 
485
  user_msg
486
  )
487
  else:
488
  enriched_prompt = system_prompt.strip()
489
 
 
 
490
  # Load model
 
491
  pipe = ModelManager.load_pipeline(model_name)
492
 
 
 
493
  # Format prompt
494
  prompt = PromptBuilder.format_conversation(history, enriched_prompt, pipe.tokenizer)
495
- prompt_debug = f"\n\n--- Prompt Preview ---\n```\n{prompt[:500]}...\n```" if len(prompt) > 500 else f"\n\n--- Prompt Preview ---\n```\n{prompt}\n```"
 
 
 
 
 
 
496
 
497
  # Configure generation
498
  config = GenerationConfig(
@@ -500,71 +693,141 @@ def chat_response(
500
  temperature=temperature,
501
  top_k=top_k,
502
  top_p=top_p,
503
- repetition_penalty=repeat_penalty
 
 
 
 
 
 
 
504
  )
505
 
506
  # Setup streamer
507
  streamer = TextIteratorStreamer(
508
- pipe.tokenizer,
 
 
 
 
509
  skip_prompt=True,
510
  skip_special_tokens=True
511
  )
512
 
513
  # Start generation in background thread
514
- gen_kwargs = config.to_dict()
515
- gen_kwargs['streamer'] = streamer
516
- gen_kwargs['return_full_text'] = False
 
 
 
 
 
 
 
 
517
 
518
  gen_thread = threading.Thread(
 
519
  target=pipe,
 
520
  args=(prompt,),
 
521
  kwargs=gen_kwargs
522
  )
 
 
523
  gen_thread.start()
 
524
 
 
525
  # Yield initial state
 
526
  yield history, search_debug
527
 
528
  # Process stream
529
- for history_update, debug_update in StreamProcessor.process_stream(streamer, history):
 
 
 
 
530
  yield history_update, debug_update
531
 
 
 
532
  # Wait for completion
533
  gen_thread.join(timeout=5.0)
534
- yield history, search_debug + prompt_debug
 
 
 
 
535
 
536
  except GeneratorExit:
537
  logger.info("Generation cancelled by user")
 
 
538
  return
539
- except Exception as e:
 
 
540
  logger.error(f"Generation error: {e}")
541
- history.append({'role': 'assistant', 'content': f"Error: {str(e)}"})
 
 
542
  yield history, search_debug
 
543
  finally:
544
  gc.collect()
545
 
 
 
 
546
  # Utility functions
 
547
  def get_model_size(model_name: str) -> float:
 
548
  """Get model size in billions of parameters"""
549
- return MODELS.get(model_name, {}).get("params_b", 4.0)
 
 
 
550
 
551
  def get_duration_estimate(
552
  model_name: str,
553
  enable_search: bool,
554
  max_tokens: int,
555
  search_timeout: float
 
 
 
 
 
 
 
556
  ) -> float:
557
  """Calculate estimated GPU duration"""
558
  model_size = get_model_size(model_name)
559
  use_aot = model_size >= 2
560
 
 
 
 
 
561
  base_duration = 20 if not use_aot else 40
562
  token_duration = max_tokens * 0.005
563
  search_duration = 10 if enable_search else 0
 
 
564
  aot_compilation = 20 if use_aot else 0
565
 
566
  return base_duration + token_duration + search_duration + aot_compilation
567
 
 
 
 
 
 
568
  def update_duration_estimate(
569
  model_name: str,
570
  enable_search: bool,
@@ -580,32 +843,76 @@ def update_duration_estimate(
580
 
581
  return f"""⏱️ **Estimated GPU Time: {duration:.1f} seconds**
582
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
583
  📊 **Model Size:** {model_size:.1f}B parameters
584
- 🔍 **Web Search:** {'Enabled (Multi-Engine)' if enable_search else 'Disabled'}"""
 
 
585
  except Exception as e:
 
586
  logger.error(f"Error calculating estimate: {e}")
587
- return f"⚠️ Error calculating estimate: {e}"
 
 
 
588
 
589
- def update_default_prompt(enable_search: bool) -> str:
 
590
  """Generate default system prompt"""
591
  return "You are a helpful assistant."
592
 
593
  # ------------------------------
594
- # Gradio UI
 
 
 
 
 
 
 
595
  # ------------------------------
596
  with gr.Blocks(
597
  title="LLM Inference",
 
598
  theme=gr.themes.Soft(
599
  primary_hue="blue",
600
  secondary_hue="blue",
601
  neutral_hue="slate",
 
 
 
 
602
  radius_size="lg",
 
603
  font=[gr.themes.GoogleFont("Syne"), "Arial", "sans-serif"]
604
  ),
605
  css="""
606
- .duration-estimate { background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); border-left: 4px solid #667eea; padding: 12px; border-radius: 8px; margin: 16px 0; }
607
- .chatbot { border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }
 
 
 
 
608
  button.primary { font-weight: 600; }
 
 
 
 
 
 
609
  .gradio-accordion { margin-bottom: 12px; }
610
  """
611
  ) as demo:
@@ -614,40 +921,79 @@ with gr.Blocks(
614
  # 🧠 LLM Inference with Multi-Engine Search
615
  """)
616
 
 
 
 
 
617
  with gr.Row():
618
  # Left Panel - Configuration
 
 
 
619
  with gr.Column(scale=3):
620
  # Core Settings (Always Visible)
 
 
621
  with gr.Group():
622
  gr.Markdown("### ⚙️ Core Settings")
 
623
  model_dd = gr.Dropdown(
624
  label="🤖 Model",
625
  choices=list(MODELS.keys()),
626
  value="Qwen3-1.7B",
627
  info="Select the language model to use"
628
  )
 
 
 
 
 
 
 
629
  search_chk = gr.Checkbox(
630
  label="🔍 Enable Web Search",
631
  value=False,
 
 
632
  info="Search across Google, DuckDuckGo, and Bing (no API required)"
633
  )
 
 
634
  sys_prompt = gr.Textbox(label="📝 System Prompt", lines=3, value=update_default_prompt(False), placeholder="Define the assistant's behavior and personality...")
635
 
636
  # Duration Estimate
637
  duration_display = gr.Markdown(
638
- value=update_duration_estimate("Qwen3-1.7B", False, 4, 50, 1024, 5.0),
 
 
 
 
 
 
 
639
  elem_classes="duration-estimate"
640
  )
641
 
 
 
 
 
642
  # Advanced Settings (Collapsible)
643
  with gr.Accordion("🎛️ Advanced Generation Parameters", open=False):
644
  max_tok = gr.Slider(
645
  64, 16384, value=1024, step=32,
 
646
  label="Max Tokens",
647
  info="Maximum length of generated response"
648
  )
649
  temp = gr.Slider(
650
- 0.1, 2.0, value=0.7, step=0.1,
 
 
 
 
 
 
651
  label="Temperature",
652
  info="Higher = more creative, Lower = more focused"
653
  )
@@ -658,10 +1004,24 @@ with gr.Blocks(
658
  info="Number of top tokens to consider"
659
  )
660
  p = gr.Slider(
 
 
 
 
 
 
 
 
 
 
 
661
  0.1, 1.0, value=0.9, step=0.05,
662
  label="Top-P",
663
  info="Nucleus sampling threshold"
664
  )
 
 
 
665
  rp = gr.Slider(
666
  1.0, 2.0, value=1.2, step=0.1,
667
  label="Repetition Penalty",
@@ -669,23 +1029,48 @@ with gr.Blocks(
669
  )
670
 
671
  # Web Search Settings (Collapsible)
672
- with gr.Accordion("🌐 Web Search Settings", open=False, visible=False) as search_settings:
 
 
 
 
 
 
 
 
 
673
  mr = gr.Number(
674
  value=4, precision=0,
675
  label="Max Results",
 
676
  info="Number of search results to retrieve"
677
  )
678
  mc = gr.Number(
679
  value=50, precision=0,
680
  label="Max Chars/Result",
681
- info="Character limit per search result"
 
 
 
 
 
 
 
682
  )
683
  st = gr.Slider(
 
684
  minimum=0.0, maximum=30.0, step=0.5, value=5.0,
685
  label="Search Timeout (s)",
 
 
686
  info="Maximum time to wait for search results"
687
  )
688
- gr.Markdown("""
 
 
 
 
 
689
  ⚠️ **Search Engines:**
690
  - Google (primary)
691
  - DuckDuckGo (fallback)
@@ -696,145 +1081,301 @@ with gr.Blocks(
696
 
697
  # Actions
698
  with gr.Row():
699
- clr = gr.Button("🗑️ Clear Chat", variant="secondary", scale=1)
 
 
 
 
 
 
 
 
 
 
700
 
701
  # Right Panel - Chat Interface
702
  with gr.Column(scale=7):
703
  chat = gr.Chatbot(
704
  type="messages",
705
- height=600,
 
 
 
706
  label="💬 Conversation",
707
  show_copy_button=True,
708
  avatar_images=(
709
- "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",
710
- "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"
 
 
 
 
 
 
 
 
711
  ),
712
  bubble_full_width=False,
713
  render_markdown=True,
 
 
714
  sanitize_html=False
 
715
  )
716
 
 
 
 
717
  # Input Area
718
  with gr.Row():
 
719
  txt = gr.Textbox(
720
- placeholder="💭 Type your message here... (Press Enter to send)",
 
 
721
  scale=9,
722
  container=False,
 
 
723
  show_label=False,
 
724
  lines=1,
725
  max_lines=5
 
 
726
  )
 
727
  with gr.Column(scale=1, min_width=120):
728
- submit_btn = gr.Button("📤 Send", variant="primary", size="lg")
 
 
729
  cancel_btn = gr.Button("⏹️ Stop", variant="stop", visible=False, size="lg")
730
 
 
 
731
  # Example Prompts
 
 
 
732
  gr.Examples(
733
  examples=[
734
  ["Explain quantum computing in simple terms"],
 
735
  ["Write a Python function to calculate fibonacci numbers"],
736
- ["What are the latest developments in AI? (Enable web search)"],
 
737
  ["Tell me a creative story about a time traveler"],
 
 
 
738
  ["Help me debug this code: def add(a,b): return a+b+1"]
739
  ],
 
740
  inputs=txt,
741
  label="💡 Example Prompts"
 
 
742
  )
743
 
744
  # Debug/Status Info (Collapsible)
745
  with gr.Accordion("🔍 Debug Info", open=False):
746
  dbg = gr.Markdown()
747
 
 
 
 
 
 
 
 
 
 
748
  # Footer
749
  gr.Markdown("""
750
  ---
751
  💡 **Tips:**
752
  - Use **Advanced Parameters** to fine-tune creativity and response length
753
- - Enable **Web Search** for real-time information (uses multiple search engines)
 
 
 
754
  - SafeSearch is **OFF** for comprehensive results
755
- - Try different **models** for various tasks (reasoning, coding, general chat)
756
- - Click the **Copy** button on responses to save them to your clipboard
 
 
 
757
  """, elem_classes="footer")
758
 
759
  # --- Event Listeners ---
760
 
761
- # Group all inputs for cleaner event handling
762
- chat_inputs = [txt, chat, sys_prompt, search_chk, mr, mc, model_dd, max_tok, temp, k, p, rp, st]
763
- # Group all UI components that can be updated.
764
- ui_components = [chat, dbg, txt, submit_btn, cancel_btn]
 
 
 
 
 
 
 
 
 
765
 
766
- def submit_and_manage_ui(user_msg, chat_history, *args):
 
767
  """
768
- Orchestrator function that manages UI state and calls the backend chat function.
 
 
 
769
  """
770
  if not user_msg.strip():
 
771
  yield {}
772
  return
773
 
 
 
 
774
  # Update UI to "generating" state
775
  yield {
776
- txt: gr.update(value="", interactive=False),
 
 
 
777
  submit_btn: gr.update(interactive=False),
 
778
  cancel_btn: gr.update(visible=True),
779
  }
780
 
781
- cancelled = False
 
 
 
 
 
782
  try:
783
- backend_args = [user_msg, chat_history] + list(args)
 
 
 
784
  for response_chunk in chat_response(*backend_args):
785
  yield {
786
  chat: response_chunk[0],
 
787
  dbg: response_chunk[1],
788
  }
789
  except GeneratorExit:
 
 
 
790
  cancelled = True
 
791
  print("Generation cancelled by user.")
792
  raise
793
  except Exception as e:
794
- print(f"An error occurred during generation: {e}")
 
 
 
 
795
  error_history = (chat_history or []) + [
 
796
  {'role': 'user', 'content': user_msg},
 
797
  {'role': 'assistant', 'content': f"**An error occurred:** {str(e)}"}
798
  ]
 
 
799
  yield {chat: error_history}
800
  finally:
 
 
801
  if not cancelled:
 
802
  print("Resetting UI state.")
803
  yield {
804
- txt: gr.update(interactive=True),
 
 
 
805
  submit_btn: gr.update(interactive=True),
806
- cancel_btn: gr.update(visible=False),
 
 
 
 
807
  }
808
 
809
  def set_cancel_flag():
 
810
  """Called by the cancel button, sets the global event."""
811
  cancel_event.set()
 
 
812
  print("Cancellation signal sent.")
813
 
 
 
814
  def reset_ui_after_cancel():
815
  """Reset UI components after cancellation."""
816
  cancel_event.clear()
817
  print("UI reset after cancellation.")
818
  return {
819
  txt: gr.update(interactive=True),
820
- submit_btn: gr.update(interactive=True),
 
 
 
 
 
 
 
821
  cancel_btn: gr.update(visible=False),
822
  }
823
 
824
- # Event for submitting text via Enter key or Submit button
 
 
 
 
825
  submit_event = txt.submit(
826
- fn=submit_and_manage_ui,
 
 
 
827
  inputs=chat_inputs,
828
  outputs=ui_components,
829
  )
 
 
 
830
  submit_btn.click(
831
  fn=submit_and_manage_ui,
832
- inputs=chat_inputs,
 
 
 
 
 
833
  outputs=ui_components,
834
  )
835
 
836
  # Event for the "Cancel" button.
837
- cancel_btn.click(
 
 
 
 
 
 
 
 
 
 
 
838
  fn=set_cancel_flag,
839
  cancels=[submit_event]
840
  ).then(
@@ -845,19 +1386,35 @@ with gr.Blocks(
845
  # Listeners for updating the duration estimate
846
  duration_inputs = [model_dd, search_chk, mr, mc, max_tok, st]
847
  for component in duration_inputs:
848
- component.change(fn=update_duration_estimate, inputs=duration_inputs, outputs=duration_display)
 
 
 
849
 
850
- # Toggle web search settings visibility
851
  def toggle_search_settings(enabled):
852
- return gr.update(visible=enabled)
 
 
 
 
853
 
854
  search_chk.change(
855
- fn=lambda enabled: (update_default_prompt(enabled), gr.update(visible=enabled)),
856
- inputs=search_chk,
 
 
 
857
  outputs=[sys_prompt, search_settings]
858
  )
 
 
859
 
 
860
  # Clear chat action
861
- clr.click(fn=lambda: ([], "", ""), outputs=[chat, txt, dbg])
 
 
 
862
 
863
  demo.launch(share=True)
 
8
  import re
9
  from typing import List, Dict, Any, Optional, Tuple, Generator
10
  from dataclasses import dataclass
 
11
  import logging
12
  import gradio as gr
13
  import torch
 
27
  # Global event for cancellation
28
  cancel_event = threading.Event()
29
 
30
+ # Constants - Handle empty token properly
31
  ACCESS_TOKEN = os.environ.get('HF_TOKEN', '')
32
+ if ACCESS_TOKEN == '':
33
+ ACCESS_TOKEN = None # Convert empty string to None for proper handling
34
+
35
  PIPELINES = {}
36
  SEARCH_TIMEOUT_DEFAULT = 5.0
 
 
37
 
38
  # Data classes for better structure
39
  @dataclass
 
271
  return cls._pipelines[model_name]
272
 
273
  repo = MODELS[model_name]["repo_id"]
274
+
275
+ # Load tokenizer without token if not available
276
+ try:
277
+ tokenizer = AutoTokenizer.from_pretrained(
278
+ repo,
279
+ token=ACCESS_TOKEN if ACCESS_TOKEN else None
280
+ )
281
+ except Exception as e:
282
+ logger.warning(f"Failed to load tokenizer with token, trying without: {e}")
283
+ tokenizer = AutoTokenizer.from_pretrained(repo)
284
 
285
  # Try different dtypes
286
  for dtype in (torch.bfloat16, torch.float16, torch.float32):
287
  try:
288
+ pipe_kwargs = {
289
+ 'task': "text-generation",
290
+ 'model': repo,
291
+ 'tokenizer': tokenizer,
292
+ 'trust_remote_code': True,
293
+ 'dtype': dtype,
294
+ 'device_map': "auto",
295
+ 'use_cache': True,
296
+ }
297
+ # Only add token if it exists
298
+ if ACCESS_TOKEN:
299
+ pipe_kwargs['token'] = ACCESS_TOKEN
300
+
301
+ pipe = pipeline(**pipe_kwargs)
302
  cls._pipelines[model_name] = pipe
303
  return pipe
304
  except Exception as e:
 
306
  continue
307
 
308
  # Final fallback
309
+ pipe_kwargs = {
310
+ 'task': "text-generation",
311
+ 'model': repo,
312
+ 'tokenizer': tokenizer,
313
+ 'trust_remote_code': True,
314
+ 'device_map': "auto",
315
+ 'use_cache': True,
316
+ }
317
+ if ACCESS_TOKEN:
318
+ pipe_kwargs['token'] = ACCESS_TOKEN
319
+
320
+ pipe = pipeline(**pipe_kwargs)
321
  cls._pipelines[model_name] = pipe
322
  return pipe
323
 
 
337
  )
338
  else:
339
  # Fallback for base LMs
340
+ prompt = f"{ prompt = f"{system_prompt.stripsystem_prompt.strip()}\n"
341
+ ()}\n"
342
  for msg in history:
343
+ role for msg in history:
344
+ role = "User" = "User" if msg['role'] == 'user if msg['role'] == 'user' else "Assistant"
345
+ prompt += f"{role}: {msg['content' else "Assistant"
346
+ prompt += f"{role}: {msg['content'].strip()}\'].strip()}\n"
347
 
348
+ n"
349
+
350
+ if not prompt.strip if not prompt.strip().endswith("Assistant:"):
351
+ ().endswith("Assistant:"):
352
+ prompt += " prompt += "Assistant: "
353
+ returnAssistant: "
354
  return prompt
355
 
356
  @staticmethod
357
+ def prompt
358
+
359
+ @staticmethod
360
+ def build_search_context(search_results: List build_search_context(search_results: List[SearchResult], system_prompt: str[SearchResult], system_prompt: str, user_query: str) -> str, user_query: str) -> str:
361
+ """Build enriched prompt with search:
362
  """Build enriched prompt with search context"""
363
  if not search_results:
364
+ context"""
365
+ if not search_results:
366
  return system_prompt.strip()
367
 
368
+ return system_prompt.strip()
369
+
370
+ formatted_results = "\n".join formatted_results = "\n".join(f"[{i(f"[{i+1}] {r.format()}" for i, r+1}] {r.format()}" for i, r in enumerate(search_results in enumerate(search_results))
371
+
372
+ return f"""{))
373
 
374
  return f"""{system_prompt.strip()}
375
 
376
+ #system_prompt.strip()}
377
+
378
+ # SEARCH CONTEXT (TRUSTED SEARCH CONTEXT (TRUSTED SOURCES ONLY)
379
+ Below are search SOURCES ONLY)
380
+ Below are search results. Treat them as the ONLY source results. Treat them as the ONLY source of truth for answering.
381
+ {formatted of truth for answering.
382
  {formatted_results}
383
 
384
+ RULES (VERY_results}
385
+
386
  RULES (VERY IMPORTANT):
387
+ - Do NOT use outside IMPORTANT):
388
+ - Do NOT use outside knowledge. Do NOT knowledge. Do NOT guess or fill missing information.
389
+ - If the answer is guess or fill missing information.
390
+ - If the answer is not clearly supported by not clearly supported by the search results, the search results, say: "Not say: "Not enough information in the provided sources."
391
+ - Every factual statement must enough information in the provided sources."
392
+ - Every factual statement must be directly supported by at least one citation be directly supported by at least one citation [citation:X].
393
+ - Do NOT [citation:X].
394
+ - Do NOT add explanations, examples, or background that add explanations, examples, or background that are not explicitly present in the sources.
395
+ are not explicitly present in the sources.
396
+ - Do NOT paraphrase beyond what is- Do NOT paraphrase beyond what is necessary for clarity.
397
+ - If sources conflict necessary for clarity.
398
  - If sources conflict, mention the conflict and cite both.
399
+ , mention the conflict and cite both.
400
+ - If multiple sources are used, distribute- If multiple sources are used, distribute citations per sentence, not only at the citations per sentence, not only at the end.
401
+
402
+ CITATION RULES end.
403
 
404
  CITATION RULES:
405
  - Use inline citations like this: [citation:1]
406
+ - If multiple sources support:
407
+ - Use inline citations like this: [citation:1]
408
  - If multiple sources support a sentence: [citation:1][citation:3]
409
+ a sentence: [citation:1][citation:3]
410
  - Never place all citations only at the end.
411
 
412
  ANSWER POLICY:
413
  - Be concise and strictly grounded.
414
+ - Never place all citations only at the end.
415
+
416
+ ANSWER POLICY:
417
+ - Be concise- No speculation, no assumptions, no "likely", no "probably".
418
+ - and strictly grounded.
419
  - No speculation, no assumptions, no "likely", no "probably".
420
+ - If the user requests a list, only include items explicitly If the user requests a list, only include items explicitly found in sources.
421
+ found in sources.
422
+ - If sources are insufficient, stop and ask for more data- If sources are insufficient, stop and ask for more data instead of guessing.
423
 
424
+ instead of guessing.
425
+
426
+ DATE CONTEXT:
427
  DATE CONTEXT:
428
+ - Today is {- Today is {datetime.now().strftime('%Y-%mdatetime.now().strftime('%Y-%m-%d')} (use only-%d')} (use only for time reference, not for assumptions).
429
+
430
+ for time reference, not for assumptions).
431
+
432
+ USER QUESTION:
433
+ {user_query}"""
434
 
435
  USER QUESTION:
436
  {user_query}"""
437
 
438
  class StreamProcessor:
439
+ """Handlesclass StreamProcessor:
440
  """Handles streaming token processing"""
441
 
442
+ @staticmethod streaming token processing"""
443
+
444
  @staticmethod
445
+ def process_stream(streamer
446
+ def process_stream(streamer: TextIteratorStreamer, history: TextIteratorStreamer, history: List[Dict]) -> Generator: List[Dict]) -> Generator[Tuple[List[Dict], str], None, None]:
447
  """Process streaming tokens and handle thinking tags"""
448
+ thought[Tuple[List[Dict], str], None, None]:
449
+ """Process streaming_buf = ''
450
+ answer_buf = ''
451
+ in_ tokens and handle thinking tags"""
452
  thought_buf = ''
453
  answer_buf = ''
454
+ inthought = False
455
+ assistant_message_started = False
456
+
457
+ for chunk in streamer:
458
+ _thought = False
459
  assistant_message_started = False
460
 
461
  for chunk in streamer:
462
  if cancel_event.is_set():
463
+ if assistant_message_start if cancel_event.is_set():
464
+ if assistant_message_started and history and history[-1]['role'] == 'ed and history and history[-1]['role'] == 'assistant':
465
+ assistant':
466
+ history[-1]['content'] += " [Generation Cancel history[-1]['content'] += " [Generation Canceled]"
467
+ yield history, "Generation canceled by usered]"
468
  yield history, "Generation canceled by user."
469
+ ."
470
  break
471
 
472
  text = chunk
473
 
474
  # Handle thinking tags
475
+ break
476
+
477
+ text = chunk
478
+
479
+ # Handle thinking tags
480
+ if not in_ if not in_thought and '<think>' in text:
481
+ thought and '<think>' in text:
482
+ in_thought in_thought = True
483
+ history.append({'role': 'assistant = True
484
+ history.append({'role': 'assistant', 'content':', 'content': '', 'metadata': {'title': '💭 Thought'}})
485
+ assistant_message_started = '', 'metadata': {'title': '💭 Thought'}})
486
  assistant_message_started = True
487
  after = text.split('<think>', 1)[1]
488
  thought_buf += after
489
 
490
+ if '</think>' in thought_buf:
491
+ before, after2 = thought_buf.split('</think>', 1 True
492
+ after = text.split('<think>', 1)[1]
493
+ thought_buf += after
494
+
495
  if '</think>' in thought_buf:
496
  before, after2 = thought_buf.split('</think>', 1)
497
  history[-1]['content'] = before.strip()
498
+ in)
499
+ history[-1]['content'] = before.strip()
500
  in_thought = False
501
+ answer_buf =_thought = False
502
  answer_buf = after2
503
+ history.append({'role after2
504
+ history.append({'role': 'assistant', 'content':': 'assistant', 'content': answer_buf})
505
+ answer_buf})
506
  else:
507
+ history[-1]['content'] = thought else:
508
  history[-1]['content'] = thought_buf
509
  yield history, ""
510
+ _buf
511
+ yield history, ""
512
  continue
513
 
514
+ if in_thought continue
515
+
516
  if in_thought:
517
  thought_buf += text
518
+ if '</think>' in thought:
519
+ thought_buf += text
520
+ if '</think>' in thought_b_buf:
521
+ beforeuf:
522
+ before, after, after2 = thought_buf.split2 = thought_buf.split('</think>('</think>', 1)
523
+ history[-1', 1)
524
  history[-1]['content'] = before.strip()
525
+ ]['content'] = before.strip()
526
  in_thought = False
527
+ answer in_thought = False
528
  answer_buf = after2
529
+ history_buf = after2
530
+ history.append({'role': 'assistant',.append({'role': 'assistant', 'content': answer_buf})
531
  else:
532
+ 'content': answer_buf})
533
+ history else:
534
  history[-1]['content'] = thought_buf
535
+ yield[-1]['content'] = thought_buf
536
  yield history, ""
537
+ history, ""
538
  continue
539
 
540
+ # continue
541
+
542
  # Stream answer
543
+ Stream answer
544
+ if not assistant if not assistant_message_started:
545
+ _message_started:
546
+ history.append({'role': 'assistant', 'content history.append({'role': 'assistant', 'content': ''})
547
+ ': ''})
548
+ assistant_message_started assistant_message_started = True
549
+
550
+ = True
551
 
552
  answer_buf += text
553
  history[-1]['content'] = answer_buf.strip()
554
+ answer_buf += text
555
+ history[-1]['content'] yield history, ""
556
+
557
+ # Main chat = answer_buf.strip()
558
  yield history, ""
559
 
560
  # Main chat function
561
+ def chat_response(
562
+ user function
563
  def chat_response(
564
  user_msg: str,
565
+ chat_history:_msg: str,
566
  chat_history: List[Dict],
567
+ system_prompt List[Dict],
568
  system_prompt: str,
569
+ enable_search: bool: str,
570
  enable_search: bool,
571
  max_results: int,
572
+ ,
573
+ max_results: int,
574
  max_chars: int,
575
+ model max_chars: int,
576
  model_name: str,
577
+ max_tokens_name: str,
578
  max_tokens: int,
579
  temperature: float,
580
  top_k: int,
581
+ top: int,
582
+ temperature: float,
583
+ top_k: int,
584
  top_p: float,
585
  repeat_penalty: float,
586
+ _p: float,
587
+ repeat_penalty: float,
588
  search_timeout: float
589
+ ) -> Generator[Tuple search_timeout: float
590
  ) -> Generator[Tuple[List[Dict], str], None, None]:
591
+ """[List[Dict], str], None, None]:
592
  """Generate streaming chat responses with search integration"""
593
 
594
+ cancel_eventGenerate streaming chat responses with search integration"""
595
+
596
  cancel_event.clear()
597
  history = list(chat_history or [])
598
  history.append({'role': 'user', 'content': user_msg})
599
 
600
+ # Perform search.clear()
601
+ history = list(chat_history or [])
602
+ history.append({'role': 'user', 'content': user_msg})
603
+
604
  # Perform search if enabled
605
+ if enabled
606
  search_results: List[SearchResult] = []
607
+ search_debug = " search_results: List[SearchResult] = []
608
  search_debug = "Web search disabled."
609
 
610
+ if enableWeb search disabled."
611
+
612
  if enable_search:
613
+ search_debug = "_search:
614
+ search_debug = "🔍 Searching across multiple engines🔍 Searching across multiple engines..."
615
+ try:
616
+ search_results = SearchManager..."
617
  try:
618
  search_results = SearchManager.search(
619
  user_msg,
620
+ .search(
621
+ user_msg,
622
  int(max_results),
623
+ int(max int(max_results),
624
  int(max_chars),
625
  float(search_timeout)
626
  )
627
 
628
+ _chars),
629
+ float(search_timeout)
630
+ )
631
+
632
+ if search if search_results:
633
+ search_results:
634
+ search_debug = f"✅ Search completed - Found {len_debug = f"✅ Search completed - Found {len(search_results)} results(search_results)} results\n\n" + "\n".join(
635
+ f"-\n\n" + "\n".join(
636
+ f"- {r.format(int(max_chars))}" for r {r.format(int(max_chars))}" for r in search_results
637
+ )
638
+ else:
639
+ search_debug = "❌ No search results found. Check internet connection or try again in search_results
640
  )
641
  else:
642
  search_debug = "❌ No search results found. Check internet connection or try again."
643
+ except Exception as e:
644
+ search_debug = f"❌ Search failed: {."
645
  except Exception as e:
646
  search_debug = f"❌ Search failed: {str(e)}"
647
+ logger.error(f"Search error:str(e)}"
648
  logger.error(f"Search error: {e}")
649
 
650
+ try:
651
+ {e}")
652
+
653
  try:
654
  # Build prompt
655
  if enable_search and search_results:
656
+ # Build prompt
657
+ if enable_search and search_results:
658
  enriched_prompt = PromptBuilder.build_search_context(
659
  search_results,
660
+ enriched_prompt = PromptBuilder.build_search_context(
661
+ search_results,
662
+ system_prompt, system_prompt,
663
+ user_msg
664
+ )
665
+ else:
666
+ enriched
667
  user_msg
668
  )
669
  else:
670
  enriched_prompt = system_prompt.strip()
671
 
672
+ # Load_prompt = system_prompt.strip()
673
+
674
  # Load model
675
+ pipe = ModelManager.load_pipeline(model_name model
676
  pipe = ModelManager.load_pipeline(model_name)
677
 
678
+ #)
679
+
680
  # Format prompt
681
  prompt = PromptBuilder.format_conversation(history, enriched_prompt, pipe.tokenizer)
682
+ Format prompt
683
+ prompt = PromptBuilder.format_conversation(history, enriched_prompt, pipe.tokenizer)
684
+ prompt_debug = f"\n\n--- Prompt Preview ---\n``` prompt_debug = f"\n\n--- Prompt Preview ---\n```\n{prompt[:500]}...\n```" if len(prompt) > 500 else f"\n\n--- Prompt Preview ---\n```\n{prompt}\n```"
685
+
686
+ # Configure generation
687
+ config = GenerationConfig(
688
+ max_tokens\n{prompt[:500]}...\n```" if len(prompt) > 500 else f"\n\n--- Prompt Preview ---\n```\n{prompt}\n```"
689
 
690
  # Configure generation
691
  config = GenerationConfig(
 
693
  temperature=temperature,
694
  top_k=top_k,
695
  top_p=top_p,
696
+ =max_tokens,
697
+ temperature=temperature,
698
+ top_k=top_k,
699
+ top_p=top_p,
700
+ repetition_penalty= repetition_penalty=repeat_penalty
701
+ )
702
+
703
+ # Setuprepeat_penalty
704
  )
705
 
706
  # Setup streamer
707
  streamer = TextIteratorStreamer(
708
+ streamer
709
+ streamer = TextIteratorStreamer(
710
+ pipe.tokenizer pipe.tokenizer,
711
+ skip_prompt=True,
712
+ skip_special_t,
713
  skip_prompt=True,
714
  skip_special_tokens=True
715
  )
716
 
717
  # Start generation in background thread
718
+ genokens=True
719
+ )
720
+
721
+ # Start generation in background thread
722
+ gen_kwargs =_kwargs = config.to_dict()
723
+ config.to_dict()
724
+ gen_kwargs['streamer'] = streamer gen_kwargs['streamer'] = streamer
725
+ gen_k
726
+ gen_kwargs['returnwargs['return_full_text'] = False
727
+
728
+ gen_thread = threading.Thread_full_text'] = False
729
 
730
  gen_thread = threading.Thread(
731
+ target=(
732
  target=pipe,
733
+ argspipe,
734
  args=(prompt,),
735
+ kwargs=gen_kwargs=(prompt,),
736
  kwargs=gen_kwargs
737
  )
738
+
739
+ )
740
  gen_thread.start()
741
+ gen_thread.start()
742
 
743
+ # Yield
744
  # Yield initial state
745
+ yield history, search_debug initial state
746
  yield history, search_debug
747
 
748
  # Process stream
749
+
750
+
751
+ # Process stream
752
+ for history_update, debug_update in for history_update, debug_update in StreamProcessor.process_stream(streamer, history):
753
+ yield history_update StreamProcessor.process_stream(streamer, history):
754
  yield history_update, debug_update
755
 
756
+ # Wait for, debug_update
757
+
758
  # Wait for completion
759
  gen_thread.join(timeout=5.0)
760
+ yield history completion
761
+ gen_thread.join(timeout=5.0)
762
+ , search_debug + prompt_de yield history, search_debug + prompt_debug
763
+
764
+ exceptbug
765
 
766
  except GeneratorExit:
767
  logger.info("Generation cancelled by user")
768
+ GeneratorExit:
769
+ logger.info("Generation cancelled by user")
770
  return
771
+ return
772
+ except Exception as e except Exception as e:
773
+ logger.error:
774
  logger.error(f"Generation error: {e}")
775
+ history.append({'role': 'ass(f"Generation error: {e}")
776
+ history.append({'role': 'assistant', 'content': f"Erroristant', 'content': f"Error: {str(e)}"})
777
+ : {str(e)}"})
778
  yield history, search_debug
779
+ yield history, search_debug
780
  finally:
781
  gc.collect()
782
 
783
+ # finally:
784
+ gc.collect()
785
+
786
  # Utility functions
787
+ def get_model_size(model_name: str) -> Utility functions
788
  def get_model_size(model_name: str) -> float:
789
+ """Get model size in billions float:
790
  """Get model size in billions of parameters"""
791
+ return MODELS.get(model_name of parameters"""
792
+ return MODELS.get(model_name, {}).get("params_b",, {}).get("params_b", 4.0)
793
+
794
+ def get_d 4.0)
795
 
796
  def get_duration_estimate(
797
  model_name: str,
798
  enable_search: bool,
799
  max_tokens: int,
800
  search_timeout: float
801
+ ) -> float:
802
+ """Calculate estimated GPU duration"""
803
+ model_size = get_modeluration_estimate(
804
+ model_name: str,
805
+ enable_search: bool,
806
+ max_tokens: int,
807
+ search_timeout: float
808
  ) -> float:
809
  """Calculate estimated GPU duration"""
810
  model_size = get_model_size(model_name)
811
  use_aot = model_size >= 2
812
 
813
+ base_duration = 20 if not use_aot else 40
814
+ token_duration = max_tokens * 0.005_size(model_name)
815
+ use_aot = model_size >= 2
816
+
817
  base_duration = 20 if not use_aot else 40
818
  token_duration = max_tokens * 0.005
819
  search_duration = 10 if enable_search else 0
820
+ aot_compilation = 20
821
+ search_duration = 10 if enable_search else 0
822
  aot_compilation = 20 if use_aot else 0
823
 
824
  return base_duration + token_duration + search_duration + aot_compilation
825
 
826
+ def update_duration_estimate(
827
+ if use_aot else 0
828
+
829
+ return base_duration + token_duration + search_duration + aot_compilation
830
+
831
  def update_duration_estimate(
832
  model_name: str,
833
  enable_search: bool,
 
843
 
844
  return f"""⏱️ **Estimated GPU Time: {duration:.1f} seconds**
845
 
846
+ model_name: str,
847
+ enable_search: bool,
848
+ max_results: int,
849
+ max_chars: int,
850
+ max_tokens: int,
851
+ search_timeout: float
852
+ ) -> str:
853
+ """Format duration estimate for display"""
854
+ try:
855
+ duration = get_duration_estimate(model_name, enable_search, max_tokens, search_timeout)
856
+ model_size = get_model_size(model_name)
857
+
858
+ return f"""⏱️ **Estimated GPU Time: {duration:.1f} seconds**
859
+
860
  📊 **Model Size:** {model_size:.1f}B parameters
861
+ 📊 **Model Size:** {model_size:.1f}B parameters
862
+ 🔍 **Web Search:** {'Enabled (Multi-Engine)' if enable_search else🔍 **Web Search:** {'Enabled (Multi-Engine)' if enable_search else 'Disabled'}"""
863
+ except Exception as 'Disabled'}"""
864
  except Exception as e:
865
+ logger.error(f"Error e:
866
  logger.error(f"Error calculating estimate: {e}")
867
+ return calculating estimate: {e}")
868
+ return f"⚠️ Error calculating estimate: f"⚠️ Error calculating estimate: {e}"
869
+
870
+ def update_default_p {e}"
871
 
872
+ def update_default_prompt(enable_search: bool) ->rompt(enable_search: bool) -> str:
873
+ """Generate default system str:
874
  """Generate default system prompt"""
875
  return "You are a helpful assistant."
876
 
877
  # ------------------------------
878
+ # Grad prompt"""
879
+ return "You are a helpful assistant."
880
+
881
+ # ------------------------------
882
+ io UI
883
+ # ------------------------------
884
+ with gr.Blocks(
885
+ title="# Gradio UI
886
  # ------------------------------
887
  with gr.Blocks(
888
  title="LLM Inference",
889
+ theme=gr.themes.SoftLLM Inference",
890
  theme=gr.themes.Soft(
891
  primary_hue="blue",
892
  secondary_hue="blue",
893
  neutral_hue="slate",
894
+ (
895
+ primary_hue="blue",
896
+ secondary_hue="blue",
897
+ neutral_hue="slate",
898
  radius_size="lg",
899
+ font=[gr.themes.GoogleFont("Syne"), "Arial", "s radius_size="lg",
900
  font=[gr.themes.GoogleFont("Syne"), "Arial", "sans-serif"]
901
  ),
902
  css="""
903
+ .durationans-serif"]
904
+ ),
905
+ css="""
906
+ .duration-estimate { background: linear-gradient(135deg, #-estimate { background: linear-gradient(667eea15 0%, #764ba215 135deg, #667eea15 0%, #764ba215 100%); border-left100%); border-left: 4px solid #667eea; padding: 12px; border-radius: 8px; margin: 16px 0; }
907
+ .chatbot: 4px solid #667eea; padding: 12px; border-radius: 8px; margin: 16px 0; }
908
+ { border-radius: 12px; box-shadow: 0 4px .chatbot { border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1 6px -1px rgba(0, 0, 0,); }
909
  button.primary { font-weight: 600; }
910
+ 0.1); }
911
+ button.primary { font-weight: 600 .gradio-accordion { margin-bottom: 12px; }
912
+ """
913
+ ) as demo:
914
+ # Header
915
+ gr.Mark; }
916
  .gradio-accordion { margin-bottom: 12px; }
917
  """
918
  ) as demo:
 
921
  # 🧠 LLM Inference with Multi-Engine Search
922
  """)
923
 
924
+ down("""
925
+ # 🧠 LLM Inference with Multi-Engine Search
926
+ """)
927
+
928
  with gr.Row():
929
  # Left Panel - Configuration
930
+ with gr.Column(scale=3):
931
+ # Core with gr.Row():
932
+ # Left Panel - Configuration
933
  with gr.Column(scale=3):
934
  # Core Settings (Always Visible)
935
+ with gr.Group():
936
+ gr.Markdown(" Settings (Always Visible)
937
  with gr.Group():
938
  gr.Markdown("### ⚙️ Core Settings")
939
+ model_dd = gr.Drop### ⚙️ Core Settings")
940
  model_dd = gr.Dropdown(
941
  label="🤖 Model",
942
  choices=list(MODELS.keys()),
943
  value="Qwen3-1.7B",
944
  info="Select the language model to use"
945
  )
946
+ search_chk = gr.Checkbox(
947
+ label="🔍 Enabledown(
948
+ label="🤖 Model",
949
+ choices=list(MODELS.keys()),
950
+ value="Qwen3-1.7B",
951
+ info="Select the language model to use"
952
+ )
953
  search_chk = gr.Checkbox(
954
  label="🔍 Enable Web Search",
955
  value=False,
956
+ info="Search across Google Web Search",
957
+ value=False,
958
  info="Search across Google, DuckDuckGo, and Bing (no API required)"
959
  )
960
+ sys_prompt = gr.Textbox(label="📝, DuckDuckGo, and Bing (no API required)"
961
+ )
962
  sys_prompt = gr.Textbox(label="📝 System Prompt", lines=3, value=update_default_prompt(False), placeholder="Define the assistant's behavior and personality...")
963
 
964
  # Duration Estimate
965
  duration_display = gr.Markdown(
966
+ value=update_duration_estimate(" System Prompt", lines=3, value=update_default_prompt(False), placeholder="Define the assistant's behavior and personality...")
967
+
968
+ # Duration Estimate
969
+ duration_display = gr.Markdown(
970
+ value=update_duration_estimate("Qwen3-Qwen3-1.7B", False, 4, 50, 1024, 5.0),
971
+ elem_classes="duration-estimate"
972
+ )
973
+ 1.7B", False, 4, 50, 1024, 5.0),
974
  elem_classes="duration-estimate"
975
  )
976
 
977
+ # Advanced Settings (Collapsible)
978
+ with gr.Accordion("🎛️ Advanced Generation Parameters", open=False):
979
+ max_tok = gr.Slider(
980
+ 64, 16384, value
981
  # Advanced Settings (Collapsible)
982
  with gr.Accordion("🎛️ Advanced Generation Parameters", open=False):
983
  max_tok = gr.Slider(
984
  64, 16384, value=1024, step=32,
985
+ label="Max=1024, step=32,
986
  label="Max Tokens",
987
  info="Maximum length of generated response"
988
  )
989
  temp = gr.Slider(
990
+ 0.1, 2.0, value=0. Tokens",
991
+ info="Maximum length of generated response"
992
+ )
993
+ temp = gr.Slider(
994
+ 0.1, 2.0,7, step=0.1,
995
+ label="Temperature",
996
+ info="Higher = more creative, Lower value=0.7, step=0.1,
997
  label="Temperature",
998
  info="Higher = more creative, Lower = more focused"
999
  )
 
1004
  info="Number of top tokens to consider"
1005
  )
1006
  p = gr.Slider(
1007
+ = more focused"
1008
+ )
1009
+ with gr.Row():
1010
+ k = gr.Slider(
1011
+ 1, 100, value=40, step=1,
1012
+ label="Top-K",
1013
+ info="Number of top tokens to consider"
1014
+ )
1015
+ p = gr 0.1, 1.0, value=0.9, step=0.05,
1016
+ label="Top-P",
1017
+ .Slider(
1018
  0.1, 1.0, value=0.9, step=0.05,
1019
  label="Top-P",
1020
  info="Nucleus sampling threshold"
1021
  )
1022
+ rp = gr.Slider(
1023
+ 1.0, 2.0, value=1.2, step=0. info="Nucleus sampling threshold"
1024
+ )
1025
  rp = gr.Slider(
1026
  1.0, 2.0, value=1.2, step=0.1,
1027
  label="Repetition Penalty",
 
1029
  )
1030
 
1031
  # Web Search Settings (Collapsible)
1032
+ with gr.Acc1,
1033
+ label="Repetition Penalty",
1034
+ info="Penalize repeated tokens"
1035
+ )
1036
+
1037
+ # Web Search Settings (Collapsible)
1038
+ with gr.Accordion("ordion("🌐 Web Search Settings", open=False, visible=False) as search_settings:
1039
+ mr = gr.Number(
1040
+ value=4, precision=0,
1041
+ label="Max🌐 Web Search Settings", open=False, visible=False) as search_settings:
1042
  mr = gr.Number(
1043
  value=4, precision=0,
1044
  label="Max Results",
1045
+ info="Number of search results to Results",
1046
  info="Number of search results to retrieve"
1047
  )
1048
  mc = gr.Number(
1049
  value=50, precision=0,
1050
  label="Max Chars/Result",
1051
+ info="Character limit per search retrieve"
1052
+ )
1053
+ mc = gr.Number(
1054
+ value=50, precision=0,
1055
+ label="Max Chars/Result",
1056
+ info=" result"
1057
+ )
1058
+ st = grCharacter limit per search result"
1059
  )
1060
  st = gr.Slider(
1061
+ minimum=0.0, maximum=30.0, step=0.5, value=.Slider(
1062
  minimum=0.0, maximum=30.0, step=0.5, value=5.0,
1063
  label="Search Timeout (s)",
1064
+ info5.0,
1065
+ label="Search Timeout (s)",
1066
  info="Maximum time to wait for search results"
1067
  )
1068
+ gr="Maximum time to wait for search results"
1069
+ )
1070
+ gr.Markdown("".Markdown("""
1071
+ ⚠️ **Search Engines:**
1072
+ - Google (primary)
1073
+ - DuckD"
1074
  ⚠️ **Search Engines:**
1075
  - Google (primary)
1076
  - DuckDuckGo (fallback)
 
1081
 
1082
  # Actions
1083
  with gr.Row():
1084
+ clr = gr.Button("uckGo (fallback)
1085
+ - Bing (fallback)
1086
+
1087
+ SafeSearch is **OFF** for comprehensive results.
1088
+ """)
1089
+
1090
+ # Actions
1091
+ with gr.Row():
1092
+ clr =🗑️ Clear Chat", variant="secondary", scale=1 gr.Button("🗑️ Clear Chat", variant="secondary", scale=1)
1093
+
1094
+ # Right Panel - Chat)
1095
 
1096
  # Right Panel - Chat Interface
1097
  with gr.Column(scale=7):
1098
  chat = gr.Chatbot(
1099
  type="messages",
1100
+ Interface
1101
+ with gr.Column(scale=7):
1102
+ chat = gr.Chatbot(
1103
+ type="messages height=600,
1104
  label="💬 Conversation",
1105
  show_copy_button=True,
1106
  avatar_images=(
1107
+ ",
1108
+ height=600,
1109
+ label="💬 Conversation",
1110
+ show_copy_button=True,
1111
+ avatar "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3C_images=(
1112
+ "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Crect width='40rect width='40' height='40' rx='20' fill='%' height='40' rx='20' fill='%23f093fb23f093fb'/%3E%3Ctext x='20' y='28' text-anchor='middle' font-size='20''/%3E%3Ctext x='20' y='28' text-anchor='middle' font-size='20' fill='white' font-family='Arial'%3E👤%3C/text%3E%3 fill='white' font-family='Arial'%3E👤%3C/text%3E%3C/svg%3E",
1113
+ "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'C/svg%3E",
1114
+ "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=' font-family='Arial'%3E🤖%3C/text%3E%3C/svg%3E"
1115
+ ),
1116
+ bubble_fullwhite' font-family='Arial'%3E🤖%3C/text%3E%3C/svg%3E"
1117
  ),
1118
  bubble_full_width=False,
1119
  render_markdown=True,
1120
+ sanit_width=False,
1121
+ render_markdown=True,
1122
  sanitize_html=False
1123
+ ize_html=False
1124
  )
1125
 
1126
+ # Input Area
1127
+ with )
1128
+
1129
  # Input Area
1130
  with gr.Row():
1131
+ txt gr.Row():
1132
  txt = gr.Textbox(
1133
+ placeholder="💭 Type your = gr.Textbox(
1134
+ placeholder="💭 Type your message here... ( message here... (Press Enter to sendPress Enter to send)",
1135
+ scale)",
1136
  scale=9,
1137
  container=False,
1138
+ =9,
1139
+ container=False,
1140
  show_label=False,
1141
+ lines=1 show_label=False,
1142
  lines=1,
1143
  max_lines=5
1144
+ ,
1145
+ max_lines=5
1146
  )
1147
+ with gr.Column(scale= )
1148
  with gr.Column(scale=1, min_width=120):
1149
+ 1, min_width=120):
1150
+ submit_btn = gr.Button("📤 Send", variant="primary", size submit_btn = gr.Button("📤 Send", variant="primary", size="lg")
1151
+ cancel_btn = gr.Button("⏹️ Stop", variant="stop", visible=False="lg")
1152
  cancel_btn = gr.Button("⏹️ Stop", variant="stop", visible=False, size="lg")
1153
 
1154
+ #, size="lg")
1155
+
1156
  # Example Prompts
1157
+ gr.Examples(
1158
+ examples=[
1159
+ ["Explain Example Prompts
1160
  gr.Examples(
1161
  examples=[
1162
  ["Explain quantum computing in simple terms"],
1163
+ ["Write a Python function to quantum computing in simple terms"],
1164
  ["Write a Python function to calculate fibonacci numbers"],
1165
+ ["What are the calculate fibonacci numbers"],
1166
+ ["What are the latest developments in AI? (Enable latest developments in AI? (Enable web search)"],
1167
  ["Tell me a creative story about a time traveler"],
1168
+ web search)"],
1169
+ ["Tell me a creative story about a time traveler"],
1170
+ ["Help me debug this code: def add(a,b): return a+b+1"]
1171
  ["Help me debug this code: def add(a,b): return a+b+1"]
1172
  ],
1173
+ inputs ],
1174
  inputs=txt,
1175
  label="💡 Example Prompts"
1176
+ =txt,
1177
+ label="💡 Example Prompts"
1178
  )
1179
 
1180
  # Debug/Status Info (Collapsible)
1181
  with gr.Accordion("🔍 Debug Info", open=False):
1182
  dbg = gr.Markdown()
1183
 
1184
+ # Footer
1185
+ gr.Markdown("""
1186
+ ---
1187
+ 💡 **Tips )
1188
+
1189
+ # Debug/Status Info (Collapsible)
1190
+ with gr.Accordion("🔍 Debug Info", open=False):
1191
+ dbg = gr.Markdown()
1192
+
1193
  # Footer
1194
  gr.Markdown("""
1195
  ---
1196
  💡 **Tips:**
1197
  - Use **Advanced Parameters** to fine-tune creativity and response length
1198
+ - Enable **Web Search:**
1199
+ - Use **Advanced Parameters** to fine-tune creativity and response length
1200
+ - Enable **Web Search** for real-time information (uses** for real-time information (uses multiple search engines)
1201
+ - SafeSearch is **OFF** multiple search engines)
1202
  - SafeSearch is **OFF** for comprehensive results
1203
+ - Try different ** for comprehensive results
1204
+ - Try different **models** for various tasks (reasonmodels** for various tasks (reasoning, coding, general chat)
1205
+ ing, coding, general chat)
1206
+ - Click the **Copy** button on - Click the **Copy** button on responses to save them to your clipboard
1207
+ responses to save them to your clipboard
1208
  """, elem_classes="footer")
1209
 
1210
  # --- Event Listeners ---
1211
 
1212
+ """, elem_classes="footer")
1213
+
1214
+ # --- Event Listeners ---
1215
+
1216
+ # Group # Group all inputs for cleaner event handling
1217
+ chat_inputs = [txt, chat, sys_p all inputs for cleaner event handling
1218
+ chat_inputs = [txt, chat, sys_prompt, search_chk,rompt, search_chk, mr, mc, model_dd, max mr, mc, model_dd, max_tok, temp, k, p_tok, temp, k, p, rp, st]
1219
+ #, rp, st]
1220
+ # Group all UI components that can Group all UI components that can be updated.
1221
+ ui_components = be updated.
1222
+ ui_components = [chat, dbg, txt, submit [chat, dbg, txt, submit_btn, cancel_btn, cancel_btn]
1223
+
1224
+ def submit_and_manage_ui(user_btn]
1225
 
1226
+ def submit_and_manage_ui(user_msg, chat_history_msg, chat_history, *args):
1227
+ , *args):
1228
  """
1229
+ Orche """
1230
+ Orchestrator function that manages UI state andstrator function that manages UI state and calls the backend chat function.
1231
+ """
1232
+ calls the backend chat function.
1233
  """
1234
  if not user_msg.strip():
1235
+ if not user_msg.strip():
1236
  yield {}
1237
  return
1238
 
1239
+ # yield {}
1240
+ return
1241
+
1242
  # Update UI to "generating" state
1243
  yield {
1244
+ txt: gr.update(value="", interactive Update UI to "generating" state
1245
+ yield {
1246
+ txt: gr.update(value="",=False),
1247
+ submit_btn: gr.update(inter interactive=False),
1248
  submit_btn: gr.update(interactive=False),
1249
+ cancel_active=False),
1250
  cancel_btn: gr.update(visible=True),
1251
  }
1252
 
1253
+ btn: gr.update(visible=True),
1254
+ }
1255
+
1256
+ cancelled = False cancelled = False
1257
+ try:
1258
+ backend_args = [user_msg,
1259
  try:
1260
+ backend_args = [user_msg, chat_history] + chat_history] + list(args)
1261
+ for response_chunk in chat_response(*backend_args):
1262
+ yield {
1263
+ chat list(args)
1264
  for response_chunk in chat_response(*backend_args):
1265
  yield {
1266
  chat: response_chunk[0],
1267
+ dbg: response: response_chunk[0],
1268
  dbg: response_chunk[1],
1269
  }
1270
  except GeneratorExit:
1271
+ _chunk[1],
1272
+ }
1273
+ except GeneratorExit:
1274
  cancelled = True
1275
+ print("Generation cancelled by user cancelled = True
1276
  print("Generation cancelled by user.")
1277
  raise
1278
  except Exception as e:
1279
+ print.")
1280
+ raise
1281
+ except Exception as e:
1282
+ print(f"An error occurred during generation: {e(f"An error occurred during generation: {e}")
1283
+ error_history = (}")
1284
  error_history = (chat_history or []) + [
1285
+ chat_history or []) + [
1286
  {'role': 'user', 'content': user_msg},
1287
+ {'role': 'assistant', 'content': f {'role': 'user', 'content': user_msg},
1288
  {'role': 'assistant', 'content': f"**An error occurred:** {str(e)}"}
1289
  ]
1290
+ yield {"**An error occurred:** {str(e)}"}
1291
+ ]
1292
  yield {chat: error_history}
1293
  finally:
1294
+ chat: error_history}
1295
+ finally:
1296
  if not cancelled:
1297
+ print(" if not cancelled:
1298
  print("Resetting UI state.")
1299
  yield {
1300
+ Resetting UI state.")
1301
+ yield {
1302
+ txt: gr.update(inter txt: gr.update(interactive=True),
1303
+ submit_btn: gractive=True),
1304
  submit_btn: gr.update(interactive=True),
1305
+ .update(interactive=True),
1306
+ cancel_btn: gr.update(visible=False cancel_btn: gr.update(visible=False),
1307
+ }
1308
+
1309
+ def set_c),
1310
  }
1311
 
1312
  def set_cancel_flag():
1313
+ """Called by the cancel button, sets the global eventancel_flag():
1314
  """Called by the cancel button, sets the global event."""
1315
  cancel_event.set()
1316
+ print("Cancellation signal."""
1317
+ cancel_event.set()
1318
  print("Cancellation signal sent.")
1319
 
1320
+ def reset_ui_after_cancel sent.")
1321
+
1322
  def reset_ui_after_cancel():
1323
  """Reset UI components after cancellation."""
1324
  cancel_event.clear()
1325
  print("UI reset after cancellation.")
1326
  return {
1327
  txt: gr.update(interactive=True),
1328
+ ():
1329
+ """Reset UI components after cancellation."""
1330
+ cancel_event.clear()
1331
+ print("UI reset after cancellation.")
1332
+ return {
1333
+ txt: gr.update(interactive=True),
1334
+ submit_btn: submit_btn: gr.update(interactive=True),
1335
+ cancel_btn: gr.update(visible gr.update(interactive=True),
1336
  cancel_btn: gr.update(visible=False),
1337
  }
1338
 
1339
+ # Event for=False),
1340
+ }
1341
+
1342
+ # Event for submitting text via Enter key or Submit submitting text via Enter key or Submit button
1343
+ submit_event = txt button
1344
  submit_event = txt.submit(
1345
+ fn=submit_and.submit(
1346
+ fn=submit_and_manage_ui_manage_ui,
1347
+ inputs=chat_inputs,
1348
+ outputs=ui,
1349
  inputs=chat_inputs,
1350
  outputs=ui_components,
1351
  )
1352
+ submit_btn.click(
1353
+ fn=submit_and_components,
1354
+ )
1355
  submit_btn.click(
1356
  fn=submit_and_manage_ui,
1357
+ inputs_manage_ui,
1358
+ inputs==chat_inputs,
1359
+ outputs=ui_components,
1360
+ )
1361
+
1362
+ # Eventchat_inputs,
1363
  outputs=ui_components,
1364
  )
1365
 
1366
  # Event for the "Cancel" button.
1367
+ for the "Cancel" button.
1368
+ cancel_btn.click cancel_btn.click(
1369
+ fn=set_cancel_flag,
1370
+ cancels=[submit_event]
1371
+ ).then(
1372
+ fn=reset_ui_after_cancel,
1373
+ outputs=ui_components
1374
+ )
1375
+
1376
+ # Listeners for updating the duration estimate
1377
+ duration_inputs = [model_dd, search_chk, mr, mc, max_tok, st]
1378
+ for(
1379
  fn=set_cancel_flag,
1380
  cancels=[submit_event]
1381
  ).then(
 
1386
  # Listeners for updating the duration estimate
1387
  duration_inputs = [model_dd, search_chk, mr, mc, max_tok, st]
1388
  for component in duration_inputs:
1389
+ component.change(fn=update_duration_ component in duration_inputs:
1390
+ component.change(fn=update_duration_estimate, inputs=duration_inputs, outputs=duration_destimate, inputs=duration_inputs, outputs=duration_display)
1391
+
1392
+ #isplay)
1393
 
1394
+ # Toggle web search Toggle web search settings visibility
1395
  def toggle_search_settings(enabled):
1396
+ settings visibility
1397
+ def toggle_search_settings(enabled):
1398
+ return gr.update( return gr.update(visible=enabled)
1399
+
1400
+ search_chvisible=enabled)
1401
 
1402
  search_chk.change(
1403
+ fn=lambda enabled:k.change(
1404
+ fn=lambda enabled: (update_default_prompt(enabled), gr.update(visible (update_default_prompt(enabled), gr.update(visible=enabled)),
1405
+ =enabled)),
1406
+ inputs=search inputs=search_chk,
1407
+ outputs_chk,
1408
  outputs=[sys_prompt, search_settings]
1409
  )
1410
+ =[sys_prompt, search_settings]
1411
+ )
1412
 
1413
+ # Clear
1414
  # Clear chat action
1415
+ chat action
1416
+ clr.click(f clr.click(fn=lambda: ([], "", ""n=lambda: ([], "", ""), outputs=[chat, txt, db), outputs=[chat, txt, dbg])
1417
+
1418
+ demo.launchg])
1419
 
1420
  demo.launch(share=True)