Spaces:
Running
Running
Update nodes.py
Browse files
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 |
-
|
|
|
|
| 108 |
|
| 109 |
try:
|
| 110 |
tree = ast.parse(code)
|
| 111 |
except SyntaxError as e:
|
| 112 |
-
|
| 113 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 127 |
hallucinated_imports.append(base)
|
| 128 |
-
|
| 129 |
elif isinstance(node, ast.ImportFrom):
|
| 130 |
if node.module:
|
| 131 |
base = node.module.split(".")[0]
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 147 |
-
|
| 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": "
|
| 160 |
-
|
| 161 |
}
|
| 162 |
|
| 163 |
-
return
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 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 |
-
-
|
| 189 |
-
-
|
| 190 |
-
-
|
| 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",
|
| 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 |
-
{
|
| 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 |
-
|
|
|
|
|
|
|
| 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["
|
| 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["
|
| 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}
|