Amala06 commited on
Commit
ca32de3
Β·
verified Β·
1 Parent(s): 3dff51c

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +411 -0
app.py ADDED
@@ -0,0 +1,411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Python Code Review Tool β€” Hugging Face Spaces
3
+ LLM: google/gemma-2b-it (local, no token restrictions)
4
+ Hardware: CPU-optimised with 4-bit quantisation
5
+ Architecture: UI β†’ Validation β†’ Pre-processing β†’ Generation β†’ Output
6
+ """
7
+
8
+ import ast
9
+ import re
10
+ import textwrap
11
+ import threading
12
+ from dataclasses import dataclass, field
13
+
14
+ import gradio as gr
15
+ import torch
16
+ from transformers import (
17
+ AutoTokenizer,
18
+ AutoModelForCausalLM,
19
+ BitsAndBytesConfig,
20
+ TextIteratorStreamer,
21
+ )
22
+
23
+ # ─────────────────────────────────────────────────────────────
24
+ # MODEL LOADING (runs once at startup)
25
+ # ─────────────────────────────────────────────────────────────
26
+
27
+ MODEL_ID = "google/gemma-2b-it"
28
+
29
+ print("Loading tokeniser…")
30
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
31
+
32
+ # 4-bit quant keeps RAM under ~3 GB β€” safe on HF free CPU
33
+ bnb_config = BitsAndBytesConfig(
34
+ load_in_4bit=True,
35
+ bnb_4bit_compute_dtype=torch.float32, # float32 for CPU
36
+ bnb_4bit_use_double_quant=True,
37
+ bnb_4bit_quant_type="nf4",
38
+ )
39
+
40
+ print("Loading model (4-bit)…")
41
+ model = AutoModelForCausalLM.from_pretrained(
42
+ MODEL_ID,
43
+ quantization_config=bnb_config,
44
+ device_map="cpu",
45
+ low_cpu_mem_usage=True,
46
+ )
47
+ model.eval()
48
+ print("Model ready βœ“")
49
+
50
+
51
+ # ─────────────────────────────────────────────────────────────
52
+ # VALIDATION & FLOW MANAGEMENT MODULE
53
+ # ─────────────────────────────────────────────────────────────
54
+
55
+ @dataclass
56
+ class ValidationResult:
57
+ valid: bool
58
+ error: str = ""
59
+ warnings: list = field(default_factory=list)
60
+
61
+
62
+ def validate_input(code: str, description: str) -> ValidationResult:
63
+ warnings = []
64
+
65
+ if not code.strip():
66
+ return ValidationResult(False, "❌ Code input is empty.")
67
+ if not description.strip():
68
+ return ValidationResult(False, "❌ Description is empty.")
69
+ if len(code.strip()) < 10:
70
+ return ValidationResult(False, "❌ Code too short to review.")
71
+ if len(description.strip()) < 5:
72
+ return ValidationResult(False, "❌ Description too brief.")
73
+ if len(code) > 30_000:
74
+ return ValidationResult(False, "❌ Code exceeds 30,000 characters. Split into smaller sections.")
75
+
76
+ python_hints = ["def ", "class ", "import ", "for ", "while ", "if ", "print", "return", "="]
77
+ if not any(kw in code for kw in python_hints):
78
+ warnings.append("⚠️ Doesn't look like Python β€” proceeding anyway.")
79
+
80
+ try:
81
+ ast.parse(code)
82
+ except SyntaxError as e:
83
+ warnings.append(f"⚠️ Syntax error at line {e.lineno}: {e.msg} β€” review will still run.")
84
+
85
+ return ValidationResult(True, warnings=warnings)
86
+
87
+
88
+ # ─────────────────────────────────────────────────────────────
89
+ # PRE-PROCESSING MODULE
90
+ # ─────────────────────────────────────────────────────────────
91
+
92
+ def estimate_complexity(code: str) -> str:
93
+ lines = [l for l in code.splitlines() if l.strip() and not l.strip().startswith("#")]
94
+ funcs = len(re.findall(r"^\s*def ", code, re.MULTILINE))
95
+ classes = len(re.findall(r"^\s*class ", code, re.MULTILINE))
96
+ if len(lines) < 20 and funcs <= 1 and classes == 0:
97
+ return "beginner"
98
+ elif len(lines) < 100 and classes <= 2:
99
+ return "intermediate"
100
+ return "advanced"
101
+
102
+
103
+ def extract_entities(code: str) -> str:
104
+ try:
105
+ tree = ast.parse(code)
106
+ entities = []
107
+ for node in ast.walk(tree):
108
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
109
+ entities.append(f"function `{node.name}`")
110
+ elif isinstance(node, ast.ClassDef):
111
+ entities.append(f"class `{node.name}`")
112
+ return ", ".join(entities[:10]) if entities else "none detected"
113
+ except Exception:
114
+ return "parse skipped (syntax errors present)"
115
+
116
+
117
+ def build_prompt(code: str, description: str, level: str) -> str:
118
+ """
119
+ Gemma-IT chat format:
120
+ <start_of_turn>user\\n…<end_of_turn>\\n<start_of_turn>model\\n
121
+ """
122
+ entities = extract_entities(code)
123
+ dedented = textwrap.dedent(code).strip()
124
+
125
+ user_msg = f"""You are an expert Python code reviewer. Review the following {level}-level Python code carefully.
126
+
127
+ ## What the code should do
128
+ {description.strip()}
129
+
130
+ ## Detected entities
131
+ {entities}
132
+
133
+ ## Code
134
+ ```python
135
+ {dedented}
136
+ ```
137
+
138
+ Write a detailed markdown review covering every section below.
139
+
140
+ ### 1. 🎯 Description Alignment
141
+ Does the code do what the description says? List any gaps or mismatches.
142
+
143
+ ### 2. πŸ› Bugs & Correctness
144
+ List every bug with line references and corrected code snippets.
145
+
146
+ ### 3. ⚑ Performance
147
+ Identify inefficiencies. Suggest faster alternatives with complexity notes.
148
+
149
+ ### 4. πŸ—οΈ Code Quality & Pythonic Style
150
+ PEP 8 compliance, naming, readability, idiomatic Python patterns.
151
+
152
+ ### 5. πŸ”’ Edge Cases & Robustness
153
+ What inputs could break this? Add guard clauses or error handling.
154
+
155
+ ### 6. βœ… Top 3 Quick Wins
156
+ The three highest-impact improvements to make first."""
157
+
158
+ return (
159
+ f"<start_of_turn>user\n{user_msg}<end_of_turn>\n"
160
+ "<start_of_turn>model\n"
161
+ )
162
+
163
+
164
+ # ─────────────────────────────────────────────────────────────
165
+ # GENERATION MODULE β€” no max_new_tokens cap
166
+ # ─────────────────────────────────────────────────────────────
167
+
168
+ def stream_review(prompt: str):
169
+ """Yields token chunks. Generation stops at natural EOS β€” no token limit."""
170
+ inputs = tokenizer(prompt, return_tensors="pt")
171
+ input_ids = inputs["input_ids"] # CPU tensor
172
+
173
+ streamer = TextIteratorStreamer(
174
+ tokenizer,
175
+ skip_prompt=True,
176
+ skip_special_tokens=True,
177
+ )
178
+
179
+ gen_kwargs = dict(
180
+ input_ids=input_ids,
181
+ streamer=streamer,
182
+ do_sample=True,
183
+ temperature=0.3, # focused, consistent output
184
+ top_p=0.9,
185
+ repetition_penalty=1.15,
186
+ # ← No max_new_tokens: model runs until EOS
187
+ )
188
+
189
+ thread = threading.Thread(target=model.generate, kwargs=gen_kwargs)
190
+ thread.start()
191
+
192
+ for token in streamer:
193
+ yield token
194
+
195
+ thread.join()
196
+
197
+
198
+ # ─────────────────────────────────────────────────────────────
199
+ # OUTPUT MODULE
200
+ # ─────────────────────────────────────────────────────────────
201
+
202
+ def format_output(raw: str, warnings: list) -> str:
203
+ warning_block = ""
204
+ if warnings:
205
+ warning_block = "\n".join(f"> {w}" for w in warnings) + "\n\n---\n\n"
206
+ cleaned = raw.strip().removesuffix("<end_of_turn>").strip()
207
+ return warning_block + cleaned
208
+
209
+
210
+ # ─────────────────────────────────────────────────────────────
211
+ # MAIN PIPELINE
212
+ # ─────────────────────────────────────────────────────────────
213
+
214
+ def review_pipeline(code: str, description: str):
215
+ # 1. Validate
216
+ result = validate_input(code, description)
217
+ if not result.valid:
218
+ yield result.error
219
+ return
220
+
221
+ header = ""
222
+ if result.warnings:
223
+ header = "\n".join(result.warnings) + "\n\n---\n\n"
224
+
225
+ yield header + "*⏳ Generating review with Gemma 2B β€” ~1–3 min on CPU. Streaming token by token…*\n\n"
226
+
227
+ # 2. Pre-process
228
+ level = estimate_complexity(code)
229
+ prompt = build_prompt(code, description, level)
230
+
231
+ # 3. Stream generation
232
+ accumulated = ""
233
+ try:
234
+ for chunk in stream_review(prompt):
235
+ accumulated += chunk
236
+ yield header + accumulated
237
+ except Exception as e:
238
+ yield header + accumulated + f"\n\n❌ Generation error: {e}"
239
+ return
240
+
241
+ # 4. Final clean output
242
+ yield format_output(header + accumulated, [])
243
+
244
+
245
+ # ─────────────────────────────────────────────────────────────
246
+ # UI MODULE
247
+ # ─────────────────────────────────────────────────────────────
248
+
249
+ EXAMPLES = [
250
+ [
251
+ "def find_duplicates(lst):\n duplicates = []\n for i in range(len(lst)):\n for j in range(len(lst)):\n if i != j and lst[i] == lst[j]:\n if lst[i] not in duplicates:\n duplicates.append(lst[i])\n return duplicates",
252
+ "Find all duplicate values in a list",
253
+ ],
254
+ [
255
+ "import requests\n\ndef fetch_user(user_id):\n r = requests.get(f'https://api.example.com/users/{user_id}')\n data = r.json()\n return data['name'], data['email']",
256
+ "Fetch a user's name and email from a REST API",
257
+ ],
258
+ [
259
+ "class BankAccount:\n def __init__(self, owner, balance=0):\n self.owner = owner\n self.balance = balance\n\n def deposit(self, amount):\n self.balance += amount\n\n def withdraw(self, amount):\n self.balance -= amount\n\n def get_balance(self):\n return self.balance",
260
+ "A simple bank account class with deposit and withdraw",
261
+ ],
262
+ [
263
+ "def quicksort(arr):\n if len(arr) <= 1:\n return arr\n pivot = arr[0]\n left = [x for x in arr[1:] if x <= pivot]\n right = [x for x in arr[1:] if x > pivot]\n return quicksort(left) + [pivot] + quicksort(right)",
264
+ "Quicksort implementation",
265
+ ],
266
+ ]
267
+
268
+ CSS = """
269
+ @import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Syne:wght@400;600;700;800&display=swap');
270
+
271
+ :root {
272
+ --bg: #0d0d0f;
273
+ --surface: #141418;
274
+ --surface2: #1c1c22;
275
+ --border: #2a2a35;
276
+ --accent: #7ee8a2;
277
+ --accent2: #4f8ef7;
278
+ --text: #e8e8f0;
279
+ --muted: #7070a0;
280
+ }
281
+
282
+ body, .gradio-container {
283
+ background: var(--bg) !important;
284
+ font-family: 'Syne', sans-serif !important;
285
+ color: var(--text) !important;
286
+ }
287
+ .gradio-container { max-width: 1300px !important; margin: 0 auto !important; }
288
+
289
+ .app-header {
290
+ text-align: center; padding: 44px 24px 28px;
291
+ border-bottom: 1px solid var(--border); margin-bottom: 28px; position: relative;
292
+ }
293
+ .app-header::before {
294
+ content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
295
+ background: linear-gradient(90deg, var(--accent), var(--accent2));
296
+ }
297
+ .app-title {
298
+ font-size: 2.6rem; font-weight: 800; letter-spacing: -0.04em; margin: 0 0 6px;
299
+ background: linear-gradient(135deg, var(--accent) 30%, var(--accent2));
300
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
301
+ }
302
+ .app-subtitle { color: var(--muted); font-size: 0.95rem; }
303
+ .badge {
304
+ display: inline-block; margin-top: 10px;
305
+ background: var(--surface2); border: 1px solid var(--border);
306
+ border-radius: 20px; padding: 4px 14px;
307
+ font-size: 0.75rem; color: var(--accent); letter-spacing: 0.08em;
308
+ font-family: 'DM Mono', monospace;
309
+ }
310
+
311
+ label, .label-wrap span {
312
+ font-family: 'Syne', sans-serif !important; font-size: 0.75rem !important;
313
+ font-weight: 600 !important; letter-spacing: 0.12em !important;
314
+ text-transform: uppercase !important; color: var(--muted) !important;
315
+ }
316
+ textarea, input[type="text"] {
317
+ background: var(--surface) !important; border: 1px solid var(--border) !important;
318
+ border-radius: 8px !important; color: var(--text) !important;
319
+ font-family: 'DM Mono', monospace !important; font-size: 0.875rem !important;
320
+ transition: border-color 0.2s !important;
321
+ }
322
+ textarea:focus, input:focus {
323
+ border-color: var(--accent) !important;
324
+ box-shadow: 0 0 0 3px rgba(126,232,162,0.1) !important;
325
+ }
326
+
327
+ button.primary {
328
+ background: var(--accent) !important; color: #0d0d0f !important;
329
+ font-family: 'Syne', sans-serif !important; font-weight: 700 !important;
330
+ font-size: 0.88rem !important; letter-spacing: 0.06em !important;
331
+ border: none !important; border-radius: 8px !important;
332
+ padding: 13px 26px !important; text-transform: uppercase !important;
333
+ transition: all 0.2s !important;
334
+ }
335
+ button.primary:hover {
336
+ filter: brightness(1.1) !important; transform: translateY(-1px) !important;
337
+ box-shadow: 0 8px 20px rgba(126,232,162,0.2) !important;
338
+ }
339
+ button.secondary {
340
+ background: transparent !important; border: 1px solid var(--border) !important;
341
+ color: var(--muted) !important; font-family: 'Syne', sans-serif !important;
342
+ font-weight: 600 !important; border-radius: 8px !important; transition: all 0.2s !important;
343
+ }
344
+ button.secondary:hover { border-color: var(--accent2) !important; color: var(--accent2) !important; }
345
+
346
+ .output-panel {
347
+ background: var(--surface) !important; border: 1px solid var(--border) !important;
348
+ border-radius: 12px !important; padding: 20px !important;
349
+ }
350
+ """
351
+
352
+ HEADER = """
353
+ <div class="app-header">
354
+ <div class="app-title">⬑ PyReview · Gemma</div>
355
+ <div class="app-subtitle">Local Gemma 2B-IT Β· No token limits Β· Runs fully offline</div>
356
+ <div class="badge">google/gemma-2b-it Β· 4-bit quantised Β· CPU</div>
357
+ </div>
358
+ """
359
+
360
+ with gr.Blocks(css=CSS, title="PyReview β€” Gemma 2B") as demo:
361
+ gr.HTML(HEADER)
362
+
363
+ with gr.Row(equal_height=False):
364
+ with gr.Column(scale=5):
365
+ code_input = gr.Code(
366
+ label="YOUR PYTHON CODE",
367
+ language="python",
368
+ lines=20,
369
+ placeholder="# Paste your Python code here…",
370
+ )
371
+ desc_input = gr.Textbox(
372
+ label="WHAT SHOULD THIS CODE DO?",
373
+ placeholder="e.g. 'Sort a list of dicts by a given key, handling missing keys'",
374
+ lines=3,
375
+ )
376
+ with gr.Row():
377
+ submit_btn = gr.Button("πŸ” Review Code", variant="primary", scale=3)
378
+ clear_btn = gr.Button("Clear", variant="secondary", scale=1)
379
+
380
+ gr.Markdown(
381
+ "> ⏱ **CPU note:** Gemma 2B takes ~1–3 min per review on free CPU. "
382
+ "Output streams token-by-token as it generates.",
383
+ elem_classes=["output-panel"],
384
+ )
385
+
386
+ with gr.Column(scale=6):
387
+ output = gr.Markdown(
388
+ value="*Paste code + description, then click **Review Code**.*",
389
+ label="REVIEW OUTPUT",
390
+ elem_classes=["output-panel"],
391
+ )
392
+
393
+ gr.Examples(
394
+ examples=EXAMPLES,
395
+ inputs=[code_input, desc_input],
396
+ label="EXAMPLE SNIPPETS β€” click any row to load",
397
+ )
398
+
399
+ submit_btn.click(
400
+ fn=review_pipeline,
401
+ inputs=[code_input, desc_input],
402
+ outputs=output,
403
+ show_progress=True,
404
+ )
405
+ clear_btn.click(
406
+ fn=lambda: ("", "", "*Paste code + description, then click **Review Code**.*"),
407
+ outputs=[code_input, desc_input, output],
408
+ )
409
+
410
+ if __name__ == "__main__":
411
+ demo.launch()