Amala06 commited on
Commit
3080081
·
verified ·
1 Parent(s): 2a37072

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +403 -0
app.py ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import ast
4
+ import sys
5
+ import json
6
+ import time
7
+ from collections import deque
8
+
9
+ import gradio as gr
10
+ from transformers import pipeline
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Model Initialization — GPT-2 via HuggingFace Transformers
14
+ # ---------------------------------------------------------------------------
15
+ print("Loading GPT-2 model… this may take a moment on first run.")
16
+ generator = pipeline("text-generation", model="gpt2", max_new_tokens=400)
17
+ print("Model loaded.")
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # MODULE 2 — Validation & Flow Management
21
+ # ---------------------------------------------------------------------------
22
+ MIN_DESCRIPTION_CHARS = 20
23
+ MAX_DESCRIPTION_CHARS = 3000
24
+ MIN_CODE_CHARS = 10
25
+ MAX_CODE_CHARS = 8000
26
+ MAX_CODE_LINES = 300
27
+
28
+ FORBIDDEN_PATTERNS = [
29
+ r"ignore (all |previous |above )?instructions",
30
+ r"disregard (all |previous |above )?instructions",
31
+ r"you are now",
32
+ r"act as (a |an )?",
33
+ r"<\s*(script|iframe|object|embed)",
34
+ r"system\s*prompt",
35
+ r"jailbreak",
36
+ ]
37
+
38
+ PYTHON_KEYWORDS = {
39
+ "def", "class", "import", "from", "return", "if", "else", "elif",
40
+ "for", "while", "try", "except", "with", "lambda", "yield", "pass",
41
+ "raise", "assert", "in", "not", "and", "or", "True", "False", "None",
42
+ "print", "len", "range", "self",
43
+ }
44
+
45
+ class RateLimiter:
46
+ def __init__(self, max_calls: int = 5, window_seconds: int = 60):
47
+ self.max_calls = max_calls
48
+ self.window_seconds = window_seconds
49
+ self._timestamps: deque = deque()
50
+
51
+ def is_allowed(self) -> tuple[bool, str]:
52
+ now = time.time()
53
+ while self._timestamps and now - self._timestamps[0] > self.window_seconds:
54
+ self._timestamps.popleft()
55
+ if len(self._timestamps) >= self.max_calls:
56
+ wait = int(self.window_seconds - (now - self._timestamps[0])) + 1
57
+ return False, (
58
+ f"⏳ Rate limit reached — {self.max_calls} requests in "
59
+ f"{self.window_seconds}s. Please wait ~{wait}s and try again."
60
+ )
61
+ self._timestamps.append(now)
62
+ return True, ""
63
+
64
+ _rate_limiter = RateLimiter(max_calls=5, window_seconds=60)
65
+
66
+ def _check_forbidden(text: str) -> str | None:
67
+ lower = text.lower()
68
+ for pattern in FORBIDDEN_PATTERNS:
69
+ if re.search(pattern, lower):
70
+ return (
71
+ "Input contains disallowed content. "
72
+ "Please remove prompt-injection or HTML patterns and try again."
73
+ )
74
+ return None
75
+
76
+ def _looks_like_python(code: str) -> tuple[bool, str]:
77
+ tokens = set(re.findall(r"[A-Za-z_]\w*", code))
78
+ if not tokens.intersection(PYTHON_KEYWORDS):
79
+ return False, (
80
+ "🐍 The code doesn't appear to be Python — no recognisable Python "
81
+ "keywords found (e.g. def, class, import, return). "
82
+ "Please submit Python code only."
83
+ )
84
+ try:
85
+ ast.parse(code)
86
+ except SyntaxError as exc:
87
+ line_hint = f" (line {exc.lineno})" if exc.lineno else ""
88
+ return False, (
89
+ f"🐍 Python syntax error{line_hint}: {exc.msg}. "
90
+ "Please fix the syntax error before evaluating."
91
+ )
92
+ return True, ""
93
+
94
+ def validate_inputs(description: str, code: str) -> list[str]:
95
+ errors: list[str] = []
96
+
97
+ if not description or not description.strip():
98
+ errors.append("📋 Requirements description is required.")
99
+ if not code or not code.strip():
100
+ errors.append("🐍 Python code is required.")
101
+ if errors:
102
+ return errors
103
+
104
+ desc, code_ = description.strip(), code.strip()
105
+
106
+ if len(desc) < MIN_DESCRIPTION_CHARS:
107
+ errors.append(f"📋 Description too short ({len(desc)} chars) — minimum is {MIN_DESCRIPTION_CHARS} characters.")
108
+ if len(code_) < MIN_CODE_CHARS:
109
+ errors.append(f"🐍 Code too short ({len(code_)} chars) — minimum is {MIN_CODE_CHARS} characters.")
110
+ if len(desc) > MAX_DESCRIPTION_CHARS:
111
+ errors.append(f"📋 Description too long ({len(desc):,} chars) — max is {MAX_DESCRIPTION_CHARS:,} characters.")
112
+ if len(code_) > MAX_CODE_CHARS:
113
+ errors.append(f"🐍 Code too long ({len(code_):,} chars) — max is {MAX_CODE_CHARS:,} characters.")
114
+ if len(code_.splitlines()) > MAX_CODE_LINES:
115
+ errors.append(f"🐍 Code has too many lines ({len(code_.splitlines())}) — max is {MAX_CODE_LINES} lines.")
116
+
117
+ if err := _check_forbidden(desc):
118
+ errors.append(f"📋 {err}")
119
+ if err := _check_forbidden(code_):
120
+ errors.append(f"🐍 {err}")
121
+
122
+ if not errors:
123
+ is_python, py_error = _looks_like_python(code_)
124
+ if not is_python:
125
+ errors.append(py_error)
126
+
127
+ if not errors:
128
+ allowed, rate_msg = _rate_limiter.is_allowed()
129
+ if not allowed:
130
+ errors.append(rate_msg)
131
+
132
+ return errors
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # MODULE 3 — Prompt Builder
136
+ # ---------------------------------------------------------------------------
137
+ def build_prompt(description: str, code: str) -> str:
138
+ return f"""You are a Python code reviewer. Evaluate whether the code meets the requirements.
139
+
140
+ Requirements:
141
+ {description}
142
+
143
+ Code:
144
+ ```python
145
+ {code}
146
+ ```
147
+
148
+ Reply ONLY with a valid JSON object (no extra text) using exactly these keys:
149
+ - "result": "Pass" or "Fail"
150
+ - "accuracy": integer 0-100
151
+ - "summary": one sentence explanation
152
+ - "issues": numbered list of issues as a single string, e.g. "1. Warning - missing docstring\\n2. Error - off-by-one"
153
+
154
+ JSON:
155
+ """
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # MODULE 4 — Generation & JSON Parsing
159
+ # ---------------------------------------------------------------------------
160
+ def _extract_json(text: str) -> dict:
161
+ """Try to extract a JSON object from the raw model output."""
162
+ # Find the first { ... } block
163
+ match = re.search(r'\{.*?\}', text, re.DOTALL)
164
+ if match:
165
+ try:
166
+ return json.loads(match.group())
167
+ except json.JSONDecodeError:
168
+ pass
169
+
170
+ # Fallback: attempt to parse keys manually
171
+ result = "Pass" if re.search(r'"result"\s*:\s*"Pass"', text, re.I) else "Fail"
172
+ acc_m = re.search(r'"accuracy"\s*:\s*(\d+)', text)
173
+ accuracy = int(acc_m.group(1)) if acc_m else 50
174
+ sum_m = re.search(r'"summary"\s*:\s*"([^"]+)"', text)
175
+ summary = sum_m.group(1) if sum_m else "Unable to parse summary from model output."
176
+ iss_m = re.search(r'"issues"\s*:\s*"([^"]*)"', text, re.DOTALL)
177
+ issues = iss_m.group(1).replace("\\n", "\n") if iss_m else "No issues extracted."
178
+
179
+ return {"result": result, "accuracy": accuracy, "summary": summary, "issues": issues}
180
+
181
+ def generate_response(prompt: str) -> dict:
182
+ outputs = generator(prompt, do_sample=False, temperature=1.0)
183
+ raw_text = outputs[0]["generated_text"]
184
+ # Only look at the text appended after the prompt
185
+ new_text = raw_text[len(prompt):]
186
+ return _extract_json(new_text)
187
+
188
+ # ---------------------------------------------------------------------------
189
+ # Core orchestration
190
+ # ---------------------------------------------------------------------------
191
+ def format_for_display(raw: dict) -> tuple[str, str, str]:
192
+ emoji = "✅" if raw["result"].upper() == "PASS" else "❌"
193
+ verdict = f"{emoji} {raw['result']}"
194
+ acc_str = f"{raw['accuracy']}%" if raw.get("accuracy", -1) >= 0 else "N/A"
195
+ metrics = f"Accuracy: {acc_str}\n\nSummary: {raw['summary']}"
196
+ return verdict, metrics, raw.get("issues", "")
197
+
198
+ def validate_and_evaluate(description: str, code: str):
199
+ errors = validate_inputs(description, code)
200
+ if errors:
201
+ return "", "", "", "\n".join(f"{i+1}. {e}" for i, e in enumerate(errors))
202
+ prompt = build_prompt(description, code)
203
+ try:
204
+ raw = generate_response(prompt)
205
+ except Exception as exc:
206
+ return "", "", "", f"❌ Model error: {exc}"
207
+ verdict, metrics, issues = format_for_display(raw)
208
+ return verdict, metrics, issues, ""
209
+
210
+ # ---------------------------------------------------------------------------
211
+ # MODULE 1 — UI
212
+ # ---------------------------------------------------------------------------
213
+ CUSTOM_CSS = """
214
+ @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;1,400&display=swap');
215
+ *, *::before, *::after { box-sizing: border-box; }
216
+ body, .gradio-container {
217
+ font-family: 'DM Sans', sans-serif !important;
218
+ background: #0D0F14 !important;
219
+ color: #E8EAF0 !important;
220
+ }
221
+ .gradio-container {
222
+ max-width: 1140px !important;
223
+ margin: 0 auto !important;
224
+ padding: 32px 24px !important;
225
+ }
226
+ .eval-header {
227
+ display: flex; align-items: center; gap: 16px;
228
+ padding: 0 0 28px; border-bottom: 1px solid #2E3140; margin-bottom: 28px;
229
+ }
230
+ .eval-header .logo {
231
+ width: 44px; height: 44px; border-radius: 10px;
232
+ background: linear-gradient(135deg, #534AB7 0%, #1D9E75 100%);
233
+ display: flex; align-items: center; justify-content: center;
234
+ flex-shrink: 0; font-size: 22px; line-height: 1;
235
+ }
236
+ .eval-header h1 {
237
+ font-size: 20px !important; font-weight: 600 !important;
238
+ letter-spacing: -0.3px !important; color: #E8EAF0 !important; margin: 0 !important;
239
+ }
240
+ .eval-header p { font-size: 13px !important; color: #9DA0B0 !important; margin: 2px 0 0 !important; }
241
+ .eval-header .model-badge {
242
+ margin-left: auto; font-family: 'Space Mono', monospace; font-size: 10px;
243
+ background: #1E2028; border: 1px solid #3A3E52; color: #6A6E80;
244
+ padding: 4px 10px; border-radius: 4px; letter-spacing: 1.5px; white-space: nowrap;
245
+ }
246
+ .gradio-textbox textarea, .gradio-code textarea, .gradio-textbox input {
247
+ background: #161820 !important; border: 1px solid #2E3140 !important;
248
+ border-radius: 10px !important; color: #E8EAF0 !important;
249
+ font-family: 'DM Sans', sans-serif !important; font-size: 13.5px !important;
250
+ line-height: 1.7 !important; padding: 14px 16px !important;
251
+ transition: border-color 0.15s !important; resize: vertical !important;
252
+ }
253
+ .gradio-textbox textarea:focus, .gradio-code textarea:focus {
254
+ border-color: #534AB7 !important; outline: none !important;
255
+ box-shadow: 0 0 0 3px rgba(83, 74, 183, 0.15) !important;
256
+ }
257
+ .gradio-textbox textarea::placeholder { color: #4A4E60 !important; }
258
+ #code-input { min-height: 300px; }
259
+ #code-input .cm-editor { min-height: 300px; }
260
+ .gradio-textbox label span, .gradio-code label span {
261
+ font-family: 'DM Sans', sans-serif !important; font-size: 13px !important;
262
+ font-weight: 700 !important; color: #E8EAF0 !important;
263
+ text-transform: uppercase !important; letter-spacing: 0.8px !important;
264
+ }
265
+ #eval-btn {
266
+ background: #534AB7 !important; border: none !important; color: #fff !important;
267
+ font-family: 'DM Sans', sans-serif !important; font-size: 14px !important;
268
+ font-weight: 500 !important; padding: 12px 32px !important; border-radius: 8px !important;
269
+ cursor: pointer !important; transition: background 0.15s, transform 0.1s !important;
270
+ }
271
+ #eval-btn:hover { background: #7F77DD !important; transform: translateY(-1px) !important; }
272
+ #clear-btn {
273
+ background: transparent !important; border: 1px solid #3A3E52 !important;
274
+ color: #9DA0B0 !important; font-family: 'DM Sans', sans-serif !important;
275
+ font-size: 13px !important; padding: 12px 20px !important; border-radius: 8px !important;
276
+ cursor: pointer !important;
277
+ }
278
+ #clear-btn:hover { border-color: #7F77DD !important; color: #E8EAF0 !important; }
279
+ .results-heading {
280
+ font-size: 11px !important; font-weight: 500 !important; color: #6A6E80 !important;
281
+ text-transform: uppercase !important; letter-spacing: 1px !important;
282
+ padding: 0 0 16px !important; border-bottom: 1px solid #2E3140 !important; margin-bottom: 20px !important;
283
+ }
284
+ #verdict-out textarea, #accuracy-out textarea {
285
+ background: #161820 !important; border: 1px solid #2E3140 !important;
286
+ border-radius: 10px !important; font-family: 'Space Mono', monospace !important;
287
+ font-size: 26px !important; font-weight: 700 !important; text-align: center !important;
288
+ padding: 20px !important; color: #E8EAF0 !important; cursor: default !important;
289
+ }
290
+ #summary-out textarea, #issues-out textarea {
291
+ background: #161820 !important; border: 1px solid #2E3140 !important;
292
+ border-radius: 10px !important; font-family: 'DM Sans', sans-serif !important;
293
+ font-size: 13.5px !important; line-height: 1.7 !important; padding: 16px !important;
294
+ color: #C0C3D0 !important; cursor: default !important;
295
+ }
296
+ #issues-out textarea { min-height: 120px !important; line-height: 1.8 !important; }
297
+ #error-out textarea {
298
+ background: #1A0E0E !important; border: 1px solid #5a2020 !important;
299
+ border-radius: 10px !important; font-family: 'DM Sans', sans-serif !important;
300
+ font-size: 13.5px !important; padding: 14px 16px !important;
301
+ color: #F0997B !important; cursor: default !important;
302
+ }
303
+ .divider { height: 1px; background: #2E3140; margin: 20px 0; }
304
+ footer { display: none !important; }
305
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
306
+ ::-webkit-scrollbar-track { background: transparent; }
307
+ ::-webkit-scrollbar-thumb { background: #3A3E52; border-radius: 3px; }
308
+ ::-webkit-scrollbar-thumb:hover { background: #534AB7; }
309
+ """
310
+
311
+ HEADER_HTML = """
312
+ <div class="eval-header">
313
+ <div class="logo">🔍</div>
314
+ <div>
315
+ <h1>Python Code Evaluator</h1>
316
+ <p>Powered by GPT-2 (HuggingFace)</p>
317
+ </div>
318
+ <div class="model-badge">GPT-2 · LOCAL</div>
319
+ </div>
320
+ """
321
+
322
+ RESULTS_HEADING_HTML = """
323
+ <div class="divider"></div>
324
+ <div class="results-heading">⬡ &nbsp; Evaluation Results</div>
325
+ """
326
+
327
+ INPUT_HINT_HTML = """
328
+ <div style="font-size:12px;color:#4A4E60;margin-top:-4px;padding-bottom:4px;">
329
+ Tip — paste your description and code above, then click <strong style="color:#7F77DD">Evaluate</strong>.
330
+ Note: GPT-2 is a small model; results are best-effort and may need interpretation.
331
+ </div>
332
+ """
333
+
334
+ def _split_metrics(metrics_str: str):
335
+ acc, summary = "", ""
336
+ if not metrics_str:
337
+ return acc, summary
338
+ for line in metrics_str.splitlines():
339
+ if line.startswith("Accuracy:"):
340
+ acc = line.replace("Accuracy:", "").strip()
341
+ elif line.startswith("Summary:"):
342
+ summary = line.replace("Summary:", "").strip()
343
+ return acc, summary
344
+
345
+ def _ui_evaluate(description: str, code: str):
346
+ verdict, metrics, issues, error = validate_and_evaluate(description, code)
347
+ accuracy, summary = _split_metrics(metrics)
348
+ if "PASS" in verdict.upper():
349
+ verdict_display = "✅ PASS"
350
+ elif "FAIL" in verdict.upper():
351
+ verdict_display = "❌ FAIL"
352
+ else:
353
+ verdict_display = verdict or ""
354
+ return verdict_display, accuracy, summary, issues, error
355
+
356
+ with gr.Blocks(title="Python Code Evaluator", css=CUSTOM_CSS) as app:
357
+
358
+ gr.HTML(HEADER_HTML)
359
+
360
+ with gr.Row(equal_height=True):
361
+ description_input = gr.Textbox(
362
+ label="Requirements Description",
363
+ placeholder=(
364
+ "Describe what the Python code is supposed to do…\n\n"
365
+ "Example: Write a function that accepts a list of integers "
366
+ "and returns the sum of all even numbers."
367
+ ),
368
+ lines=10, max_lines=20, elem_id="desc-input",
369
+ )
370
+ code_input = gr.Code(
371
+ label="Python Code", language="python",
372
+ lines=10, max_lines=30, elem_id="code-input",
373
+ )
374
+
375
+ gr.HTML(INPUT_HINT_HTML)
376
+
377
+ with gr.Row():
378
+ eval_btn = gr.Button("Evaluate", variant="primary", elem_id="eval-btn", scale=0)
379
+ clear_btn = gr.Button("Clear", variant="secondary", elem_id="clear-btn", scale=0)
380
+
381
+ gr.HTML(RESULTS_HEADING_HTML)
382
+
383
+ with gr.Row(equal_height=True):
384
+ verdict_out = gr.Textbox(label="Verdict", interactive=False, elem_id="verdict-out", scale=1)
385
+ accuracy_out = gr.Textbox(label="Accuracy", interactive=False, elem_id="accuracy-out", scale=1)
386
+ summary_out = gr.Textbox(label="Summary", interactive=False, lines=3, elem_id="summary-out", scale=2)
387
+
388
+ issues_out = gr.Textbox(label="Issues Detected", interactive=False, lines=5, elem_id="issues-out")
389
+ error_out = gr.Textbox(label="Status / Errors", interactive=False, visible=True, elem_id="error-out")
390
+
391
+ outputs = [verdict_out, accuracy_out, summary_out, issues_out, error_out]
392
+ eval_btn.click(fn=_ui_evaluate, inputs=[description_input, code_input], outputs=outputs)
393
+
394
+ def _clear():
395
+ return "", "", "", "", "", ""
396
+
397
+ clear_btn.click(
398
+ fn=_clear, inputs=[],
399
+ outputs=[description_input, code_input, verdict_out, accuracy_out, summary_out, issues_out],
400
+ )
401
+
402
+ if __name__ == "__main__":
403
+ app.launch()