Krishp1 commited on
Commit
c3b692f
Β·
verified Β·
1 Parent(s): f799160

Update nodes.py

Browse files
Files changed (1) hide show
  1. nodes.py +101 -85
nodes.py CHANGED
@@ -6,6 +6,7 @@ import subprocess
6
  import re
7
  import hashlib
8
  import importlib.util
 
9
 
10
  from langchain_groq import ChatGroq
11
  from langchain_core.messages import HumanMessage, SystemMessage
@@ -97,74 +98,60 @@ Write complete working Python code only:
97
  # ─────────────────────────────────────────
98
  # NODE 3 β€” AST VALIDATOR
99
  # ─────────────────────────────────────────
100
- import ast
101
- import importlib.util
102
-
103
- from state import State
104
-
105
-
106
  def ast_validator(state: State):
107
- code = state["code"]
 
108
 
109
  try:
110
  tree = ast.parse(code)
111
  except SyntaxError as e:
112
- return {
113
- "ast_valid": False,
114
- "error": f"SyntaxError: {e}",
115
- "feedback": f"Fix syntax error: {e}"
116
- }
117
 
 
 
 
118
  hallucinated_imports = []
119
-
120
  for node in ast.walk(tree):
121
-
122
  if isinstance(node, ast.Import):
123
  for alias in node.names:
124
  base = alias.name.split(".")[0]
125
-
126
- if importlib.util.find_spec(base) is None:
 
 
 
127
  hallucinated_imports.append(base)
128
-
129
  elif isinstance(node, ast.ImportFrom):
130
  if node.module:
131
  base = node.module.split(".")[0]
132
-
133
- if importlib.util.find_spec(base) is None:
 
 
 
134
  hallucinated_imports.append(base)
135
 
136
- missing_hints = [
137
- fn.name
138
- for fn in ast.walk(tree)
139
- if isinstance(fn, ast.FunctionDef)
140
- and fn.returns is None
141
- ]
142
-
143
- feedback = []
144
-
145
  if hallucinated_imports:
146
- feedback.append(
147
- f"Unknown imports detected: {list(set(hallucinated_imports))}"
148
- )
149
-
150
- if missing_hints:
151
- feedback.append(
152
- f"Missing return type hints: {missing_hints}"
153
- )
154
-
155
- # FAIL validation if any issue exists
156
- if feedback:
157
  return {
158
  "ast_valid": False,
159
- "error": "\n".join(feedback),
160
- "feedback": "\n".join(feedback)
161
  }
162
 
163
- return {
164
- "ast_valid": True,
165
- "error": "",
166
- "feedback": ""
167
- }
 
 
 
 
 
 
168
 
169
  # ─────────────────────────────────────────
170
  # NODE 4 β€” TEST GENERATOR
@@ -175,28 +162,26 @@ def test_generator(state: State):
175
 
176
  response = llm.invoke([
177
  SystemMessage(content="""You are a Python testing expert.
178
- Return ONLY runnable Python test code β€” no markdown, no backticks.
179
- DO NOT use 'unittest', 'pytest', or 'sys'."""),
180
  HumanMessage(content=f"""
181
  Generate test cases for this code:
 
182
  TASK: {state['task']}
183
  CODE:
184
  {code}
185
 
186
  Rules:
187
- - Copy ALL function definitions inline.
188
- - Use ONLY simple 'assert' statements for validation.
189
- - Do NOT use 'unittest' or 'sys'.
190
- - If a test fails, let the script raise an AssertionError.
191
- - Print "All tests passed!" at the end if successful.
192
- - Wrap all test calls in a 'try...except' block to print the error before exiting.
193
 
194
  Return ONLY runnable Python code:
195
  """)
196
  ])
197
 
198
  tests = response.content
199
- # ... (keep existing regex cleaning)
200
  tests = re.sub(r"```python", "", tests)
201
  tests = re.sub(r"```", "", tests)
202
  tests = tests.strip()
@@ -241,10 +226,16 @@ def tester(state: State):
241
  except Exception as e:
242
  test_output = f"Test run error: {e}"
243
 
 
 
 
 
 
244
  return {
245
  "test_result": result.stdout + "\n" + test_output,
246
  "error": "",
247
  "passed": True,
 
248
  "fixed_code": ""
249
  }
250
  else:
@@ -316,31 +307,48 @@ Return ONLY complete runnable Python code:
316
  def performance_benchmarker(state: State):
317
  print("\n⚑ Benchmarking performance...")
318
  code = state["fixed_code"] if state["fixed_code"] else state["code"]
