Amala06 commited on
Commit
91eb8de
·
verified ·
1 Parent(s): e12a510

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +47 -387
app.py CHANGED
@@ -1,411 +1,71 @@
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
-
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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import torch
3
+ from transformers import AutoTokenizer, AutoModelForCausalLM
 
 
 
 
 
4
 
5
+ # --------- Load Model ---------
6
+ MODEL_NAME = "google/gemma-2b-it" # instruction-tuned version
 
7
 
8
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
9
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  model = AutoModelForCausalLM.from_pretrained(
11
+ MODEL_NAME,
12
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
13
+ device_map="auto"
 
14
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ # --------- Core Function ---------
17
+ def evaluate_code(requirement, code):
18
+ prompt = f"""
19
+ You are a strict software evaluator.
20
 
21
+ Your task:
22
+ Check if the given code satisfies the requirement.
 
23
 
24
+ Rules:
25
+ - Answer ONLY "YES" or "NO"
26
+ - Do NOT explain
27
+ - Be strict and logical
 
 
 
 
 
28
 
29
+ Requirement:
30
+ {requirement}
31
 
32
+ Code:
33
+ {code}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
+ Answer:
36
+ """
 
 
 
 
 
 
37
 
38
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
 
 
 
 
39
 
40
+ outputs = model.generate(
41
+ **inputs,
42
+ max_new_tokens=5,
43
+ temperature=0.2, # low randomness for consistency
44
+ do_sample=False
 
 
 
45
  )
46
 
47
+ response = tokenizer.decode(outputs[0], skip_special_tokens=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
+ # Extract only YES/NO
50
+ if "YES" in response.upper():
51
+ return "✅ YES"
52
+ elif "NO" in response.upper():
53
+ return "❌ NO"
54
+ else:
55
+ return "⚠️ Unable to determine"
56
 
57
+ # --------- UI ---------
58
+ with gr.Blocks() as app:
59
+ gr.Markdown("## 🧠 Code Requirement Validator (Gemma 2)")
60
+ gr.Markdown("Enter a requirement and code. The model will check if the requirement is met.")
61
 
62
+ requirement = gr.Textbox(label="Requirement", lines=4, placeholder="e.g. Function should return sum of two numbers")
63
+ code = gr.Textbox(label="Code", lines=10, placeholder="Paste your code here...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ output = gr.Textbox(label="Result")
 
66
 
67
+ btn = gr.Button("Evaluate")
 
 
 
 
 
 
 
 
 
68
 
69
+ btn.click(fn=evaluate_code, inputs=[requirement, code], outputs=output)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
+ app.launch()