LH-Tech-AI commited on
Commit
2b146f9
·
verified ·
1 Parent(s): 59a260f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +447 -0
app.py ADDED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import torch
3
+ from transformers import (
4
+ AutoTokenizer,
5
+ AutoModelForCausalLM,
6
+ T5ForConditionalGeneration,
7
+ T5Tokenizer,
8
+ )
9
+ import time
10
+ import hashlib
11
+ from typing import List, Dict, Tuple, Optional
12
+ import json
13
+ import os
14
+
15
+ # ============================================================
16
+ # Configuration
17
+ # ============================================================
18
+
19
+ DEFAULT_MODEL = "SupraLabs/Supra-50M-Instruct"
20
+ TITLE_MODEL_ID = "SupraLabs/Supra-Title-Flan-85M"
21
+
22
+ # Available models
23
+ AVAILABLE_MODELS = {
24
+ "Supra-50M-Instruct": {
25
+ "id": "SupraLabs/Supra-50M-Instruct",
26
+ "type": "instruct",
27
+ "description": "50M parameter instruction-tuned model, suitable for general chat"
28
+ },
29
+ "Supra-50M-Reasoning": {
30
+ "id": "SupraLabs/Supra-50M-Reasoning",
31
+ "type": "reasoning",
32
+ "description": "50M reasoning model that outputs a thought process"
33
+ },
34
+ "Supra-1.5-50M-Instruct-exp": {
35
+ "id": "SupraLabs/Supra-1.5-50M-Instruct-exp",
36
+ "type": "instruct",
37
+ "description": "Experimental 50M instruct model with 5K context length"
38
+ },
39
+ "Supra-50M-Base": {
40
+ "id": "SupraLabs/Supra-50M-Base",
41
+ "type": "base",
42
+ "description": "50M base model, pure next‑token prediction"
43
+ },
44
+ "StorySupra-10M": {
45
+ "id": "SupraLabs/StorySupra-10M",
46
+ "type": "base",
47
+ "description": "10M story generation model"
48
+ },
49
+ "Supra-Mini-v5-8M": {
50
+ "id": "SupraLabs/Supra-Mini-v5-8M",
51
+ "type": "base",
52
+ "description": "8M ultra‑small model for fast experimentation"
53
+ }
54
+ }
55
+
56
+ # ============================================================
57
+ # Model caching
58
+ # ============================================================
59
+
60
+ _model_cache = {}
61
+ _title_model = None
62
+ _title_tokenizer = None
63
+
64
+ # ============================================================
65
+ # Title generator (Supra-Title-Flan-85M)
66
+ # ============================================================
67
+
68
+ def load_title_model():
69
+ """Load the title generation model."""
70
+ global _title_model, _title_tokenizer
71
+ if _title_model is None:
72
+ print(f"[*] Loading title model: {TITLE_MODEL_ID}")
73
+ _title_tokenizer = T5Tokenizer.from_pretrained(TITLE_MODEL_ID)
74
+ _title_model = T5ForConditionalGeneration.from_pretrained(
75
+ TITLE_MODEL_ID,
76
+ torch_dtype=torch.float32
77
+ )
78
+ _title_model.eval()
79
+ return _title_model, _title_tokenizer
80
+
81
+ def generate_chat_title(user_message: str, max_new_tokens: int = 32) -> str:
82
+ """Generate a conversation title based on the first user message."""
83
+ try:
84
+ model, tokenizer = load_title_model()
85
+ prompt = f"generate title: {user_message.strip()}"
86
+ inputs = tokenizer(
87
+ prompt,
88
+ return_tensors="pt",
89
+ max_length=512,
90
+ truncation=True,
91
+ )
92
+ with torch.no_grad():
93
+ outputs = model.generate(
94
+ **inputs,
95
+ max_new_tokens=max_new_tokens,
96
+ num_beams=4,
97
+ early_stopping=True,
98
+ )
99
+ title = tokenizer.decode(outputs[0], skip_special_tokens=True)
100
+ if len(title) > 50:
101
+ title = title[:47] + "..."
102
+ return title.strip() or "New Conversation"
103
+ except Exception as e:
104
+ print(f"[!] Title generation failed: {e}")
105
+ return "New Conversation"
106
+
107
+ # ============================================================
108
+ # Conversation model loader
109
+ # ============================================================
110
+
111
+ def load_model(model_key: str):
112
+ """Load the specified conversation model."""
113
+ if model_key in _model_cache:
114
+ return _model_cache[model_key]
115
+
116
+ model_info = AVAILABLE_MODELS.get(model_key)
117
+ if not model_info:
118
+ raise ValueError(f"Unknown model: {model_key}")
119
+
120
+ model_id = model_info["id"]
121
+ model_type = model_info["type"]
122
+
123
+ print(f"[*] Loading model: {model_id}")
124
+
125
+ device = "cuda" if torch.cuda.is_available() else "cpu"
126
+ torch_dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32
127
+
128
+ tokenizer = AutoTokenizer.from_pretrained(model_id)
129
+ model = AutoModelForCausalLM.from_pretrained(
130
+ model_id,
131
+ torch_dtype=torch_dtype,
132
+ device_map="auto" if torch.cuda.is_available() else None
133
+ )
134
+ if not torch.cuda.is_available():
135
+ model = model.to(device)
136
+ model.eval()
137
+
138
+ _model_cache[model_key] = (model, tokenizer, model_type, device)
139
+ return _model_cache[model_key]
140
+
141
+ # ============================================================
142
+ # Prompt construction
143
+ # ============================================================
144
+
145
+ def build_prompt(model_type: str, message: str, history: List[Tuple[str, str]]) -> str:
146
+ """Construct the prompt according to the model type."""
147
+ # Build conversation history in a standard format
148
+ conversation = ""
149
+ for user_msg, bot_msg in history:
150
+ conversation += f"User: {user_msg}\nAssistant: {bot_msg}\n"
151
+ conversation += f"User: {message}\nAssistant:"
152
+
153
+ if model_type == "reasoning":
154
+ # For reasoning models, we add the thought trigger token.
155
+ # The model will then generate <|begin_of_thought|> ... <|end_of_thought|>
156
+ # followed by <|begin_of_solution|> ... <|end_of_solution|>
157
+ return conversation + " <|begin_of_thought|>"
158
+ else:
159
+ return conversation
160
+
161
+ # ============================================================
162
+ # Response generation
163
+ # ============================================================
164
+
165
+ def generate_response(
166
+ model_key: str,
167
+ message: str,
168
+ history: List[Tuple[str, str]],
169
+ max_new_tokens: int = 512,
170
+ temperature: float = 0.7,
171
+ top_p: float = 0.9,
172
+ top_k: int = 50,
173
+ repetition_penalty: float = 1.1,
174
+ ) -> str:
175
+ """Generate a response from the selected model."""
176
+ try:
177
+ model, tokenizer, model_type, device = load_model(model_key)
178
+
179
+ prompt = build_prompt(model_type, message, history)
180
+
181
+ inputs = tokenizer(
182
+ prompt,
183
+ return_tensors="pt",
184
+ truncation=True,
185
+ max_length=2048 if "1.5" in model_key else 1024,
186
+ )
187
+ inputs = {k: v.to(device) for k, v in inputs.items()}
188
+
189
+ with torch.no_grad():
190
+ outputs = model.generate(
191
+ **inputs,
192
+ max_new_tokens=max_new_tokens,
193
+ temperature=temperature,
194
+ top_p=top_p,
195
+ top_k=top_k,
196
+ repetition_penalty=repetition_penalty,
197
+ do_sample=True,
198
+ pad_token_id=tokenizer.eos_token_id,
199
+ eos_token_id=tokenizer.eos_token_id,
200
+ )
201
+
202
+ full_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
203
+
204
+ # Extract the assistant's reply (remove the prompt)
205
+ if prompt in full_text:
206
+ response = full_text[len(prompt):].strip()
207
+ else:
208
+ # Fallback: split by "Assistant:" if present
209
+ parts = full_text.split("Assistant:")
210
+ response = parts[-1].strip() if len(parts) > 1 else full_text.strip()
211
+
212
+ # For reasoning models, keep the whole thought+answer structure
213
+ if model_type == "reasoning" and "<|begin_of_thought|>" in response:
214
+ # We return everything after the prompt; the user will see the thought process.
215
+ pass
216
+
217
+ return response or "(Model did not produce a valid response)"
218
+
219
+ except Exception as e:
220
+ print(f"[!] Generation error: {e}")
221
+ return f"Error: {str(e)}"
222
+
223
+ # ============================================================
224
+ # Gradio Interface
225
+ # ============================================================
226
+
227
+ def chat_interface(
228
+ message: str,
229
+ history: List[Dict],
230
+ model_choice: str,
231
+ temperature: float,
232
+ max_tokens: int,
233
+ ):
234
+ """Gradio chat interface callback."""
235
+ if not message or not message.strip():
236
+ yield history, ""
237
+ return
238
+
239
+ # Convert history format
240
+ formatted_history = []
241
+ for i in range(0, len(history), 2):
242
+ if i + 1 < len(history):
243
+ formatted_history.append((history[i]["content"], history[i+1]["content"]))
244
+
245
+ response = generate_response(
246
+ model_choice,
247
+ message,
248
+ formatted_history,
249
+ max_new_tokens=max_tokens,
250
+ temperature=temperature,
251
+ )
252
+
253
+ history.append({"role": "user", "content": message})
254
+ history.append({"role": "assistant", "content": response})
255
+
256
+ yield history, ""
257
+
258
+ def get_title_from_first_message(message: str) -> str:
259
+ """Generate a title from the first user message."""
260
+ if message and message.strip():
261
+ return generate_chat_title(message)
262
+ return "New Conversation"
263
+
264
+ # ============================================================
265
+ # Create Gradio app
266
+ # ============================================================
267
+
268
+ def create_app():
269
+ """Create and return the Gradio Blocks app."""
270
+
271
+ with gr.Blocks(
272
+ title="SupraChat – SupraLabs Chat Interface",
273
+ theme=gr.themes.Soft(
274
+ primary_hue="blue",
275
+ secondary_hue="gray",
276
+ neutral_hue="gray",
277
+ ),
278
+ css="""
279
+ .chatbot-container {
280
+ max-width: 800px;
281
+ margin: 0 auto;
282
+ }
283
+ .model-selector {
284
+ margin-bottom: 10px;
285
+ }
286
+ .title-input {
287
+ font-size: 1.2em;
288
+ font-weight: bold;
289
+ }
290
+ """
291
+ ) as demo:
292
+
293
+ gr.Markdown("""
294
+ # 🤖 SupraChat
295
+
296
+ Chat interface powered by SupraLabs' ultra‑small language models.
297
+ Conversation history is stored in RAM and cleared when you leave the page.
298
+ """)
299
+
300
+ with gr.Row():
301
+ with gr.Column(scale=4):
302
+ model_choice = gr.Dropdown(
303
+ choices=list(AVAILABLE_MODELS.keys()),
304
+ value=DEFAULT_MODEL,
305
+ label="Select Model",
306
+ info="Different models have different strengths",
307
+ )
308
+ with gr.Column(scale=2):
309
+ temperature = gr.Slider(
310
+ minimum=0.1,
311
+ maximum=1.5,
312
+ value=0.7,
313
+ step=0.1,
314
+ label="Temperature",
315
+ info="Higher = more creative",
316
+ )
317
+ with gr.Column(scale=2):
318
+ max_tokens = gr.Slider(
319
+ minimum=64,
320
+ maximum=1024,
321
+ value=512,
322
+ step=64,
323
+ label="Max New Tokens",
324
+ info="Maximum length of the reply",
325
+ )
326
+
327
+ chatbot = gr.Chatbot(
328
+ label="Conversation",
329
+ type="messages",
330
+ height=500,
331
+ )
332
+
333
+ with gr.Row():
334
+ msg = gr.Textbox(
335
+ label="Message",
336
+ placeholder="Type your message here...",
337
+ scale=9,
338
+ container=False,
339
+ )
340
+ send_btn = gr.Button("Send", scale=1, variant="primary")
341
+
342
+ with gr.Row():
343
+ clear_btn = gr.Button("🗑️ Clear Chat", variant="secondary", size="sm")
344
+ title_display = gr.Textbox(
345
+ label="Conversation Title",
346
+ placeholder="Auto‑generated from the first message",
347
+ interactive=False,
348
+ scale=1,
349
+ )
350
+
351
+ state = gr.State([])
352
+
353
+ # ============================================================
354
+ # Event handlers
355
+ # ============================================================
356
+
357
+ def respond(
358
+ message: str,
359
+ history: List[Dict],
360
+ model: str,
361
+ temp: float,
362
+ max_tok: int,
363
+ ):
364
+ if not message or not message.strip():
365
+ return history, "", history, ""
366
+
367
+ # Generate title on first message
368
+ title = ""
369
+ if len(history) == 0:
370
+ title = get_title_from_first_message(message)
371
+
372
+ # Generate response
373
+ formatted_history = []
374
+ for i in range(0, len(history), 2):
375
+ if i + 1 < len(history):
376
+ formatted_history.append((history[i]["content"], history[i+1]["content"]))
377
+
378
+ response = generate_response(
379
+ model,
380
+ message,
381
+ formatted_history,
382
+ max_new_tokens=max_tok,
383
+ temperature=temp,
384
+ )
385
+
386
+ history.append({"role": "user", "content": message})
387
+ history.append({"role": "assistant", "content": response})
388
+
389
+ # If this was the first message, set title
390
+ if len(history) == 2:
391
+ title = get_title_from_first_message(message)
392
+
393
+ return history, "", history, title
394
+
395
+ def clear_chat():
396
+ return [], "", "New Conversation"
397
+
398
+ # Send button
399
+ send_btn.click(
400
+ fn=respond,
401
+ inputs=[msg, state, model_choice, temperature, max_tokens],
402
+ outputs=[chatbot, msg, state, title_display],
403
+ )
404
+
405
+ # Enter key
406
+ msg.submit(
407
+ fn=respond,
408
+ inputs=[msg, state, model_choice, temperature, max_tokens],
409
+ outputs=[chatbot, msg, state, title_display],
410
+ )
411
+
412
+ # Clear
413
+ clear_btn.click(
414
+ fn=clear_chat,
415
+ inputs=[],
416
+ outputs=[chatbot, msg, title_display],
417
+ ).then(
418
+ lambda: [],
419
+ outputs=[state]
420
+ )
421
+
422
+ gr.Markdown("""
423
+ ---
424
+ ### 📋 Model Overview
425
+
426
+ | Model | Type | Description |
427
+ |-------|------|-------------|
428
+ | **Supra-50M-Instruct** | Instruct | General‑purpose chat, 50M parameters |
429
+ | **Supra-50M-Reasoning** | Reasoning | Includes a thought process for complex tasks |
430
+ | **Supra-1.5-50M-Instruct-exp** | Instruct | Experimental, 5K context window |
431
+ | **Supra-50M-Base** | Base | Raw language modelling, no instruction tuning |
432
+ | **StorySupra-10M** | Base | Specialised for story generation |
433
+ | **Supra-Mini-v5-8M** | Base | Extremely small, fast responses |
434
+
435
+ > 💡 **Note**: Conversation history is kept in memory only. It will be cleared when you reload or close the page.
436
+ """)
437
+
438
+ return demo
439
+
440
+ # ============================================================
441
+ # Launch
442
+ # ============================================================
443
+
444
+ if __name__ == "__main__":
445
+ demo = create_app()
446
+ demo.queue()
447
+ demo.launch(share=False)