Saffn commited on
Commit
b311643
·
0 Parent(s):

Deploy premium Claude-style LLM chatbot with web search, scraping, memory, and access control

Browse files
Files changed (9) hide show
  1. .gitignore +12 -0
  2. README.md +70 -0
  3. app.py +26 -0
  4. requirements.txt +9 -0
  5. src/__init__.py +1 -0
  6. src/config.py +250 -0
  7. src/engine.py +328 -0
  8. src/tools.py +98 -0
  9. src/ui.py +289 -0
.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ .pytest_cache/
5
+ .DS_Store
6
+ .env
7
+ .venv
8
+ venv/
9
+ env/
10
+ .idea/
11
+ .vscode/
12
+ *.log
README.md ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Claude-Style Modular LLM Space
3
+ emoji: 🤖
4
+ colorFrom: indigo
5
+ colorTo: slate
6
+ sdk: gradio
7
+ sdk_version: 4.44.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ short_description: A premium Claude-style chatbot with web search, scraping, and memory.
12
+ ---
13
+
14
+ # Claude-Style Modular LLM Chatbot
15
+
16
+ A production-grade, premium LLM chatbot interface optimized for Hugging Face Spaces (free tier).
17
+
18
+ It features a minimalist **Claude-style interface** (built with custom CSS and Gradio), an advanced reasoning system prompt, **real-time web search (via DuckDuckGo)**, **automatic web page scraping**, and **conversational memory**.
19
+
20
+ ## 🚀 Key Features
21
+
22
+ * **Claude-Style Minimalist UI**: Sleek, elegant light/dark mode inspired by Anthropic's Claude, complete with high-quality typography, clean card structures, and responsive layouts.
23
+ * **Tri-Mode Inference Engine**:
24
+ 1. **Local CPU (Quantized PyTorch)**: Run lightweight models (e.g., `Qwen/Qwen2.5-1.5B-Instruct`) locally on the free CPU tier (fits within 16GB RAM).
25
+ 2. **Zero-GPU (Free GPU Sharing)**: Dynamic GPU acceleration utilizing Hugging Face's shared Zero-GPU pool (for 7B/8B models like `Qwen/Qwen2.5-7B-Instruct`).
26
+ 3. **HF Serverless Inference API**: Instant, zero-overhead connectivity to massive models (like `Qwen/Qwen2.5-72B-Instruct` or `Meta-Llama-3.3-70B-Instruct`) using your Hugging Face API token.
27
+ * **Real-time Web Search & Scraper**: Toggleable web-search capability that searches DuckDuckGo, scrapes page contents, and injects context directly into the prompt.
28
+ * **Conversational Memory**: Maintains session chat history dynamically.
29
+ * **State-of-the-Art System Prompt**: Tailored prompts that guide the model to provide detailed, step-by-step thinking, well-formatted markdown, and objective answers.
30
+
31
+ ## 🛠️ Hugging Face One-Click Deployment
32
+
33
+ Click the button below to deploy this template directly to your Hugging Face Spaces:
34
+
35
+ [![Deploy to Spaces](https://huggingface.co/datasets/huggingface/badges/resolve/main/deploy-to-spaces-lg.svg)](https://huggingface.co/new-space?template=SmartGenzAI1/llm)
36
+
37
+ ### 🔒 Access Control (Private Deployment)
38
+ To restrict access so **only you** can use the chatbot:
39
+ 1. In your Hugging Face Space, navigate to **Settings** -> **Variables and secrets**.
40
+ 2. Create a new **Secret** with:
41
+ - **Name**: `APP_PASSWORD`
42
+ - **Value**: Your chosen private passcode (e.g., `my_private_passcode123`).
43
+ 3. Once saved, Hugging Face will prompt a secure login page when accessing the Space. You will log in using `admin` as the username and your passcode as the password.
44
+
45
+ ### 💻 Manual Git Deployment
46
+ To push manually:
47
+ 1. Create a new Space on [Hugging Face](https://huggingface.co/new-space).
48
+ 2. Choose **Gradio** as the SDK.
49
+ 3. Push these files to your Space's repository:
50
+ ```bash
51
+ git remote add origin https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
52
+ git add .
53
+ git commit -m "Deploying Claude-style chatbot"
54
+ git push -u origin main --force
55
+ ```
56
+
57
+
58
+ ## ⚙️ Project Structure
59
+
60
+ ```
61
+ ├── app.py # Space entrypoint
62
+ ├── requirements.txt # Python dependencies
63
+ ├── README.md # Space configuration & docs
64
+ └── src/
65
+ ├── __init__.py
66
+ ├── config.py # Theme styling, system prompts, & model settings
67
+ ├── tools.py # Web Search & Web Scraping engine
68
+ ├── engine.py # Modular multi-backend inference runner
69
+ └── ui.py # Custom Gradio interface components
70
+ ```
app.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from src.ui import build_interface
3
+
4
+ if __name__ == "__main__":
5
+ # Build the Gradio interface
6
+ demo = build_interface()
7
+
8
+ # Check for private passcode environment variable
9
+ password = os.environ.get("APP_PASSWORD")
10
+
11
+ # Hugging Face Spaces runs on port 7860 by default.
12
+ # We bind to 0.0.0.0 to make the service reachable within the HF container.
13
+ launch_kwargs = {
14
+ "server_name": "0.0.0.0",
15
+ "server_port": 7860,
16
+ "show_api": False,
17
+ "concurrency_limit": 10
18
+ }
19
+
20
+ # Enable login window if password secret is configured
21
+ if password:
22
+ launch_kwargs["auth"] = ("admin", password)
23
+ print("Secure authentication enabled via APP_PASSWORD secret.")
24
+
25
+ demo.launch(**launch_kwargs)
26
+
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=4.44.0
2
+ torch
3
+ transformers>=4.43.0
4
+ accelerate>=0.30.0
5
+ huggingface_hub>=0.24.0
6
+ duckduckgo_search>=6.2.0
7
+ beautifulsoup4>=4.12.0
8
+ html2text>=2024.2.26
9
+ requests
src/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Package initialization for the Claude-style LLM chatbot space.
src/config.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Configuration file for Claude-style LLM Space
2
+
3
+ # Available Model Configurations
4
+ MODEL_CONFIGS = {
5
+ "Local CPU (Lightweight)": [
6
+ {
7
+ "name": "Qwen 2.5 1.5B Instruct",
8
+ "repo_id": "Qwen/Qwen2.5-1.5B-Instruct",
9
+ "description": "Blazing fast on CPU, highly competent. Ideal for basic CPU spaces.",
10
+ "default": True
11
+ },
12
+ {
13
+ "name": "Llama 3.2 1B Instruct",
14
+ "repo_id": "meta-llama/Llama-3.2-1B-Instruct",
15
+ "description": "Ultra-lightweight model by Meta. Low RAM footprint.",
16
+ "default": False
17
+ },
18
+ {
19
+ "name": "Llama 3.2 3B Instruct",
20
+ "repo_id": "meta-llama/Llama-3.2-3B-Instruct",
21
+ "description": "Very smart, well-balanced for CPU. Might take a bit longer to load.",
22
+ "default": False
23
+ }
24
+ ],
25
+ "Zero-GPU (Accelerated)": [
26
+ {
27
+ "name": "Qwen 2.5 7B Instruct",
28
+ "repo_id": "Qwen/Qwen2.5-7B-Instruct",
29
+ "description": "Excellent reasoning and coding. Highly recommended for Zero-GPU.",
30
+ "default": True
31
+ },
32
+ {
33
+ "name": "Llama 3 8B Instruct",
34
+ "repo_id": "meta-llama/Meta-Llama-3-8B-Instruct",
35
+ "description": "Meta's standard 8B model. Balanced and conversational.",
36
+ "default": False
37
+ },
38
+ {
39
+ "name": "Mistral 7B Instruct v0.3",
40
+ "repo_id": "mistralai/Mistral-7B-Instruct-v0.3",
41
+ "description": "Classic developer favorite. Excellent instruction following.",
42
+ "default": False
43
+ }
44
+ ],
45
+ "HF Serverless API (Zero Overhead)": [
46
+ {
47
+ "name": "Llama 3.3 70B Instruct",
48
+ "repo_id": "meta-llama/Llama-3.3-70B-Instruct",
49
+ "description": "Massive 70B model. State-of-the-art reasoning, fully hosted by Hugging Face.",
50
+ "default": True
51
+ },
52
+ {
53
+ "name": "Qwen 2.5 72B Instruct",
54
+ "repo_id": "Qwen/Qwen2.5-72B-Instruct",
55
+ "description": "Extremely powerful, rivals commercial LLMs. Hosted by Hugging Face.",
56
+ "default": False
57
+ },
58
+ {
59
+ "name": "Mixtral 8x7B Instruct",
60
+ "repo_id": "mistralai/Mixtral-8x7B-Instruct-v0.1",
61
+ "description": "High-speed Mixture of Experts model. Hosted by Hugging Face.",
62
+ "default": False
63
+ }
64
+ ]
65
+ }
66
+
67
+ # The Leaked-Style System Prompt (inspired by Claude 3.5 Sonnet & ChatGPT Custom Instructions)
68
+ SYSTEM_PROMPT = """You are a highly advanced AI coding assistant and researcher named Antigravity, engineered by the Google DeepMind team. You approach every interaction with objective precision, extreme intelligence, and structured depth.
69
+
70
+ You must strictly adhere to the following behavioral and formatting rules:
71
+
72
+ 1. THOUGHT PROCESS (Chain of Thought):
73
+ - Before answering, you must analyze the user's query and plan your solution step-by-step.
74
+ - You MUST wrap your detailed reasoning inside a `<thinking>` block.
75
+ - In your reasoning, break down the core components of the problem, consider edge cases, verify code syntax mentally, and map out the response structure.
76
+ - Example:
77
+ <thinking>
78
+ The user is asking for X.
79
+ First, I need to analyze Y...
80
+ Then, I should structure the solution like Z...
81
+ </thinking>
82
+
83
+ 2. DIRECTNESS & TONE:
84
+ - Never use generic conversational filler or robotic pleasantries. Avoid starting responses with "Sure, I can help with that," "Here is the code," or "As an AI...".
85
+ - Adopt an objective, clear, and intellectual tone. Speak directly to the user.
86
+ - Do not make assumptions. If a query is ambiguous, explain the ambiguity and outline the options or ask for clarification.
87
+
88
+ 3. KNOWLEDGE & CAPABILITIES:
89
+ - You have access to real-time web search and web scraping tools. When web context is provided, rely on it to answer queries accurately and provide sources/citations where appropriate.
90
+ - If you do not know the answer, admit it honestly.
91
+
92
+ 4. FORMATTING & CODE STYLE:
93
+ - Use GitHub-style markdown for all responses.
94
+ - Write clean, production-grade, fully commented code blocks.
95
+ - Never write placeholders like `// TODO: implement this` or `...` in code outputs unless explicitly asked. Always write complete, copy-pasteable files.
96
+ - Use bold headers, clean lists, and Markdown tables to make information easily scannable.
97
+ - Use LaTeX syntax for math equations (e.g., inline: \\( E=mc^2 \\), block: \\$\\$ \\sum_{i=1}^n i \\$\\$).
98
+
99
+ Current Date/Time: {datetime}
100
+ """
101
+
102
+ # Premium Claude-Style Custom CSS for Gradio
103
+ CLAUDE_CSS = """
104
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@300;400;500;600;700&display=swap');
105
+
106
+ /* Apply custom typography globally */
107
+ body, .gradio-container {
108
+ font-family: 'Inter', sans-serif !important;
109
+ background-color: #0b0f19 !important; /* Premium dark background */
110
+ color: #f3f4f6 !important;
111
+ }
112
+
113
+ /* Claude style header styling */
114
+ h1, h2, h3, h4 {
115
+ font-family: 'Outfit', sans-serif !important;
116
+ font-weight: 600;
117
+ }
118
+
119
+ /* Sidebar configuration panel */
120
+ .sidebar-panel {
121
+ background-color: rgba(17, 24, 39, 0.7) !important;
122
+ backdrop-filter: blur(12px) !important;
123
+ border: 1px solid rgba(255, 255, 255, 0.08) !important;
124
+ border-radius: 16px !important;
125
+ padding: 20px !important;
126
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2) !important;
127
+ }
128
+
129
+ /* Customizing the main chatbot */
130
+ .chatbot-container {
131
+ border: 1px solid rgba(255, 255, 255, 0.08) !important;
132
+ border-radius: 16px !important;
133
+ background-color: rgba(17, 24, 39, 0.4) !important;
134
+ backdrop-filter: blur(12px) !important;
135
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2) !important;
136
+ overflow: hidden;
137
+ }
138
+
139
+ /* Hide default gradio borders and adjust message padding */
140
+ .chatbot-container .message-row {
141
+ padding: 16px 24px !important;
142
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05) !important;
143
+ }
144
+
145
+ /* User chat bubble styling - elegant, dark-grey with thin border */
146
+ .chatbot-container .user {
147
+ background-color: rgba(59, 130, 246, 0.1) !important;
148
+ border: 1px solid rgba(59, 130, 246, 0.2) !important;
149
+ border-radius: 12px 12px 0px 12px !important;
150
+ padding: 12px 16px !important;
151
+ align-self: flex-end;
152
+ }
153
+
154
+ /* Assistant chat bubble styling - clean borderless transparent, minimalist like Claude */
155
+ .chatbot-container .bot {
156
+ background-color: transparent !important;
157
+ border: none !important;
158
+ padding: 12px 0px !important;
159
+ }
160
+
161
+ /* Custom CSS to style thinking process blocks */
162
+ details.thinking-block {
163
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
164
+ border-radius: 8px !important;
165
+ background-color: rgba(255, 255, 255, 0.03) !important;
166
+ padding: 10px 14px !important;
167
+ margin-bottom: 12px !important;
168
+ font-size: 0.9em !important;
169
+ color: #9ca3af !important;
170
+ transition: all 0.3s ease;
171
+ }
172
+
173
+ details.thinking-block[open] {
174
+ border-color: rgba(59, 130, 246, 0.3) !important;
175
+ background-color: rgba(59, 130, 246, 0.02) !important;
176
+ }
177
+
178
+ details.thinking-block summary {
179
+ font-weight: 500 !important;
180
+ color: #60a5fa !important;
181
+ cursor: pointer !important;
182
+ outline: none !important;
183
+ user-select: none !important;
184
+ }
185
+
186
+ /* Beautiful buttons styling */
187
+ .action-btn {
188
+ background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%) !important;
189
+ color: white !important;
190
+ font-weight: 500 !important;
191
+ border: none !important;
192
+ border-radius: 8px !important;
193
+ transition: all 0.2s ease !important;
194
+ box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.2) !important;
195
+ }
196
+
197
+ .action-btn:hover {
198
+ transform: translateY(-1px) !important;
199
+ box-shadow: 0 6px 12px -1px rgba(37, 99, 235, 0.4) !important;
200
+ }
201
+
202
+ .action-btn:active {
203
+ transform: translateY(1px) !important;
204
+ }
205
+
206
+ /* Secondary/outline buttons (like Web Search toggle) */
207
+ .secondary-btn {
208
+ background-color: rgba(255, 255, 255, 0.05) !important;
209
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
210
+ color: #f3f4f6 !important;
211
+ border-radius: 8px !important;
212
+ transition: all 0.2s ease !important;
213
+ }
214
+
215
+ .secondary-btn:hover {
216
+ background-color: rgba(255, 255, 255, 0.1) !important;
217
+ border-color: rgba(255, 255, 255, 0.2) !important;
218
+ }
219
+
220
+ /* Inputs and textareas */
221
+ input, textarea, select {
222
+ background-color: rgba(31, 41, 55, 0.8) !important;
223
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
224
+ border-radius: 8px !important;
225
+ color: #f3f4f6 !important;
226
+ padding: 8px 12px !important;
227
+ }
228
+
229
+ input:focus, textarea:focus, select:focus {
230
+ border-color: #2563eb !important;
231
+ box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2) !important;
232
+ outline: none !important;
233
+ }
234
+
235
+ /* Adjust sliders aesthetics */
236
+ input[type="range"] {
237
+ accent-color: #2563eb !important;
238
+ }
239
+
240
+ /* Status logs and output cards */
241
+ .status-card {
242
+ background-color: rgba(251, 191, 36, 0.1) !important;
243
+ border: 1px solid rgba(251, 191, 36, 0.2) !important;
244
+ border-radius: 8px !important;
245
+ padding: 10px 14px !important;
246
+ font-size: 0.9em !important;
247
+ color: #fbbf24 !important;
248
+ margin-bottom: 12px !important;
249
+ }
250
+ """
src/engine.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gc
3
+ import time
4
+ from datetime import datetime
5
+ import torch
6
+ from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
7
+ from huggingface_hub import InferenceClient
8
+ from src.config import SYSTEM_PROMPT, MODEL_CONFIGS
9
+ from src.tools import web_search, scrape_url, format_search_results_for_prompt
10
+
11
+ # Conditional Zero-GPU Spaces import
12
+ try:
13
+ import spaces
14
+ HAS_SPACES = True
15
+ gpu_decorator = spaces.GPU
16
+ except ImportError:
17
+ HAS_SPACES = False
18
+ # Dummy decorator if not on HF Zero-GPU
19
+ def gpu_decorator(f):
20
+ return f
21
+
22
+ # Global Model Cache variables
23
+ _current_model = None
24
+ _current_tokenizer = None
25
+ _current_repo_id = None
26
+
27
+ def unload_model():
28
+ """Unloads the currently cached model and tokenizer to free RAM/GPU memory."""
29
+ global _current_model, _current_tokenizer, _current_repo_id
30
+ if _current_model is not None:
31
+ print(f"Unloading model: {_current_repo_id} to free memory...")
32
+ del _current_model
33
+ del _current_tokenizer
34
+ _current_model = None
35
+ _current_tokenizer = None
36
+ _current_repo_id = None
37
+ # Force garbage collection and CUDA cache clearing
38
+ gc.collect()
39
+ if torch.cuda.is_available():
40
+ torch.cuda.empty_cache()
41
+ time.sleep(1)
42
+
43
+ def get_local_model(repo_id: str):
44
+ """
45
+ Retrieves the local tokenizer and model, loading them from Hugging Face
46
+ cache if not already loaded in the memory cache.
47
+ """
48
+ global _current_model, _current_tokenizer, _current_repo_id
49
+
50
+ if _current_repo_id == repo_id and _current_model is not None:
51
+ return _current_model, _current_tokenizer
52
+
53
+ # Unload previous model to avoid out-of-memory errors
54
+ unload_model()
55
+
56
+ print(f"Loading model: {repo_id}...")
57
+ tokenizer = AutoTokenizer.from_pretrained(repo_id)
58
+
59
+ # Determine the device mapping (GPU if available, else CPU)
60
+ if torch.cuda.is_available():
61
+ device_map = "auto"
62
+ torch_dtype = torch.float16
63
+ else:
64
+ device_map = "cpu"
65
+ # On CPU, float32 is most stable, bfloat16 can be used if CPU supports it
66
+ torch_dtype = torch.float32
67
+
68
+ model = AutoModelForCausalLM.from_pretrained(
69
+ repo_id,
70
+ device_map=device_map,
71
+ torch_dtype=torch_dtype,
72
+ low_cpu_mem_usage=True
73
+ )
74
+
75
+ _current_model = model
76
+ _current_tokenizer = tokenizer
77
+ _current_repo_id = repo_id
78
+
79
+ print(f"Successfully loaded {repo_id} into memory.")
80
+ return model, tokenizer
81
+
82
+ # Zero-GPU wraps the execution. We use the gpu_decorator.
83
+ @gpu_decorator
84
+ def generate_local_inference(prompt_text: str, repo_id: str, max_new_tokens: int, temperature: float, top_p: float):
85
+ """
86
+ Executes local text generation with streaming capabilities.
87
+ Works seamlessly on both CPU and Zero-GPU spaces.
88
+ """
89
+ model, tokenizer = get_local_model(repo_id)
90
+
91
+ # Check device
92
+ device = next(model.parameters()).device
93
+
94
+ # Tokenize input
95
+ inputs = tokenizer(prompt_text, return_tensors="pt").to(device)
96
+
97
+ # Set up streaming iterator
98
+ streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, clean_up_tokenization_spaces=True)
99
+
100
+ # Prepare generation parameters
101
+ # Adjust temperature constraints (transformers expects temp > 0 if do_sample is True)
102
+ do_sample = temperature > 0.0
103
+ gen_kwargs = {
104
+ "input_ids": inputs["input_ids"],
105
+ "attention_mask": inputs.get("attention_mask"),
106
+ "max_new_tokens": max_new_tokens,
107
+ "temperature": temperature if do_sample else None,
108
+ "top_p": top_p if do_sample else None,
109
+ "do_sample": do_sample,
110
+ "streamer": streamer,
111
+ "pad_token_id": tokenizer.eos_token_id
112
+ }
113
+
114
+ # Run in a background thread to allow streaming
115
+ from threading import Thread
116
+ thread = Thread(target=model.generate, kwargs=gen_kwargs)
117
+ thread.start()
118
+
119
+ # Yield tokens as they arrive
120
+ generated_text = ""
121
+ for new_text in streamer:
122
+ generated_text += new_text
123
+ yield generated_text
124
+
125
+ thread.join()
126
+
127
+ def run_serverless_api_inference(messages: list, repo_id: str, max_new_tokens: int, temperature: float, top_p: float, hf_token: str = None):
128
+ """
129
+ Runs text generation via HF Serverless Inference API client.
130
+ Streams tokens in real time.
131
+ """
132
+ # Retrieve token from environment variables if not provided explicitly
133
+ token = hf_token or os.environ.get("HF_TOKEN")
134
+
135
+ # Initialize Client
136
+ client = InferenceClient(model=repo_id, token=token)
137
+
138
+ generated_text = ""
139
+ try:
140
+ response_stream = client.chat_completion(
141
+ messages=messages,
142
+ max_tokens=max_new_tokens,
143
+ temperature=temperature,
144
+ top_p=top_p,
145
+ stream=True
146
+ )
147
+
148
+ for chunk in response_stream:
149
+ content = chunk.choices[0].delta.content
150
+ if content:
151
+ generated_text += content
152
+ yield generated_text
153
+ except Exception as e:
154
+ error_msg = f"Serverless API Error: {str(e)}\n\n"
155
+ if not token:
156
+ error_msg += "💡 Tip: Many models require a valid Hugging Face Token for serverless inference. Please enter your HF Token in the sidebar panel."
157
+ yield error_msg
158
+
159
+ def build_prompt_with_history(messages: list, system_prompt: str, tokenizer=None) -> str:
160
+ """
161
+ Formats the conversation history using standard chat templates.
162
+ """
163
+ formatted_messages = [{"role": "system", "content": system_prompt}] + messages
164
+
165
+ if tokenizer is not None and hasattr(tokenizer, "apply_chat_template"):
166
+ try:
167
+ return tokenizer.apply_chat_template(formatted_messages, tokenize=False, add_generation_prompt=True)
168
+ except Exception:
169
+ pass
170
+
171
+ # Fallback to general formatting if template is unavailable
172
+ prompt_str = ""
173
+ for msg in formatted_messages:
174
+ role = msg["role"]
175
+ content = msg["content"]
176
+ if role == "system":
177
+ prompt_str += f"<|im_start|>system\n{content}<|im_end|>\n"
178
+ elif role == "user":
179
+ prompt_str += f"<|im_start|>user\n{content}<|im_end|>\n"
180
+ elif role == "assistant":
181
+ prompt_str += f"<|im_start|>assistant\n{content}<|im_end|>\n"
182
+ prompt_str += "<|im_start|>assistant\n"
183
+ return prompt_str
184
+
185
+ def format_thinking_tags(text: str) -> str:
186
+ """
187
+ Replaces model <thinking></thinking> tags with clean, modern HTML Details panels
188
+ for premium rendering in the Gradio chat viewport.
189
+ """
190
+ if "<thinking>" in text:
191
+ parts = text.split("<thinking>", 1)
192
+ before_thinking = parts[0]
193
+ rest = parts[1]
194
+
195
+ if "</thinking>" in rest:
196
+ thinking_parts = rest.split("</thinking>", 1)
197
+ thinking_content = thinking_parts[0]
198
+ after_thinking = thinking_parts[1]
199
+ return f"{before_thinking}<details class='thinking-block'><summary>Thought Process</summary>\n\n{thinking_content.strip()}\n\n</details>\n\n{after_thinking}"
200
+ else:
201
+ # Thinking block is still generating, render it open
202
+ return f"{before_thinking}<details open class='thinking-block'><summary>Thinking Process...</summary>\n\n{rest.strip()}\n\n</details>"
203
+ return text
204
+
205
+ def execute_chat(
206
+ message: str,
207
+ history: list,
208
+ mode: str,
209
+ model_name: str,
210
+ system_prompt_preset: str,
211
+ max_new_tokens: int,
212
+ temperature: float,
213
+ top_p: float,
214
+ enable_search: bool,
215
+ hf_token: str
216
+ ):
217
+ """
218
+ Orchestrates the chat request, performs search if toggled, builds the history,
219
+ and runs inference on the selected backend mode (Local CPU, Zero-GPU, or API).
220
+ """
221
+ # 1. Look up the repo_id from configs
222
+ repo_id = None
223
+ for item in MODEL_CONFIGS.get(mode, []):
224
+ if item["name"] == model_name:
225
+ repo_id = item["repo_id"]
226
+ break
227
+
228
+ if not repo_id:
229
+ yield history + [[message, "Configuration Error: Selected model details not found."]], ""
230
+ return
231
+
232
+ # 2. Handle web search if enabled
233
+ search_context = ""
234
+ status_update = ""
235
+
236
+ if enable_search:
237
+ status_update = f"🔍 Searching web for: '{message}'...\n"
238
+ yield history + [[message, status_update]], ""
239
+
240
+ results = web_search(message, max_results=3)
241
+ if results:
242
+ status_update += f"📄 Scraped {len(results)} relevant web sources. Integrating context...\n"
243
+ yield history + [[message, status_update]], ""
244
+
245
+ # Scrape details from the top result to enrich context
246
+ top_url = results[0]["url"]
247
+ scraped_content = scrape_url(top_url, max_chars=3000)
248
+
249
+ # Format combined search results
250
+ search_context = format_search_results_for_prompt(message, results)
251
+ search_context += f"\nDetailed body scraped from source [1] ({top_url}):\n{scraped_content}\n---\n"
252
+ else:
253
+ status_update += "❌ Web search returned no results. Proceeding with model knowledge...\n"
254
+ yield history + [[message, status_update]], ""
255
+ time.sleep(1)
256
+
257
+ # 3. Compile history into standard Gradio message formats
258
+ chat_messages = []
259
+ for user_msg, bot_msg in history:
260
+ # If the bot response has status logs from web search, strip them so LLM doesn't read them as its own words
261
+ clean_bot_msg = bot_msg
262
+ if "🔍 Searching web" in bot_msg:
263
+ # Split and get the text after the final status separator if it exists
264
+ parts = bot_msg.split("---\n")
265
+ if len(parts) > 1:
266
+ clean_bot_msg = parts[-1]
267
+ else:
268
+ # Fallback if structure is different
269
+ clean_bot_msg = bot_msg.split("\n")[-1]
270
+
271
+ chat_messages.append({"role": "user", "content": user_msg})
272
+ chat_messages.append({"role": "assistant", "content": clean_bot_msg})
273
+
274
+ # Prepare active prompt contents
275
+ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
276
+ compiled_system_prompt = system_prompt_preset.format(datetime=current_time)
277
+
278
+ # Prepend search context to user query if found
279
+ if search_context:
280
+ user_query_content = f"{search_context}User Query: {message}"
281
+ else:
282
+ user_query_content = message
283
+
284
+ chat_messages.append({"role": "user", "content": user_query_content})
285
+
286
+ # 4. Invoke inference backend
287
+ if mode == "HF Serverless API (Zero Overhead)":
288
+ # Stream response from API
289
+ api_stream = run_serverless_api_inference(
290
+ messages=chat_messages,
291
+ repo_id=repo_id,
292
+ max_new_tokens=max_new_tokens,
293
+ temperature=temperature,
294
+ top_p=top_p,
295
+ hf_token=hf_token
296
+ )
297
+
298
+ for partial_text in api_stream:
299
+ formatted_text = format_thinking_tags(partial_text)
300
+ full_response = status_update + formatted_text if status_update else formatted_text
301
+ yield history + [[message, full_response]], ""
302
+
303
+ else:
304
+ # Local CPU or Zero-GPU mode
305
+ # Load local tokenizer (temporarily to build prompt or load model)
306
+ # Note: loading tokenizer is fast and lightweight
307
+ try:
308
+ tokenizer = AutoTokenizer.from_pretrained(repo_id)
309
+ except Exception:
310
+ tokenizer = None
311
+
312
+ prompt_text = build_prompt_with_history(chat_messages, compiled_system_prompt, tokenizer)
313
+
314
+ # Free up variables
315
+ del tokenizer
316
+
317
+ local_stream = generate_local_inference(
318
+ prompt_text=prompt_text,
319
+ repo_id=repo_id,
320
+ max_new_tokens=max_new_tokens,
321
+ temperature=temperature,
322
+ top_p=top_p
323
+ )
324
+
325
+ for partial_text in local_stream:
326
+ formatted_text = format_thinking_tags(partial_text)
327
+ full_response = status_update + formatted_text if status_update else formatted_text
328
+ yield history + [[message, full_response]], ""
src/tools.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import urllib.parse
3
+ import requests
4
+ from bs4 import BeautifulSoup
5
+ import html2text
6
+ from duckduckgo_search import DDGS
7
+
8
+ # Standard browser headers to avoid getting blocked by websites
9
+ HEADERS = {
10
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
11
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
12
+ "Accept-Language": "en-US,en;q=0.5",
13
+ "Referer": "https://www.google.com/"
14
+ }
15
+
16
+ def clean_text(text: str) -> str:
17
+ """Cleans excess whitespace and formats text nicely."""
18
+ # Replace multiple newlines/spaces with single ones
19
+ text = re.sub(r'\n+', '\n', text)
20
+ text = re.sub(r' +', ' ', text)
21
+ return text.strip()
22
+
23
+ def web_search(query: str, max_results: int = 3) -> list:
24
+ """
25
+ Searches DuckDuckGo and returns a list of dictionaries with titles, hrefs, and body snippets.
26
+ Falls back gracefully if the search fails.
27
+ """
28
+ try:
29
+ results = []
30
+ with DDGS() as ddgs:
31
+ for r in ddgs.text(query, max_results=max_results):
32
+ results.append({
33
+ "title": r.get("title", "No Title"),
34
+ "url": r.get("href", ""),
35
+ "snippet": r.get("body", "")
36
+ })
37
+ return results
38
+ except Exception as e:
39
+ print(f"Error during DuckDuckGo search: {e}")
40
+ return []
41
+
42
+ def scrape_url(url: str, max_chars: int = 4000) -> str:
43
+ """
44
+ Fetches the web page content and converts it to clean markdown.
45
+ Truncates the output to fit context windows.
46
+ """
47
+ if not url.startswith("http"):
48
+ return "Invalid URL format."
49
+
50
+ try:
51
+ response = requests.get(url, headers=HEADERS, timeout=8)
52
+ if response.status_code != 200:
53
+ return f"Failed to retrieve page. Status code: {response.status_code}"
54
+
55
+ # Detect and convert content
56
+ content_type = response.headers.get('Content-Type', '').lower()
57
+ if 'text/html' not in content_type:
58
+ return f"Scraping is limited to HTML content. Content-Type received: {content_type}"
59
+
60
+ # Initialize html2text converter
61
+ h = html2text.HTML2Text()
62
+ h.ignore_links = False
63
+ h.ignore_images = True
64
+ h.ignore_emphasis = False
65
+ h.body_width = 0 # Wrap lines at infinity
66
+
67
+ # Extract HTML
68
+ html = response.text
69
+ markdown_content = h.handle(html)
70
+
71
+ # Clean text
72
+ markdown_content = clean_text(markdown_content)
73
+
74
+ if len(markdown_content) > max_chars:
75
+ return markdown_content[:max_chars] + "\n\n... [Content Truncated due to size constraints] ..."
76
+
77
+ return markdown_content
78
+
79
+ except requests.exceptions.Timeout:
80
+ return "Scraping error: Connection timed out."
81
+ except Exception as e:
82
+ return f"Scraping error occurred: {str(e)}"
83
+
84
+ def format_search_results_for_prompt(query: str, search_results: list) -> str:
85
+ """Formats search results and snippets into a structured text context block."""
86
+ if not search_results:
87
+ return "No search results returned for the query."
88
+
89
+ context = f"### WEB SEARCH RESULTS FOR: '{query}'\n"
90
+ context += "Below are relevant snippets retrieved from the web. Use these to formulate a factually correct answer:\n\n"
91
+
92
+ for idx, res in enumerate(search_results, 1):
93
+ context += f"Source [{idx}]: {res['title']}\n"
94
+ context += f"URL: {res['url']}\n"
95
+ context += f"Snippet: {res['snippet']}\n\n"
96
+
97
+ context += "---\n"
98
+ return context
src/ui.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import torch
3
+ from src.config import MODEL_CONFIGS, SYSTEM_PROMPT, CLAUDE_CSS
4
+ from src.engine import execute_chat, HAS_SPACES
5
+
6
+ def get_hardware_status():
7
+ """Returns a user-friendly string indicating the current runtime hardware."""
8
+ if HAS_SPACES:
9
+ return "🟢 Hugging Face Zero-GPU (A100 Dynamic Allocation)"
10
+ elif torch.cuda.is_available():
11
+ return f"🟢 Local GPU: {torch.cuda.get_device_name(0)}"
12
+ else:
13
+ return "⚪ Standard CPU Mode (Free Tier)"
14
+
15
+ def update_model_dropdown(mode):
16
+ """Updates the model choice list when the backend mode is toggled."""
17
+ models = [m["name"] for m in MODEL_CONFIGS[mode]]
18
+ default_model = next(m["name"] for m in MODEL_CONFIGS[mode] if m["default"])
19
+ return gr.Dropdown(choices=models, value=default_model, label="Active Model")
20
+
21
+ def add_user_message(message, history):
22
+ """Adds the user message to the chat container and clears the input box."""
23
+ if not message.strip():
24
+ return "", history
25
+ return "", history + [[message, "⏳ Initializing inference engine..."]]
26
+
27
+ def execute_chat_ui(
28
+ history,
29
+ mode,
30
+ model_name,
31
+ system_prompt_preset,
32
+ max_new_tokens,
33
+ temperature,
34
+ top_p,
35
+ enable_search,
36
+ hf_token
37
+ ):
38
+ """
39
+ UI Wrapper that processes the active chatbot history state,
40
+ runs the backend generator, and streams response updates.
41
+ """
42
+ if not history:
43
+ return
44
+
45
+ # Extract latest user message and the history preceding it
46
+ user_message = history[-1][0]
47
+ past_history = history[:-1]
48
+
49
+ # Run chat execution generator
50
+ chat_generator = execute_chat(
51
+ message=user_message,
52
+ history=past_history,
53
+ mode=mode,
54
+ model_name=model_name,
55
+ system_prompt_preset=system_prompt_preset,
56
+ max_new_tokens=max_new_tokens,
57
+ temperature=temperature,
58
+ top_p=top_p,
59
+ enable_search=enable_search,
60
+ hf_token=hf_token
61
+ )
62
+
63
+ for updated_history, _ in chat_generator:
64
+ yield updated_history
65
+
66
+ def build_interface():
67
+ """Constructs the Gradio user interface using custom styles and themes."""
68
+ # Custom light/dark theme initialization
69
+ theme = gr.themes.Soft(
70
+ primary_hue="blue",
71
+ secondary_hue="slate",
72
+ neutral_hue="slate",
73
+ font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
74
+ font_mono=[gr.themes.GoogleFont("Roboto Mono"), "ui-monospace", "SFMono-Regular", "monospace"]
75
+ ).set(
76
+ body_background_fill="#0b0f19",
77
+ body_background_fill_dark="#0b0f19",
78
+ block_background_fill="rgba(17, 24, 39, 0.5)",
79
+ block_background_fill_dark="rgba(17, 24, 39, 0.5)",
80
+ border_color_primary="rgba(255, 255, 255, 0.08)",
81
+ border_color_primary_dark="rgba(255, 255, 255, 0.08)"
82
+ )
83
+
84
+ with gr.Blocks(theme=theme, css=CLAUDE_CSS, title="Antigravity Chat") as demo:
85
+ # State to store the raw message during submission sequence
86
+
87
+ with gr.Row():
88
+ with gr.Column(scale=12):
89
+ gr.HTML(
90
+ """
91
+ <div style="text-align: center; margin-bottom: 24px; margin-top: 10px;">
92
+ <h1 style="font-size: 2.8em; margin-bottom: 5px; background: linear-gradient(90deg, #60a5fa, #a78bfa); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
93
+ ANTIGRAVITY CHAT
94
+ </h1>
95
+ <p style="font-size: 1.1em; color: #9ca3af; max-width: 600px; margin: 0 auto;">
96
+ A premium Claude-style chatbot environment designed for Hugging Face free tier.
97
+ Equipped with real-time web search, page scraping, and cognitive system reasoning.
98
+ </p>
99
+ </div>
100
+ """
101
+ )
102
+
103
+ with gr.Row():
104
+ # Side Control Panel (Sidebar)
105
+ with gr.Column(scale=3, elem_classes=["sidebar-panel"]):
106
+ gr.Markdown("### ⚙️ System Settings")
107
+
108
+ hardware_text = get_hardware_status()
109
+ gr.HTML(
110
+ f"""
111
+ <div style="font-size: 0.85em; padding: 8px 12px; background-color: rgba(255,255,255,0.03); border-radius: 8px; border: 1px solid rgba(255,255,255,0.05); margin-bottom: 15px;">
112
+ <span style="color: #9ca3af;">Host Hardware:</span><br/>
113
+ <strong style="color: #38bdf8;">{hardware_text}</strong>
114
+ </div>
115
+ """
116
+ )
117
+
118
+ # Mode selection
119
+ mode_dropdown = gr.Dropdown(
120
+ choices=list(MODEL_CONFIGS.keys()),
121
+ value="Local CPU (Lightweight)",
122
+ label="Inference Backend Mode",
123
+ interactive=True
124
+ )
125
+
126
+ # Model selection (changes dynamically based on mode)
127
+ model_choices = [m["name"] for m in MODEL_CONFIGS["Local CPU (Lightweight)"]]
128
+ default_model = next(m["name"] for m in MODEL_CONFIGS["Local CPU (Lightweight)"] if m["default"])
129
+
130
+ model_dropdown = gr.Dropdown(
131
+ choices=model_choices,
132
+ value=default_model,
133
+ label="Active Model",
134
+ interactive=True
135
+ )
136
+
137
+ # Web Search Toggle
138
+ enable_search = gr.Checkbox(
139
+ label="🔍 Enable Web Search (DuckDuckGo)",
140
+ value=False,
141
+ interactive=True
142
+ )
143
+
144
+ # Token field (hidden input for HF Serverless inference token)
145
+ hf_token = gr.Textbox(
146
+ label="Hugging Face API Token (optional)",
147
+ placeholder="hf_...",
148
+ type="password",
149
+ info="Required for gated Serverless models (e.g. Llama 3.3). Get one at hf.co/settings/tokens"
150
+ )
151
+
152
+ # Advanced Settings Accordion
153
+ with gr.Accordion("🛠️ Advanced Parameters", open=False):
154
+ system_prompt = gr.Textbox(
155
+ label="System Instruction Prompt",
156
+ value=SYSTEM_PROMPT,
157
+ lines=8,
158
+ max_lines=15
159
+ )
160
+
161
+ max_tokens = gr.Slider(
162
+ minimum=64,
163
+ maximum=4096,
164
+ value=1024,
165
+ step=64,
166
+ label="Max New Tokens"
167
+ )
168
+
169
+ temperature = gr.Slider(
170
+ minimum=0.0,
171
+ maximum=1.2,
172
+ value=0.7,
173
+ step=0.1,
174
+ label="Temperature (0.0 = deterministic)"
175
+ )
176
+
177
+ top_p = gr.Slider(
178
+ minimum=0.1,
179
+ maximum=1.0,
180
+ value=0.9,
181
+ step=0.05,
182
+ label="Top-P Sampling"
183
+ )
184
+
185
+ # System actions
186
+ clear_btn = gr.Button("🗑️ Clear Chat History", variant="secondary", elem_classes=["secondary-btn"])
187
+
188
+ # Main Chat Area
189
+ with gr.Column(scale=9):
190
+ chatbot = gr.Chatbot(
191
+ label="Chat Window",
192
+ elem_classes=["chatbot-container"],
193
+ show_label=False,
194
+ avatar_images=(None, "https://huggingface.co/front/assets/huggingface_logo-noborder.svg"),
195
+ height=580,
196
+ bubble_full_width=False
197
+ )
198
+
199
+ with gr.Row():
200
+ input_box = gr.Textbox(
201
+ placeholder="Ask Antigravity anything... (e.g., 'What happened in AI news this week?' with search on)",
202
+ show_label=False,
203
+ scale=10
204
+ )
205
+ submit_btn = gr.Button("Send", variant="primary", scale=1, elem_classes=["action-btn"])
206
+
207
+ # Prompts suggestions
208
+ gr.Markdown("💡 **Quick Prompts**")
209
+ with gr.Row():
210
+ suggestion_1 = gr.Button("Draft a clean Python function using asyncio to scrape web data.", variant="secondary", elem_classes=["secondary-btn"])
211
+ suggestion_2 = gr.Button("Search the web for the latest advancements in LLM reasoning models.", variant="secondary", elem_classes=["secondary-btn"])
212
+ suggestion_3 = gr.Button("Explain quantum computing superposition using a simple real-life analogy.", variant="secondary", elem_classes=["secondary-btn"])
213
+
214
+ # Define UI event linkages
215
+
216
+ # 1. Mode dropdown change updates the Model selection dropdown options
217
+ mode_dropdown.change(
218
+ fn=update_model_dropdown,
219
+ inputs=[mode_dropdown],
220
+ outputs=[model_dropdown]
221
+ )
222
+
223
+ # 2. Main submit event chain (for Enter key submit)
224
+ submit_event = input_box.submit(
225
+ fn=add_user_message,
226
+ inputs=[input_box, chatbot],
227
+ outputs=[input_box, chatbot],
228
+ queue=False
229
+ ).then(
230
+ fn=execute_chat_ui,
231
+ inputs=[
232
+ chatbot,
233
+ mode_dropdown,
234
+ model_dropdown,
235
+ system_prompt,
236
+ max_tokens,
237
+ temperature,
238
+ top_p,
239
+ enable_search,
240
+ hf_token
241
+ ],
242
+ outputs=[chatbot]
243
+ )
244
+
245
+ # 3. Submit button click event chain
246
+ click_event = submit_btn.click(
247
+ fn=add_user_message,
248
+ inputs=[input_box, chatbot],
249
+ outputs=[input_box, chatbot],
250
+ queue=False
251
+ ).then(
252
+ fn=execute_chat_ui,
253
+ inputs=[
254
+ chatbot,
255
+ mode_dropdown,
256
+ model_dropdown,
257
+ system_prompt,
258
+ max_tokens,
259
+ temperature,
260
+ top_p,
261
+ enable_search,
262
+ hf_token
263
+ ],
264
+ outputs=[chatbot]
265
+ )
266
+
267
+ # 4. Clear chat history button event
268
+ clear_btn.click(fn=lambda: None, outputs=chatbot, queue=False)
269
+
270
+ # 5. Suggestion prompt buttons click events
271
+ def load_suggestion(text):
272
+ # When clicked, populate textbox, enable web search if query implies it
273
+ search_enabled = "Search the web" in text or "latest advancements" in text
274
+ return text, gr.update(value=search_enabled)
275
+
276
+ suggestion_1.click(
277
+ fn=lambda: load_suggestion("Draft a clean Python function using asyncio to scrape web data."),
278
+ outputs=[input_box, enable_search]
279
+ )
280
+ suggestion_2.click(
281
+ fn=lambda: load_suggestion("Search the web for the latest advancements in LLM reasoning models."),
282
+ outputs=[input_box, enable_search]
283
+ )
284
+ suggestion_3.click(
285
+ fn=lambda: load_suggestion("Explain quantum computing superposition using a simple real-life analogy."),
286
+ outputs=[input_box, enable_search]
287
+ )
288
+
289
+ return demo