kirikir13 commited on
Commit
fac81d9
·
verified ·
1 Parent(s): ade509f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +460 -0
app.py ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # Qwen 3.6 27B CoderBot — Hugging Face ZeroGPU Space
3
+ # Model: DavidAU/Qwen3.6-27B-Heretic-Uncensored-FINETUNE-NEO-CODE-Di-IMatrix-MAX-GGUF
4
+ # Quant: Q4_K_M (~16GB, 94.5% of BF16) — fits ZeroGPU 48GB VRAM
5
+ # =============================================================================
6
+
7
+ import spaces
8
+ import gradio as gr
9
+ import os
10
+ import json
11
+ import tempfile
12
+ import traceback
13
+ from pathlib import Path
14
+ from typing import List, Dict, Optional, Generator
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # MODEL CONFIGURATION — change these env vars or edit directly
18
+ # ---------------------------------------------------------------------------
19
+
20
+ MODEL_REPO = os.environ.get(
21
+ "MODEL_REPO",
22
+ "DavidAU/Qwen3.6-27B-Heretic-Uncensored-FINETUNE-NEO-CODE-Di-IMatrix-MAX-GGUF",
23
+ )
24
+ MODEL_FILE = os.environ.get(
25
+ "MODEL_FILE",
26
+ "Qwen3.6-27B-NEO-CODE-HERE-2T-OT-Q4_K_M.gguf", # 16.1 GB, 94.5% of BF16
27
+ )
28
+ N_CTX = int(os.environ.get("N_CTX", "8192")) # context window
29
+ N_GPU_LAYERS = int(os.environ.get("N_GPU_LAYERS", "99")) # offload all layers
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # SYSTEM PROMPT — tuned for coding / code review
33
+ # ---------------------------------------------------------------------------
34
+
35
+ SYSTEM_PROMPT = """You are Qwen CoderBot — an uncensored 27B coding assistant. You excel at:
36
+
37
+ • Writing production-quality code in Python, TypeScript, JavaScript, Rust, Go, C/C++, SQL, shell, and more
38
+ • Debugging, refactoring, and reviewing complex code
39
+ • Explaining algorithms, data structures, architecture patterns, and system design
40
+ • Reading and understanding code files users share with you
41
+
42
+ Rules:
43
+ - Always put code in ```language ... ``` blocks with the correct language tag.
44
+ - When the user shares files, read them carefully before answering. Refer back to specific lines or functions.
45
+ - Be precise, concise, and thorough. Prefer working, runnable solutions.
46
+ - If you're unsure, say so — never fabricate APIs or libraries.
47
+ - For Qwen "thinking" mode, you may use ... blocks.
48
+ """
49
+
50
+ # ===========================================================================
51
+ # MODEL LOADER — lazy singleton, re-initialised inside @spaces.GPU when
52
+ # the GPU becomes available. We download the GGUF at module level (CPU)
53
+ # and stream it into GPU memory on first call.
54
+ # ===========================================================================
55
+
56
+ _llm = None
57
+ _model_path: Optional[str] = None
58
+
59
+
60
+ def _ensure_model_downloaded() -> str:
61
+ """Download the GGUF once to the persistent HuggingFace cache."""
62
+ global _model_path
63
+ if _model_path is not None:
64
+ return _model_path
65
+ from huggingface_hub import hf_hub_download
66
+ print(f"[CoderBot] Downloading {MODEL_FILE} from {MODEL_REPO} …")
67
+ _model_path = hf_hub_download(
68
+ repo_id=MODEL_REPO,
69
+ filename=MODEL_FILE,
70
+ )
71
+ print(f"[CoderBot] Model cached at {_model_path}")
72
+ return _model_path
73
+
74
+
75
+ def _get_llm():
76
+ """Return (or create) the llama-cpp Llama instance on GPU."""
77
+ global _llm
78
+ if _llm is not None:
79
+ return _llm
80
+ from llama_cpp import Llama
81
+ model_path = _ensure_model_downloaded()
82
+ print(f"[CoderBot] Loading model onto GPU (n_gpu_layers={N_GPU_LAYERS}) …")
83
+ _llm = Llama(
84
+ model_path=model_path,
85
+ n_ctx=N_CTX,
86
+ n_gpu_layers=N_GPU_LAYERS,
87
+ chat_format="chatml",
88
+ verbose=False,
89
+ seed=-1,
90
+ )
91
+ print("[CoderBot] Model loaded ✓")
92
+ return _llm
93
+
94
+
95
+ # ===========================================================================
96
+ # FILE PARSERS — extract text from every common dev / doc format
97
+ # ===========================================================================
98
+
99
+ def _read_text(path: str, encoding: str = "utf-8") -> str:
100
+ """Safe text-file reader with encoding fallback."""
101
+ encodings = [encoding, "utf-8-sig", "latin-1", "cp1252", "utf-16"]
102
+ for enc in encodings:
103
+ try:
104
+ with open(path, "r", encoding=enc) as f:
105
+ return f.read()
106
+ except (UnicodeDecodeError, UnicodeError):
107
+ continue
108
+ # Last resort
109
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
110
+ return f.read()
111
+
112
+
113
+ def parse_file(file_path: str, file_name: str) -> str:
114
+ """
115
+ Parse a single file and return a markdown-formatted string with its
116
+ contents, ready to be pasted into the model context.
117
+ """
118
+ suffix = Path(file_name).suffix.lower()
119
+ name = Path(file_name).name
120
+
121
+ # ---- Plain text / code files ----
122
+ if suffix in (
123
+ ".txt", ".md", ".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
124
+ ".css", ".scss", ".less", ".html", ".htm", ".xml", ".svg",
125
+ ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf",
126
+ ".sh", ".bash", ".zsh", ".fish", ".ps1", ".bat",
127
+ ".sql", ".graphql", ".prisma",
128
+ ".c", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".hh",
129
+ ".java", ".kt", ".kts", ".scala", ".groovy",
130
+ ".go", ".rs", ".rb", ".php", ".pl", ".pm",
131
+ ".swift", ".r", ".lua", ".zig", ".nim", ".dart",
132
+ ".dockerfile", ".makefile", ".cmake", ".gradle",
133
+ ".env", ".gitignore", ".editorconfig",
134
+ ".tf", ".tfvars", ".hcl",
135
+ ".vue", ".svelte", ".astro",
136
+ ".ipynb",
137
+ ):
138
+ content = _read_text(file_path)
139
+ lang = suffix.lstrip(".")
140
+ if suffix == ".md":
141
+ lang = "markdown"
142
+ elif suffix in (".yml",):
143
+ lang = "yaml"
144
+ elif suffix in (".tf", ".tfvars"):
145
+ lang = "hcl"
146
+ elif suffix in (".htm",):
147
+ lang = "html"
148
+ elif suffix in (".Dockerfile",):
149
+ lang = "dockerfile"
150
+ elif suffix == ".ipynb":
151
+ # Parse Jupyter notebook to extract code + markdown cells
152
+ try:
153
+ nb = json.loads(content)
154
+ lines = []
155
+ for cell in nb.get("cells", []):
156
+ cell_type = cell.get("cell_type", "code")
157
+ source = "".join(cell.get("source", []))
158
+ if cell_type == "code":
159
+ lines.append(f"```python\n{source}\n```")
160
+ else:
161
+ lines.append(source)
162
+ content = "\n\n".join(lines)
163
+ return f"### 📓 {name}\n\n{content}\n\n---\n"
164
+ except Exception:
165
+ pass
166
+ return f"### 📄 `{name}`\n```{lang}\n{content}\n```\n\n---\n"
167
+
168
+ # ---- JSON ----
169
+ if suffix == ".json":
170
+ content = _read_text(file_path)
171
+ try:
172
+ parsed = json.loads(content)
173
+ pretty = json.dumps(parsed, indent=2, ensure_ascii=False)
174
+ return f"### 📄 `{name}`\n```json\n{pretty}\n```\n\n---\n"
175
+ except Exception:
176
+ return f"### 📄 `{name}`\n```json\n{content}\n```\n\n---\n"
177
+
178
+ # ---- CSV ----
179
+ if suffix == ".csv":
180
+ import csv
181
+ import io
182
+ content = _read_text(file_path)
183
+ reader = csv.reader(io.StringIO(content))
184
+ rows = list(reader)
185
+ if len(rows) > 51: # truncate very large CSVs
186
+ truncated = rows[:50]
187
+ truncated.append([f"… {len(rows) - 50} more rows truncated"])
188
+ rows = truncated
189
+ col_widths = [max(len(str(cell)) for cell in col) for col in zip(*rows)]
190
+ table = "\n".join(
191
+ "| " + " | ".join(str(cell).ljust(w) for cell, w in zip(row, col_widths)) + " |"
192
+ for row in rows
193
+ )
194
+ header_sep = "|-" + "-|-".join("-" * w for w in col_widths) + "-|"
195
+ table = table.split("\n", 1)
196
+ table.insert(1, header_sep)
197
+ return f"### 📊 `{name}`\n" + "\n".join(table) + "\n\n---\n"
198
+
199
+ # ---- Word documents ----
200
+ if suffix == ".docx":
201
+ try:
202
+ import docx
203
+ doc = docx.Document(file_path)
204
+ text = "\n\n".join(p.text for p in doc.paragraphs if p.text.strip())
205
+ return f"### 📝 `{name}`\n{text}\n\n---\n"
206
+ except ImportError:
207
+ return f"### ⚠️ `{name}` (DOCX — install python-docx)\n\n---\n"
208
+ except Exception as e:
209
+ return f"### ⚠️ `{name}` (DOCX parse error: {e})\n\n---\n"
210
+
211
+ # ---- PDF ----
212
+ if suffix == ".pdf":
213
+ try:
214
+ import pdfplumber
215
+ with pdfplumber.open(file_path) as pdf:
216
+ text = "\n\n".join(
217
+ page.extract_text() or "" for page in pdf.pages
218
+ )
219
+ return f"### 📑 `{name}`\n{text}\n\n---\n"
220
+ except ImportError:
221
+ return f"### ⚠️ `{name}` (PDF — install pdfplumber)\n\n---\n"
222
+ except Exception as e:
223
+ return f"### ⚠️ `{name}` (PDF parse error: {e})\n\n---\n"
224
+
225
+ # ---- Fallback: try to read as text ----
226
+ try:
227
+ content = _read_text(file_path)
228
+ return f"### 📄 `{name}`\n```\n{content[:10000]}\n```\n\n---\n"
229
+ except Exception as e:
230
+ return f"### ❌ `{name}` (could not read: {e})\n\n---\n"
231
+
232
+
233
+ def parse_all_files(files) -> str:
234
+ """Parse a list of Gradio file objects into a single context string."""
235
+ if not files:
236
+ return ""
237
+ parts = []
238
+ for f in files:
239
+ # Gradio file objects may be dicts or have .name attribute
240
+ if isinstance(f, dict):
241
+ path = f.get("path") or f.get("name")
242
+ name = f.get("orig_name") or f.get("name", "unknown")
243
+ elif hasattr(f, "name"):
244
+ path = f.name
245
+ name = getattr(f, "orig_name", Path(path).name)
246
+ else:
247
+ path = str(f)
248
+ name = Path(path).name
249
+ try:
250
+ parts.append(parse_file(path, name))
251
+ except Exception as e:
252
+ parts.append(f"### ❌ `{name}`\nParse error: {e}\n\n---\n")
253
+ return "\n".join(parts)
254
+
255
+
256
+ # ===========================================================================
257
+ # MAIN GENERATION FUNCTION — wrapped with @spaces.GPU for ZeroGPU
258
+ # ===========================================================================
259
+
260
+ @spaces.GPU
261
+ def chatbot_respond(
262
+ message: str,
263
+ history: List,
264
+ uploaded_files,
265
+ ) -> Generator[List, None, None]:
266
+ """
267
+ Called on every user message. history is the Gradio chatbot history
268
+ (list of [user_msg, bot_msg] pairs). uploaded_files is the current
269
+ file list from gr.File.
270
+
271
+ Yields the updated history list after each token chunk, giving a
272
+ streaming effect in the Gradio Chatbot.
273
+ """
274
+ # ---- 1. Parse any newly uploaded files ----
275
+ file_context = parse_all_files(uploaded_files)
276
+
277
+ # ---- 2. Build the message list for llama-cpp-python (ChatML format) ----
278
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}]
279
+
280
+ # If files were uploaded, inject their content as a system-level note
281
+ if file_context:
282
+ messages.append({
283
+ "role": "user",
284
+ "content": (
285
+ "[The user has uploaded the following files. "
286
+ "Read them carefully and refer to them in your answers.]\n\n"
287
+ + file_context
288
+ ),
289
+ })
290
+ messages.append({
291
+ "role": "assistant",
292
+ "content": (
293
+ "Got it! I've read through all the uploaded files. "
294
+ "Ask me anything about them."
295
+ ),
296
+ })
297
+
298
+ # Append the real conversation history
299
+ for user_msg, bot_msg in history:
300
+ if user_msg:
301
+ messages.append({"role": "user", "content": user_msg})
302
+ if bot_msg:
303
+ messages.append({"role": "assistant", "content": bot_msg})
304
+
305
+ # Append the current message
306
+ messages.append({"role": "user", "content": message})
307
+
308
+ # ---- 3. Stream the completion ----
309
+ llm = _get_llm()
310
+ stream = llm.create_chat_completion(
311
+ messages=messages,
312
+ temperature=0.6, # lower = more precise for coding
313
+ top_p=0.8,
314
+ top_k=20,
315
+ max_tokens=4096,
316
+ stream=True,
317
+ stop=["<|im_end|>", "<|endoftext|>"],
318
+ )
319
+
320
+ partial = ""
321
+ for chunk in stream:
322
+ choices = chunk.get("choices", [])
323
+ if choices:
324
+ delta = choices[0].get("delta", {})
325
+ content = delta.get("content", "")
326
+ if content:
327
+ partial += content
328
+ # YIELD the updated history so Gradio streams into the chat UI
329
+ yield history + [[message, partial]]
330
+
331
+ # Final yield to guarantee the full message is rendered
332
+ yield history + [[message, partial]]
333
+
334
+
335
+ # ===========================================================================
336
+ # GRADIO UI — clean two‑column layout: chat on the left, file drop on right
337
+ # ===========================================================================
338
+
339
+ def create_demo() -> gr.Blocks:
340
+ css = """
341
+ .file-upload-col { background: var(--background-fill-secondary); border-radius: 12px; padding: 16px; }
342
+ footer { display: none !important; }
343
+ """
344
+ with gr.Blocks(
345
+ css=css,
346
+ theme=gr.themes.Soft(primary_hue="violet", secondary_hue="slate"),
347
+ title="Qwen 3.6 27B CoderBot",
348
+ ) as demo:
349
+
350
+ gr.Markdown(
351
+ """
352
+ # 🧠 Qwen 3.6 27B CoderBot
353
+ **Uncensored · Code-optimized · ZeroGPU-powered**
354
+ Drop code files, docs, JSON, CSVs — chat about them with a 27B coding LLM.
355
+ """,
356
+ )
357
+
358
+ with gr.Row(equal_height=True):
359
+ # ---- LEFT: Chat ----
360
+ with gr.Column(scale=3):
361
+ chatbot = gr.Chatbot(
362
+ label="Chat",
363
+ height=580,
364
+ bubble_full_width=False,
365
+ avatar_images=(
366
+ None,
367
+ "https://huggingface.co/front/assets/huggingface_logo-noborder.svg",
368
+ ),
369
+ )
370
+ with gr.Row():
371
+ msg = gr.Textbox(
372
+ placeholder="Ask about your code, or just start chatting …",
373
+ scale=8,
374
+ show_label=False,
375
+ container=False,
376
+ )
377
+ send = gr.Button("▶", scale=1, variant="primary", min_width=48)
378
+ with gr.Row():
379
+ clear_btn = gr.Button("🗑 Clear chat", size="sm", scale=1)
380
+
381
+ # ---- RIGHT: File upload + info ----
382
+ with gr.Column(scale=1, elem_classes="file-upload-col"):
383
+ gr.Markdown("### 📎 Drop Files")
384
+ files = gr.File(
385
+ file_count="multiple",
386
+ label="Upload code, docs, data …",
387
+ file_types=[
388
+ ".py", ".ts", ".tsx", ".js", ".jsx", ".mjs",
389
+ ".json", ".yaml", ".yml", ".toml",
390
+ ".md", ".txt", ".csv",
391
+ ".html", ".css", ".scss", ".xml", ".svg",
392
+ ".c", ".cpp", ".h", ".hpp", ".java", ".kt",
393
+ ".go", ".rs", ".rb", ".php", ".swift",
394
+ ".sql", ".sh", ".bash", ".ps1",
395
+ ".docx", ".pdf",
396
+ ".env", ".gitignore", ".cfg", ".ini", ".conf",
397
+ ".vue", ".svelte", ".dockerfile",
398
+ ],
399
+ )
400
+ gr.Markdown(
401
+ """
402
+ **Supported:** `.py` `.ts` `.js` `.json` `.md` `.docx` `.pdf` `.csv`
403
+ `.yaml` `.toml` `.html` `.css` `.sql` `.go` `.rs` `.java` + more
404
+
405
+ Files are parsed and sent as context — the model reads them
406
+ before answering.
407
+ """
408
+ )
409
+ uploaded_info = gr.Markdown("_No files uploaded yet._")
410
+ file_content_state = gr.State("")
411
+
412
+ # ---- Event wiring ----
413
+
414
+ def update_file_info(uploaded):
415
+ if not uploaded:
416
+ return "_No files uploaded._", ""
417
+ names = []
418
+ for f in uploaded:
419
+ if isinstance(f, dict):
420
+ names.append(f.get("orig_name", "?"))
421
+ else:
422
+ names.append(getattr(f, "orig_name", Path(str(f)).name))
423
+ label = "**Uploaded:**\n" + "\n".join(f"• `{n}`" for n in names)
424
+ return label, parse_all_files(uploaded)
425
+
426
+ def respond(message, history, current_files):
427
+ """Generator wrapper that streams into the chatbot."""
428
+ for updated_history in chatbot_respond(message, history, current_files):
429
+ yield updated_history
430
+
431
+ # Trigger response on text submit or send button
432
+ msg.submit(
433
+ respond,
434
+ inputs=[msg, chatbot, files],
435
+ outputs=[chatbot],
436
+ ).then(lambda: "", None, [msg])
437
+
438
+ send.click(
439
+ respond,
440
+ inputs=[msg, chatbot, files],
441
+ outputs=[chatbot],
442
+ ).then(lambda: "", None, [msg])
443
+
444
+ # Clear chat
445
+ clear_btn.click(lambda: None, None, chatbot, queue=False)
446
+
447
+ # File upload feedback
448
+ files.change(update_file_info, files, [uploaded_info, file_content_state])
449
+
450
+ return demo
451
+
452
+
453
+ # ===========================================================================
454
+ # LAUNCH
455
+ # ===========================================================================
456
+
457
+ if __name__ == "__main__":
458
+ demo = create_demo()
459
+ demo.queue(default_concurrency_limit=1, max_size=4)
460
+ demo.launch()