319
- clean_code = code.replace("'", "")
320
-
321
- benchmark_code = (
322
- code + "\n\n"
323
- "import timeit as _t, ast as _a\n"
324
- "_tree = _a.parse('''" + clean_code + "''')\n"
325
- "_fns = [n.name for n in _a.walk(_tree) "
326
- "if isinstance(n, _a.FunctionDef) and not n.name.startswith('_')]\n"
327
- "if _fns:\n"
328
- " _f = _fns[0]\n"
329
- " _ran = False\n"
330
- " for _call in [_f+'(100)', _f+'(\"hello\")', _f+'([1,2,3,4,5])', _f+'(\"racecar\")', _f+'(10)']:\n"
331
- " try:\n"
332
- " _ms = _t.timeit(_call, globals=globals(), number=1000)*1000\n"
333
- " print('BENCHMARK:'+str(round(_ms,2))+'ms')\n"
334
- " _ran = True\n"
335
- " break\n"
336
- " except: continue\n"
337
- " if not _ran: print('BENCHMARK:skipped')\n"
338
- "else: print('BENCHMARK:skipped')\n"
339
- )
340
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  result = subprocess.run(
343
- ["python", "-c", benchmark_code],
344
  capture_output=True, text=True, timeout=20
345
  )
346
  output = result.stdout + result.stderr
@@ -359,6 +367,9 @@ def performance_benchmarker(state: State):
359
  except Exception as e:
360
  print(f"⚠️ Benchmark error: {e}")
361
  return {"benchmark_ms": 0.0}
 
 
 
362
 
363
  # ─────────────────────────────────────────
364
  # NODE 8 β€” DEBUGGER
@@ -366,12 +377,16 @@ def performance_benchmarker(state: State):
366
  def debugger(state: State):
367
  print(f"\nπŸ”§ Debugger fixing (attempt {state['retries']+1})...")
368
 
 
 
 
 
