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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +91 -721
app.py CHANGED
@@ -20,22 +20,18 @@ import json
20
  import urllib.parse
21
  from config import MODELS
22
 
23
- # Configure logging
24
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
25
  logger = logging.getLogger(__name__)
26
 
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
40
  class SearchResult:
41
  title: str
@@ -64,8 +60,6 @@ class GenerationConfig:
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',
@@ -85,11 +79,8 @@ class SearchEngine:
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}",
@@ -108,7 +99,6 @@ class GoogleSearch(SearchEngine):
108
 
109
  soup = BeautifulSoup(response.text, 'html.parser')
110
 
111
- # Try multiple selectors
112
  selectors = [
113
  ('div', 'g'),
114
  ('div', 'tF2Cxc'),
@@ -128,17 +118,14 @@ class GoogleSearch(SearchEngine):
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
@@ -171,8 +158,6 @@ class GoogleSearch(SearchEngine):
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:
@@ -189,8 +174,6 @@ class DuckDuckGoSearch(SearchEngine):
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:
@@ -223,22 +206,12 @@ class BingSearch(SearchEngine):
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))
@@ -255,24 +228,20 @@ class SearchManager:
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
 
275
- # Load tokenizer without token if not available
276
  try:
277
  tokenizer = AutoTokenizer.from_pretrained(
278
  repo,
@@ -282,7 +251,6 @@ class ModelManager:
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 = {
@@ -294,7 +262,6 @@ class ModelManager:
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
 
@@ -305,7 +272,6 @@ class ModelManager:
305
  logger.warning(f"Failed to load with {dtype}: {e}")
306
  continue
307
 
308
- # Final fallback
309
  pipe_kwargs = {
310
  'task': "text-generation",
311
  'model': repo,
@@ -322,11 +288,8 @@ class ModelManager:
322
  return pipe
323
 
324
  class PromptBuilder:
325
- """Builds prompts for different models"""
326
-
327
  @staticmethod
328
  def format_conversation(history: List[Dict], system_prompt: str, tokenizer) -> str:
329
- """Format conversation with proper chat template"""
330
  if hasattr(tokenizer, "chat_template") and tokenizer.chat_template:
331
  messages = [{"role": "system", "content": system_prompt.strip()}] + history
332
  return tokenizer.apply_chat_template(
@@ -336,462 +299,214 @@ class PromptBuilder:
336
  enable_thinking=True
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(
692
  max_tokens=max_tokens,
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,
@@ -799,35 +514,16 @@ def get_duration_estimate(
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,
@@ -836,21 +532,6 @@ def update_duration_estimate(
836
  max_tokens: int,
837
  search_timeout: float
838
  ) -> str:
839
- """Format duration estimate for display"""
840
- try:
841
- duration = get_duration_estimate(model_name, enable_search, max_tokens, search_timeout)
842
- model_size = get_model_size(model_name)
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)
@@ -858,142 +539,64 @@ def update_duration_estimate(
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:
919
- # Header
920
  gr.Markdown("""
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,73 +607,33 @@ with gr.Blocks(
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",
1028
  info="Penalize repeated tokens"
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)
@@ -1079,303 +642,129 @@ with gr.Blocks(
1079
  SafeSearch is **OFF** for comprehensive results.
1080
  """)
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(
@@ -1383,38 +772,19 @@ Resetting UI state.")
1383
  outputs=ui_components
1384
  )
1385
 
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)
 
20
  import urllib.parse
21
  from config import MODELS
22
 
 
23
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
24
  logger = logging.getLogger(__name__)
25
 
 
26
  cancel_event = threading.Event()
27
 
 
28
  ACCESS_TOKEN = os.environ.get('HF_TOKEN', '')
29
  if ACCESS_TOKEN == '':
30
+ ACCESS_TOKEN = None
31
 
32
  PIPELINES = {}
33
  SEARCH_TIMEOUT_DEFAULT = 5.0
34
 
 
35
  @dataclass
36
  class SearchResult:
37
  title: str
 
60
  }
61
 
62
  class SearchEngine:
 
 
63
  USER_AGENTS = [
64
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
65
  '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',
 
79
  }
80
 
81
  class GoogleSearch(SearchEngine):
 
 
82
  @staticmethod
83
  def search(query: str, max_results: int = 6, max_chars: int = 50) -> List[SearchResult]:
 
84
  encoded_query = quote_plus(query)
85
  search_urls = [
86
  f"https://www.google.com/search?q={encoded_query}&safe=off&num={max_results}",
 
99
 
100
  soup = BeautifulSoup(response.text, 'html.parser')
101
 
 
102
  selectors = [
103
  ('div', 'g'),
104
  ('div', 'tF2Cxc'),
 
118
  results = []
119
  for result in search_results[:max_results]:
120
  try:
 
121
  title_elem = result.find('h3') or result.find('h2')
122
  if not title_elem:
123
  continue
124
 
 
125
  snippet_elem = result.find('div', class_='VwiC3b') or \
126
  result.find('div', class_='IsZvec') or \
127
  result.find('div', class_='lEBKkf')
128
 
 
129
  link_elem = result.find('a')
130
  if not link_elem:
131
  continue
 
158
  return []
159
 
160
  class DuckDuckGoSearch(SearchEngine):
 
 
161
  @staticmethod
162
  def search(query: str, max_results: int = 6, max_chars: int = 50) -> List[SearchResult]:
163
  try:
 
174
  return []
175
 
176
  class BingSearch(SearchEngine):
 
 
177
  @staticmethod
178
  def search(query: str, max_results: int = 6, max_chars: int = 50) -> List[SearchResult]:
179
  try:
 
206
  return []
207
 
208
  class SearchManager:
209
+ _engines = [GoogleSearch, DuckDuckGoSearch, BingSearch]
 
 
 
 
 
 
210
 
211
  @classmethod
212
  def search(cls, query: str, max_results: int = 6, max_chars: int = 50, timeout: float = 5.0) -> List[SearchResult]:
 
 
 
213
  for engine_cls in cls._engines:
214
  try:
 
215
  result_container = []
216
  search_thread = threading.Thread(
217
  target=lambda: result_container.extend(engine_cls.search(query, max_results, max_chars))
 
228
  logger.warning(f"Search engine {engine_cls.__name__} failed: {e}")
229
  continue
230
 
231
+ return []
232
 
233
  class ModelManager:
 
 
234
  _pipelines = {}
235
  _lock = threading.Lock()
236
 
237
  @classmethod
238
  def load_pipeline(cls, model_name: str) -> pipeline:
 
239
  with cls._lock:
240
  if model_name in cls._pipelines:
241
  return cls._pipelines[model_name]
242
 
243
  repo = MODELS[model_name]["repo_id"]
244
 
 
245
  try:
246
  tokenizer = AutoTokenizer.from_pretrained(
247
  repo,
 
251
  logger.warning(f"Failed to load tokenizer with token, trying without: {e}")
252
  tokenizer = AutoTokenizer.from_pretrained(repo)
253
 
 
254
  for dtype in (torch.bfloat16, torch.float16, torch.float32):
255
  try:
256
  pipe_kwargs = {
 
262
  'device_map': "auto",
263
  'use_cache': True,
264
  }
 
265
  if ACCESS_TOKEN:
266
  pipe_kwargs['token'] = ACCESS_TOKEN
267
 
 
272
  logger.warning(f"Failed to load with {dtype}: {e}")
273
  continue
274
 
 
275
  pipe_kwargs = {
276
  'task': "text-generation",
277
  'model': repo,
 
288
  return pipe
289
 
290
  class PromptBuilder:
 
 
291
  @staticmethod
292
  def format_conversation(history: List[Dict], system_prompt: str, tokenizer) -> str:
 
293
  if hasattr(tokenizer, "chat_template") and tokenizer.chat_template:
294
  messages = [{"role": "system", "content": system_prompt.strip()}] + history
295
  return tokenizer.apply_chat_template(
 
299
  enable_thinking=True
300
  )
301
  else:
302
+ prompt = f"{system_prompt.strip()}\n"
 
 
303
  for msg in history:
304
+ if msg['role'] == 'user':
305
+ prompt += f"User: {msg['content'].strip()}\n"
306
+ elif msg['role'] == 'assistant':
307
+ prompt += f"Assistant: {msg['content'].strip()}\n"
 
 
308
 
309
+ if not prompt.strip().endswith("Assistant:"):
310
+ prompt += "Assistant: "
 
 
311
  return prompt
312
 
313
  @staticmethod
314
+ def build_search_context(search_results: List[SearchResult], system_prompt: str, user_query: str) -> str:
 
 
 
 
 
315
  if not search_results:
 
 
 
 
316
  return system_prompt.strip()
317
 
318
+ formatted_results = "\n".join(f"[{i+1}] {r.format()}" for i, r in enumerate(search_results))
 
 
319
 
320
  return f"""{system_prompt.strip()}
321
 
322
+ # SEARCH CONTEXT (TRUSTED SOURCES ONLY)
323
+ Below are search results. Treat them as the ONLY source of truth for answering.
 
 
 
 
324
  {formatted_results}
325
 
 
 
326
  RULES (VERY IMPORTANT):
327
+ - Do NOT use outside knowledge. Do NOT guess or fill missing information.
328
+ - If the answer is not clearly supported by the search results, say: "Not enough information in the provided sources."
329
+ - Every factual statement must be directly supported by at least one citation [citation:X].
330
+ - Do NOT add explanations, examples, or background that are not explicitly present in the sources.
331
+ - Do NOT paraphrase beyond what is necessary for clarity.
 
 
 
 
 
 
332
  - If sources conflict, mention the conflict and cite both.
333
+ - If multiple sources are used, distribute citations per sentence, not only at the end.
 
 
 
334
 
335
  CITATION RULES:
336
  - Use inline citations like this: [citation:1]
 
 
337
  - If multiple sources support a sentence: [citation:1][citation:3]
 
338
  - Never place all citations only at the end.
339
 
340
  ANSWER POLICY:
341
  - Be concise and strictly grounded.
 
 
 
 
 
342
  - No speculation, no assumptions, no "likely", no "probably".
343
+ - If the user requests a list, only include items explicitly found in sources.
344
+ - If sources are insufficient, stop and ask for more data instead of guessing.
 
345
 
 
 
 
346
  DATE CONTEXT:
347
+ - Today is {datetime.now().strftime('%Y-%m-%d')} (use only for time reference, not for assumptions).
 
 
 
 
 
348
 
349
  USER QUESTION:
350
  {user_query}"""
351
 
352
  class StreamProcessor:
 
 
 
 
 
353
  @staticmethod
354
+ def process_stream(streamer: TextIteratorStreamer, history: List[Dict]) -> Generator[Tuple[List[Dict], str], None, None]:
 
 
 
 
 
 
355
  thought_buf = ''
356
  answer_buf = ''
357
+ in_thought = False
 
 
 
 
358
  assistant_message_started = False
359
 
360
  for chunk in streamer:
361
  if cancel_event.is_set():
362
+ if assistant_message_started and history and history[-1]['role'] == 'assistant':
363
+ history[-1]['content'] += " [Generation Canceled]"
 
 
 
364
  yield history, "Generation canceled by user."
 
365
  break
366
 
367
  text = chunk
368
 
369
+ if not in_thought and '<think>' in text:
370
+ in_thought = True
371
+ history.append({'role': 'assistant', 'content': '', 'metadata': {'title': '💭 Thought'}})
 
 
 
 
 
 
 
 
 
372
  assistant_message_started = True
373
  after = text.split('<think>', 1)[1]
374
  thought_buf += after
375
 
 
 
 
 
 
376
  if '</think>' in thought_buf:
377
  before, after2 = thought_buf.split('</think>', 1)
378
  history[-1]['content'] = before.strip()
 
 
379
  in_thought = False
 
380
  answer_buf = after2
381
+ history.append({'role': 'assistant', 'content': answer_buf})
 
 
382
  else:
 
383
  history[-1]['content'] = thought_buf
384
  yield history, ""
 
 
385
  continue
386
 
 
 
387
  if in_thought:
388
  thought_buf += text
389
+ if '</think>' in thought_buf:
390
+ before, after2 = thought_buf.split('</think>', 1)
 
 
 
 
391
  history[-1]['content'] = before.strip()
 
392
  in_thought = False
 
393
  answer_buf = after2
394
+ history.append({'role': 'assistant', 'content': answer_buf})
 
395
  else:
 
 
396
  history[-1]['content'] = thought_buf
 
397
  yield history, ""
 
398
  continue
399
 
400
+ if not assistant_message_started:
401
+ history.append({'role': 'assistant', 'content': ''})
402
+ assistant_message_started = True
 
 
 
 
 
 
 
 
403
 
404
  answer_buf += text
405
  history[-1]['content'] = answer_buf.strip()
 
 
 
 
406
  yield history, ""
407
 
 
 
 
408
  def chat_response(
409
  user_msg: str,
 
410
  chat_history: List[Dict],
 
411
  system_prompt: str,
 
412
  enable_search: bool,
413
  max_results: int,
 
 
414
  max_chars: int,
 
415
  model_name: str,
 
416
  max_tokens: int,
417
  temperature: float,
418
  top_k: int,
 
 
 
419
  top_p: float,
420
  repeat_penalty: float,
 
 
421
  search_timeout: float
 
422
  ) -> Generator[Tuple[List[Dict], str], None, None]:
 
 
 
 
 
423
  cancel_event.clear()
424
  history = list(chat_history or [])
425
  history.append({'role': 'user', 'content': user_msg})
426
 
 
 
 
 
 
 
427
  search_results: List[SearchResult] = []
 
428
  search_debug = "Web search disabled."
429
 
 
 
430
  if enable_search:
431
+ search_debug = "🔍 Searching across multiple engines..."
 
 
 
432
  try:
433
  search_results = SearchManager.search(
434
  user_msg,
 
 
435
  int(max_results),
 
436
  int(max_chars),
437
  float(search_timeout)
438
  )
439
 
440
+ if search_results:
441
+ search_debug = f"✅ Search completed - Found {len(search_results)} results\n\n" + "\n".join(
442
+ f"- {r.format(int(max_chars))}" for r in search_results
 
 
 
 
 
 
 
 
 
443
  )
444
  else:
445
  search_debug = "❌ No search results found. Check internet connection or try again."
 
 
446
  except Exception as e:
447
  search_debug = f"❌ Search failed: {str(e)}"
 
448
  logger.error(f"Search error: {e}")
449
 
450
  try:
 
 
 
 
 
 
451
  if enable_search and search_results:
452
  enriched_prompt = PromptBuilder.build_search_context(
453
  search_results,
454
+ system_prompt,
 
 
 
 
 
 
455
  user_msg
456
  )
457
  else:
458
  enriched_prompt = system_prompt.strip()
459
 
 
 
 
 
460
  pipe = ModelManager.load_pipeline(model_name)
461
 
 
 
 
462
  prompt = PromptBuilder.format_conversation(history, enriched_prompt, pipe.tokenizer)
463
+ 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```"
 
 
 
 
 
 
464
 
 
465
  config = GenerationConfig(
466
  max_tokens=max_tokens,
467
  temperature=temperature,
468
  top_k=top_k,
469
  top_p=top_p,
470
+ repetition_penalty=repeat_penalty
 
 
 
 
471
  )
472
 
 
 
 
 
 
 
473
  streamer = TextIteratorStreamer(
474
+ pipe.tokenizer,
 
 
475
  skip_prompt=True,
476
  skip_special_tokens=True
477
  )
478
 
479
+ gen_kwargs = config.to_dict()
480
+ gen_kwargs['streamer'] = streamer
481
+ gen_kwargs['return_full_text'] = False
 
 
 
 
 
 
 
 
 
482
 
483
  gen_thread = threading.Thread(
 
484
  target=pipe,
 
485
  args=(prompt,),
 
486
  kwargs=gen_kwargs
487
  )
 
 
488
  gen_thread.start()
 
489
 
 
 
 
490
  yield history, search_debug
491
 
492
+ for history_update, debug_update in StreamProcessor.process_stream(streamer, history):
 
 
 
 
 
493
  yield history_update, debug_update
494
 
 
 
 
495
  gen_thread.join(timeout=5.0)
496
+ yield history, search_debug + prompt_debug
 
 
 
 
497
 
498
  except GeneratorExit:
499
  logger.info("Generation cancelled by user")
 
 
500
  return
501
+ except Exception as e:
 
 
502
  logger.error(f"Generation error: {e}")
503
+ history.append({'role': 'assistant', 'content': f"Error: {str(e)}"})
 
 
504
  yield history, search_debug
 
505
  finally:
506
  gc.collect()
507
 
 
 
 
 
 
508
  def get_model_size(model_name: str) -> float:
509
+ return MODELS.get(model_name, {}).get("params_b", 4.0)
 
 
 
 
 
510
 
511
  def get_duration_estimate(
512
  model_name: str,
 
514
  max_tokens: int,
515
  search_timeout: float
516
  ) -> float:
 
 
 
 
 
 
 
 
517
  model_size = get_model_size(model_name)
518
  use_aot = model_size >= 2
519
 
 
 
 
 
520
  base_duration = 20 if not use_aot else 40
521
  token_duration = max_tokens * 0.005
522
  search_duration = 10 if enable_search else 0
 
 
523
  aot_compilation = 20 if use_aot else 0
524
 
525
  return base_duration + token_duration + search_duration + aot_compilation
526
 
 
 
 
 
 
527
  def update_duration_estimate(
528
  model_name: str,
529
  enable_search: bool,
 
532
  max_tokens: int,
533
  search_timeout: float
534
  ) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
  try:
536
  duration = get_duration_estimate(model_name, enable_search, max_tokens, search_timeout)
537
  model_size = get_model_size(model_name)
 
539
  return f"""⏱️ **Estimated GPU Time: {duration:.1f} seconds**
540
 
541
  📊 **Model Size:** {model_size:.1f}B parameters
542
+ 🔍 **Web Search:** {'Enabled (Multi-Engine)' if enable_search else 'Disabled'}"""
 
 
543
  except Exception as e:
 
544
  logger.error(f"Error calculating estimate: {e}")
545
+ return f"⚠️ Error calculating estimate: {e}"
 
 
 
546
 
547
+ def update_default_prompt(enable_search: bool) -> str:
 
 
548
  return "You are a helpful assistant."
549
 
 
 
 
 
 
 
 
 
 
 
550
  with gr.Blocks(
551
  title="LLM Inference",
 
552
  theme=gr.themes.Soft(
553
  primary_hue="blue",
554
  secondary_hue="blue",
555
  neutral_hue="slate",
 
 
 
 
556
  radius_size="lg",
 
557
  font=[gr.themes.GoogleFont("Syne"), "Arial", "sans-serif"]
558
  ),
559
  css="""
560
+ .duration-estimate { background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); border-left: 4px solid #667eea; padding: 12px; border-radius: 8px; margin: 16px 0; }
561
+ .chatbot { border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }
 
 
 
 
562
  button.primary { font-weight: 600; }
 
 
 
 
 
 
563
  .gradio-accordion { margin-bottom: 12px; }
564
  """
565
  ) as demo:
 
566
  gr.Markdown("""
567
  # 🧠 LLM Inference with Multi-Engine Search
568
  """)
569
 
 
 
 
 
570
  with gr.Row():
 
571
  with gr.Column(scale=3):
 
 
 
 
 
 
572
  with gr.Group():
573
  gr.Markdown("### ⚙️ Core Settings")
 
574
  model_dd = gr.Dropdown(
575
  label="🤖 Model",
576
  choices=list(MODELS.keys()),
577
  value="Qwen3-1.7B",
578
  info="Select the language model to use"
579
  )
 
 
 
 
 
 
 
580
  search_chk = gr.Checkbox(
581
  label="🔍 Enable Web Search",
582
  value=False,
 
 
583
  info="Search across Google, DuckDuckGo, and Bing (no API required)"
584
  )
 
 
585
  sys_prompt = gr.Textbox(label="📝 System Prompt", lines=3, value=update_default_prompt(False), placeholder="Define the assistant's behavior and personality...")
586
 
 
587
  duration_display = gr.Markdown(
588
+ value=update_duration_estimate("Qwen3-1.7B", False, 4, 50, 1024, 5.0),
 
 
 
 
 
 
 
589
  elem_classes="duration-estimate"
590
  )
591
 
 
 
 
 
 
592
  with gr.Accordion("🎛️ Advanced Generation Parameters", open=False):
593
  max_tok = gr.Slider(
594
  64, 16384, value=1024, step=32,
 
595
  label="Max Tokens",
596
  info="Maximum length of generated response"
597
  )
598
  temp = gr.Slider(
599
+ 0.1, 2.0, value=0.7, step=0.1,
 
 
 
 
 
 
600
  label="Temperature",
601
  info="Higher = more creative, Lower = more focused"
602
  )
 
607
  info="Number of top tokens to consider"
608
  )
609
  p = gr.Slider(
 
 
 
 
 
 
 
 
 
 
 
610
  0.1, 1.0, value=0.9, step=0.05,
611
  label="Top-P",
612
  info="Nucleus sampling threshold"
613
  )
 
 
 
614
  rp = gr.Slider(
615
  1.0, 2.0, value=1.2, step=0.1,
616
  label="Repetition Penalty",
617
  info="Penalize repeated tokens"
618
  )
619
 
620
+ with gr.Accordion("🌐 Web Search Settings", open=False, visible=False) as search_settings:
 
 
 
 
 
 
 
 
 
 
621
  mr = gr.Number(
622
  value=4, precision=0,
623
  label="Max Results",
 
624
  info="Number of search results to retrieve"
625
  )
626
  mc = gr.Number(
627
  value=50, precision=0,
628
  label="Max Chars/Result",
629
+ info="Character limit per search result"
 
 
 
 
 
 
 
630
  )
631
  st = gr.Slider(
 
632
  minimum=0.0, maximum=30.0, step=0.5, value=5.0,
633
  label="Search Timeout (s)",
 
 
634
  info="Maximum time to wait for search results"
635
  )
636
+ gr.Markdown("""
 
 
 
 
 
637
  ⚠️ **Search Engines:**
638
  - Google (primary)
639
  - DuckDuckGo (fallback)
 
642
  SafeSearch is **OFF** for comprehensive results.
643
  """)
644
 
 
 
 
 
 
 
 
 
 
645
  with gr.Row():
646
+ clr = gr.Button("🗑️ Clear Chat", variant="secondary", scale=1)
647
 
 
 
 
648
  with gr.Column(scale=7):
649
  chat = gr.Chatbot(
650
  type="messages",
 
 
 
 
 
 
 
 
651
  height=600,
652
  label="💬 Conversation",
653
  show_copy_button=True,
654
+ avatar_images=(
655
+ "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",
656
+ "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"
 
 
 
657
  ),
658
  bubble_full_width=False,
659
  render_markdown=True,
 
 
660
  sanitize_html=False
 
661
  )
662
 
 
 
 
 
663
  with gr.Row():
 
664
  txt = gr.Textbox(
665
+ placeholder="💭 Type your message here... (Press Enter to send)",
 
 
666
  scale=9,
667
  container=False,
 
 
668
  show_label=False,
 
669
  lines=1,
670
  max_lines=5
 
 
671
  )
 
672
  with gr.Column(scale=1, min_width=120):
673
+ submit_btn = gr.Button("📤 Send", variant="primary", size="lg")
 
 
674
  cancel_btn = gr.Button("⏹️ Stop", variant="stop", visible=False, size="lg")
675
 
 
 
 
 
 
 
676
  gr.Examples(
677
  examples=[
678
  ["Explain quantum computing in simple terms"],
 
679
  ["Write a Python function to calculate fibonacci numbers"],
680
+ ["What are the latest developments in AI? (Enable web search)"],
 
681
  ["Tell me a creative story about a time traveler"],
 
 
 
682
  ["Help me debug this code: def add(a,b): return a+b+1"]
683
  ],
 
684
  inputs=txt,
685
  label="💡 Example Prompts"
 
 
686
  )
687
 
 
 
 
 
 
 
 
 
 
 
688
  with gr.Accordion("🔍 Debug Info", open=False):
689
  dbg = gr.Markdown()
690
 
 
691
  gr.Markdown("""
692
  ---
693
  💡 **Tips:**
694
  - Use **Advanced Parameters** to fine-tune creativity and response length
695
+ - Enable **Web Search** for real-time information (uses multiple search engines)
 
 
 
696
  - SafeSearch is **OFF** for comprehensive results
697
+ - Try different **models** for various tasks (reasoning, coding, general chat)
698
+ - Click the **Copy** button on responses to save them to your clipboard
 
 
 
699
  """, elem_classes="footer")
700
 
701
+ chat_inputs = [txt, chat, sys_prompt, search_chk, mr, mc, model_dd, max_tok, temp, k, p, rp, st]
702
+ ui_components = [chat, dbg, txt, submit_btn, cancel_btn]
703
 
704
+ def submit_and_manage_ui(user_msg, chat_history, *args):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
705
  if not user_msg.strip():
 
706
  yield {}
707
  return
708
 
 
 
 
 
 
 
709
  yield {
710
+ txt: gr.update(value="", interactive=False),
 
711
  submit_btn: gr.update(interactive=False),
 
712
  cancel_btn: gr.update(visible=True),
713
  }
714
 
715
+ cancelled = False
 
 
 
716
  try:
717
+ backend_args = [user_msg, chat_history] + list(args)
 
 
 
 
 
718
  for response_chunk in chat_response(*backend_args):
719
  yield {
720
  chat: response_chunk[0],
 
721
  dbg: response_chunk[1],
722
  }
723
  except GeneratorExit:
 
 
 
724
  cancelled = True
 
725
  print("Generation cancelled by user.")
726
  raise
727
  except Exception as e:
728
+ print(f"An error occurred during generation: {e}")
 
 
 
 
729
  error_history = (chat_history or []) + [
 
730
  {'role': 'user', 'content': user_msg},
 
731
  {'role': 'assistant', 'content': f"**An error occurred:** {str(e)}"}
732
  ]
 
 
733
  yield {chat: error_history}
734
  finally:
 
 
735
  if not cancelled:
 
736
  print("Resetting UI state.")
737
  yield {
738
+ txt: gr.update(interactive=True),
 
 
 
739
  submit_btn: gr.update(interactive=True),
740
+ cancel_btn: gr.update(visible=False),
 
 
 
 
741
  }
742
 
743
  def set_cancel_flag():
 
 
 
 
744
  cancel_event.set()
745
  print("Cancellation signal sent.")
746
 
 
 
747
  def reset_ui_after_cancel():
 
748
  cancel_event.clear()
749
  print("UI reset after cancellation.")
750
  return {
751
  txt: gr.update(interactive=True),
752
+ submit_btn: gr.update(interactive=True),
 
 
 
 
 
 
 
753
  cancel_btn: gr.update(visible=False),
754
  }
755
 
 
 
 
 
 
756
  submit_event = txt.submit(
757
+ fn=submit_and_manage_ui,
 
 
 
758
  inputs=chat_inputs,
759
  outputs=ui_components,
760
  )
 
 
 
761
  submit_btn.click(
762
  fn=submit_and_manage_ui,
763
+ inputs=chat_inputs,
 
 
 
 
 
764
  outputs=ui_components,
765
  )
766
 
767
+ cancel_btn.click(
 
 
 
 
 
 
 
 
 
 
 
 
768
  fn=set_cancel_flag,
769
  cancels=[submit_event]
770
  ).then(
 
772
  outputs=ui_components
773
  )
774
 
 
775
  duration_inputs = [model_dd, search_chk, mr, mc, max_tok, st]
776
  for component in duration_inputs:
777
+ component.change(fn=update_duration_estimate, inputs=duration_inputs, outputs=duration_display)
 
 
 
778
 
 
779
  def toggle_search_settings(enabled):
780
+ return gr.update(visible=enabled)
 
 
 
 
781
 
782
  search_chk.change(
783
+ fn=lambda enabled: (update_default_prompt(enabled), gr.update(visible=enabled)),
784
+ inputs=search_chk,
 
 
 
785
  outputs=[sys_prompt, search_settings]
786
  )
 
 
 
 
 
 
 
787
 
788
+ clr.click(fn=lambda: ([], "", ""), outputs=[chat, txt, dbg])
789
 
790
  demo.launch(share=True)