369
  response = llm.invoke([
370
  SystemMessage(content="""You are a Python debugger.
371
  Fix the exact error. Return ONLY fixed code β€” no markdown, no backticks."""),
372
  HumanMessage(content=f"""
373
  CODE:
374
- {state['code']}
375
 
376
  ERROR:
377
  {state['error']}
@@ -402,7 +417,9 @@ Return complete fixed Python code only:
402
  # ─────────────────────────────────────────
403
  def security_auditor(state: State):
404
  print("\nπŸ”’ Security check...")
405
- code = state["final_code"] if state["final_code"] else state["code"]
 
 
406
 
407
  dangerous = [
408
  ("eval(", "Code execution via eval"),
@@ -432,7 +449,7 @@ def security_auditor(state: State):
432
  # ─────────────────────────────────────────
433
  def complexity_judge(state: State):
434
  print("\nπŸ“Š Complexity check...")
435
- code = state["final_code"] if state["final_code"] else state["code"]
436
  lines = code.split("\n")
437
  issues = []
438
 
@@ -471,7 +488,7 @@ def complexity_judge(state: State):
471
  # ─────────────────────────────────────────
472
  def self_reflection(state: State):
473
  print("\nπŸͺž Self Reflection...")
474
- code = state["final_code"] if state["final_code"] else state["code"]
475
 
476
  response = llm.invoke([
477
  SystemMessage(content="""You are a senior Python engineer.
@@ -510,7 +527,8 @@ NOTES: <one sentence>"""),
510
  "reflection_ok": False,
511
  "reflection_notes": f"Issues: {issues_text}. {notes}",
512
  "confidence_score": confidence,
513
- "error": f"Reflection failed ({confidence}/10): {issues_text}"
 
514
  }
515
 
516
  print(f"βœ… Reflection approved ({confidence}/10)")
@@ -563,8 +581,6 @@ EXPLANATION:
563
  "explanation": explanation,
564
  "review": "Polished and explained"
565
  }
566
-
567
-
568
  # ─────────────────────────────────────────
569
  # NODE 13 β€” EXPLAINER (passthrough)
570
  # ─────────────────────────────────────────
@@ -575,4 +591,4 @@ def explainer(state: State):
575
 
576
  # LangGraph requires a state update.
577
  # Re-writing the existing explanation satisfies this rule.
578
- return {"explanation": explanation}
 
6
  import re
7
  import hashlib
8
  import importlib.util
9
+ import tempfile
10
 
11
  from langchain_groq import ChatGroq
12
  from langchain_core.messages import HumanMessage, SystemMessage
 
98
  # ─────────────────────────────────────────
99
  # NODE 3 β€” AST VALIDATOR
100
  # ─────────────────────────────────────────
 
 
 
 
 
 
101
  def ast_validator(state: State):
102
+ print("\n🌳 AST Validator checking syntax...")
103
+ code = state["fixed_code"] if state["fixed_code"] else state["code"]
104
 
105
  try:
106
  tree = ast.parse(code)
107
  except SyntaxError as e:
108
+ print(f"❌ Syntax error: {e}")
109
+ return {"ast_valid": False, "error": f"SyntaxError at line {e.lineno}: {e.msg}"}
 
 
 
110
 
111
+ # Hard-fail on hallucinated imports: catching this here is cheaper than
112
+ # letting it slide through to the Tester, where it would just crash with
113
+ # ModuleNotFoundError anyway and burn a Tester retry instead of an AST one.
114
  hallucinated_imports = []
 
115
  for node in ast.walk(tree):
 
116
  if isinstance(node, ast.Import):
117
  for alias in node.names:
118
  base = alias.name.split(".")[0]
119
+ try:
120
+ found = importlib.util.find_spec(base) is not None
121
+ except (ImportError, ValueError, ModuleNotFoundError):
122
+ found = False
123
+ if not found:
124
  hallucinated_imports.append(base)
 
125
  elif isinstance(node, ast.ImportFrom):
126
  if node.module:
127
  base = node.module.split(".")[0]
128
+ try:
129
+ found = importlib.util.find_spec(base) is not None
130
+ except (ImportError, ValueError, ModuleNotFoundError):
131
+ found = False
132
+ if not found:
133
  hallucinated_imports.append(base)
134
 
 
 
 
 
 
 
 
 
 
135
  if hallucinated_imports:
136
+ unknown = list(set(hallucinated_imports))
137
+ print(f"❌ Hallucinated/unavailable imports: {unknown}")
 
 
 
 
 
 
 
 
 
138
  return {
139
  "ast_valid": False,
140
+ "error": f"Unknown or unavailable imports: {unknown}. "
141
+ f"Rewrite using only the Python standard library or already-imported packages."
142
  }
143
 
144
+ # Missing return type hints stay a soft warning, not a hard failure.
145
+ # Escalating this to a blocker would burn one of only 3 AST retries on a
146
+ # style nitpick, and if AST fails 3 times the pipeline ends early with
147
+ # NO code shown to the user at all β€” far worse than a missing "-> int".
148
+ missing = [n.name for n in ast.walk(tree)
149
+ if isinstance(n, ast.FunctionDef) and not n.returns and n.name != "__init__"]
150
+ if missing:
151
+ print(f"⚠️ Missing return hints: {missing}")
152
+
153
+ print("βœ… AST passed!")
154
+ return {"ast_valid": True}
155
 
156
  # ─────────────────────────────────────────
157
  # NODE 4 β€” TEST GENERATOR
 
162
 
163
  response = llm.invoke([
164
  SystemMessage(content="""You are a Python testing expert.
165
+ Return ONLY runnable Python test code β€” no markdown, no backticks."""),
 
166
  HumanMessage(content=f"""
167
  Generate test cases for this code:
168
+
169
  TASK: {state['task']}
170
  CODE:
171
  {code}
172
 
173
  Rules:
174
+ - Copy ALL function definitions inline β€” do NOT import from files
175
+ - Cover: normal cases, edge cases, large input
176
+ - Call each test function at the bottom to run them
177
+ - Do NOT use unittest or sys β€” just plain assert statements
178
+ - Print "All tests passed!" at the end if successful
 
179
 
180
  Return ONLY runnable Python code:
181
  """)
182
  ])
183
 
184
  tests = response.content
 
185
  tests = re.sub(r"```python", "", tests)
186
  tests = re.sub(r"```", "", tests)
187
  tests = tests.strip()
 
226
  except Exception as e:
227
  test_output = f"Test run error: {e}"
228
 
229
+ # Promote whichever version actually passed into "code" so every
230
+ # downstream node (hypothesis/benchmark/security/complexity/reviewer)
231
+ # keeps working on the tested version instead of silently
232
+ # falling back to the pre-fix original once fixed_code is cleared.
233
+ working_code = code
234
  return {
235
  "test_result": result.stdout + "\n" + test_output,
236
  "error": "",
237
  "passed": True,
238
+ "code": working_code,
239
  "fixed_code": ""
240
  }
241
  else:
 
307
  def performance_benchmarker(state: State):
308
  print("\n⚑ Benchmarking performance...")
309
  code = state["fixed_code"] if state["fixed_code"] else state["code"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
 
311
+ # Find the target function via ast in-process β€” no need to re-parse
312
+ # inside the subprocess, so no string-embedding/quote-stripping needed.
313
+ try:
314
+ tree = ast.parse(code)
315
+ fn_names = [n.name for n in ast.walk(tree)
316
+ if isinstance(n, ast.FunctionDef) and not n.name.startswith("_")]
317
+ except SyntaxError:
318
+ fn_names = []
319
+
320
+ if not fn_names:
321
+ print("⚑ No benchmarkable function found β€” skipped")
322
+ return {"benchmark_ms": 0.0}
323
+
324
+ fn_name = fn_names[0]
325
+ tmp_path = None
326
  try:
327
+ with tempfile.NamedTemporaryFile(
328
+ mode="w", suffix=".py", delete=False, encoding="utf-8"
329
+ ) as tmp:
330
+ tmp.write(code)
331
+ tmp.write("\n\n")
332
+ tmp.write(
333
+ "import timeit as _t\n"
334
+ f"_f = {fn_name}\n"
335
+ "_ran = False\n"
336
+ "for _call in [lambda: _f(100), lambda: _f('hello'), "
337
+ "lambda: _f([1,2,3,4,5]), lambda: _f('racecar'), lambda: _f(10)]:\n"
338
+ " try:\n"
339
+ " _ms = _t.timeit(_call, number=1000) * 1000\n"
340
+ " print('BENCHMARK:' + str(round(_ms, 2)) + 'ms')\n"
341
+ " _ran = True\n"
342
+ " break\n"
343
+ " except Exception:\n"
344
+ " continue\n"
345
+ "if not _ran:\n"
346
+ " print('BENCHMARK:skipped')\n"
347
+ )
348
+ tmp_path = tmp.name
349
+
350
  result = subprocess.run(
351
+ ["python", tmp_path],
352
  capture_output=True, text=True, timeout=20
353
  )
354
  output = result.stdout + result.stderr
 
367
  except Exception as e:
368
  print(f"⚠️ Benchmark error: {e}")
369
  return {"benchmark_ms": 0.0}
370
+ finally:
371
+ if tmp_path and os.path.exists(tmp_path):
372
+ os.remove(tmp_path)
373
 
374
  # ─────────────────────────────────────────
375
  # NODE 8 β€” DEBUGGER
 
377
  def debugger(state: State):
378
  print(f"\nπŸ”§ Debugger fixing (attempt {state['retries']+1})...")
379
 
380
+ # Fix on top of the most recent attempt (fixed_code), not the stale
381
+ # original β€” otherwise each retry throws away the previous retry's work.
382
+ base_code = state["fixed_code"] if state["fixed_code"] else state["code"]
383
+
384
  response = llm.invoke([
385
  SystemMessage(content="""You are a Python debugger.
386
  Fix the exact error. Return ONLY fixed code β€” no markdown, no backticks."""),
387
  HumanMessage(content=f"""
388
  CODE:
389
+ {base_code}
390
 
391
  ERROR:
392
  {state['error']}
 
417
  # ─────────────────────────────────────────
418
  def security_auditor(state: State):
419
  print("\nπŸ”’ Security check...")
420
+ # final_code isn't set until the reviewer node runs, which is *after*
421
+ # this node in the graph β€” referencing it here was always a no-op.
422
+ code = state["fixed_code"] if state["fixed_code"] else state["code"]
423
 
424
  dangerous = [
425
  ("eval(", "Code execution via eval"),
 
449
  # ─────────────────────────────────────────
450
  def complexity_judge(state: State):
451
  print("\nπŸ“Š Complexity check...")
452
+ code = state["fixed_code"] if state["fixed_code"] else state["code"]
453
  lines = code.split("\n")
454
  issues = []
455
 
 
488
  # ─────────────────────────────────────────
489
  def self_reflection(state: State):
490
  print("\nπŸͺž Self Reflection...")
491
+ code = state["fixed_code"] if state["fixed_code"] else state["code"]
492
 
493
  response = llm.invoke([
494
  SystemMessage(content="""You are a senior Python engineer.
 
527
  "reflection_ok": False,
528
  "reflection_notes": f"Issues: {issues_text}. {notes}",
529
  "confidence_score": confidence,
530
+ "error": f"Reflection failed ({confidence}/10): {issues_text}",
531
+ "reflection_retries": state.get("reflection_retries", 0) + 1
532
  }
533
 
534
  print(f"βœ… Reflection approved ({confidence}/10)")
 
581
  "explanation": explanation,
582
  "review": "Polished and explained"
583
  }
 
 
584
  # ─────────────────────────────────────────
585
  # NODE 13 β€” EXPLAINER (passthrough)
586
  # ─────────────────────────────────────────
 
591
 
592
  # LangGraph requires a state update.
593
  # Re-writing the existing explanation satisfies this rule.
594
+ return {"explanation": explanation}