Spaces:
Sleeping
Sleeping
Commit ·
2cf9a43
1
Parent(s): a3d500f
fix: anti-loop logic and guided prompts for LLM agent
Browse files- baseline/run_baseline.py +116 -63
baseline/run_baseline.py
CHANGED
|
@@ -10,84 +10,84 @@ import os, time, json, re, requests
|
|
| 10 |
BASE_URL = os.environ.get("DATAENGENV_URL", "http://localhost:8000")
|
| 11 |
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
|
| 12 |
|
| 13 |
-
SYSTEM_PROMPT = """You are a senior Data Engineer
|
| 14 |
-
The pipeline has 4 stages. You must fix each stage by submitting a correct fix.
|
| 15 |
|
| 16 |
-
You will receive
|
| 17 |
-
- script_content: the current Python script
|
| 18 |
-
- last_run_error: any error from the last run
|
| 19 |
-
- last_run_output: stdout from the last run
|
| 20 |
-
- data_preview: dataset info (columns, nulls, stats)
|
| 21 |
-
- actor_feedback: feedback from the MLOps reviewer bot
|
| 22 |
|
| 23 |
-
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
5. Ask the MLOps reviewer for feedback:
|
| 38 |
-
{"action_type": "query_actor", "payload": {}}
|
| 39 |
-
|
| 40 |
-
6. Submit when you believe the fix is correct:
|
| 41 |
-
{"action_type": "submit", "payload": {}}
|
| 42 |
-
|
| 43 |
-
STRATEGY:
|
| 44 |
-
- Start by inspecting data OR running the script to see the error
|
| 45 |
-
- Read the error carefully and diagnose the root cause
|
| 46 |
-
- Use edit_script to fix the bug (the "old" text must EXACTLY match the script)
|
| 47 |
-
- Run the script again to verify your fix works
|
| 48 |
-
- Submit once the script runs without errors
|
| 49 |
-
|
| 50 |
-
IMPORTANT: Respond with ONLY a JSON object. No explanation, no markdown, no extra text."""
|
| 51 |
|
| 52 |
|
| 53 |
def parse_action(raw_text: str) -> dict:
|
| 54 |
"""Extract a JSON action from the LLM's response."""
|
| 55 |
-
|
| 56 |
-
match = re.search(r'\{[^{}]*\}', raw_text, re.DOTALL)
|
| 57 |
if match:
|
| 58 |
try:
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
| 60 |
if "action_type" in action:
|
|
|
|
|
|
|
| 61 |
return action
|
| 62 |
except json.JSONDecodeError:
|
| 63 |
pass
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
|
| 68 |
def format_observation(obs: dict, reward: dict = None) -> str:
|
| 69 |
-
"""Format
|
| 70 |
-
parts = []
|
| 71 |
-
parts.append(f"Current Stage: {obs.get('current_stage', '?')}")
|
| 72 |
-
parts.append(f"Step: {obs.get('stage_step_number', '?')}")
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
|
|
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
|
|
|
| 79 |
|
| 80 |
-
|
| 81 |
-
|
|
|
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
|
|
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
|
|
|
| 88 |
|
| 89 |
if reward:
|
| 90 |
-
parts.append(f"\nREWARD: {reward.get('score', 0)}
|
| 91 |
|
| 92 |
return "\n".join(parts)
|
| 93 |
|
|
@@ -102,10 +102,15 @@ def run_episode():
|
|
| 102 |
obs = reset_resp if "current_stage" in reset_resp else reset_resp.get("observation", reset_resp)
|
| 103 |
|
| 104 |
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
total_steps = 0
|
| 108 |
final_score = 0.0
|
|
|
|
|
|
|
| 109 |
|
| 110 |
print(f"\n{'='*60}")
|
| 111 |
print(f" PIPELINEOPS ARENA — LLM AGENT (Llama 3.1 8B)")
|
|
@@ -114,19 +119,58 @@ def run_episode():
|
|
| 114 |
for step in range(60):
|
| 115 |
# Ask the LLM what to do
|
| 116 |
try:
|
| 117 |
-
# Keep context manageable — system + last
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
response = client.chat.completions.create(
|
| 120 |
model="llama-3.1-8b-instant",
|
| 121 |
messages=trimmed,
|
| 122 |
-
max_tokens=
|
| 123 |
-
temperature=0.
|
| 124 |
)
|
| 125 |
raw = response.choices[0].message.content.strip()
|
| 126 |
action = parse_action(raw)
|
| 127 |
except Exception as e:
|
| 128 |
print(f" LLM error: {e}")
|
| 129 |
-
action = {"action_type": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
# Take the action
|
| 132 |
try:
|
|
@@ -135,15 +179,24 @@ def run_episode():
|
|
| 135 |
reward = result.get("reward", {})
|
| 136 |
total_steps += 1
|
| 137 |
|
| 138 |
-
# Print what happened
|
| 139 |
stage = obs.get("current_stage", "?")
|
| 140 |
score = reward.get("score", 0)
|
| 141 |
msg = reward.get("message", "")
|
| 142 |
print(f" Step {total_steps:02d} | Stage {stage} | {action['action_type']:<15} | reward={score:.2f} | {msg[:50]}")
|
| 143 |
|
| 144 |
-
#
|
| 145 |
messages.append({"role": "assistant", "content": json.dumps(action)})
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
if obs.get("done", False):
|
| 149 |
final_score = reward.get("score", 0.0)
|
|
@@ -173,7 +226,7 @@ def run_episode():
|
|
| 173 |
|
| 174 |
|
| 175 |
if __name__ == "__main__":
|
| 176 |
-
print("Running PipelineOps Arena — LLM Agent
|
| 177 |
results = run_episode()
|
| 178 |
|
| 179 |
with open("baseline/results.json", "w") as f:
|
|
|
|
| 10 |
BASE_URL = os.environ.get("DATAENGENV_URL", "http://localhost:8000")
|
| 11 |
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
|
| 12 |
|
| 13 |
+
SYSTEM_PROMPT = """You are a senior Data Engineer. A broken ML pipeline must be fixed.
|
|
|
|
| 14 |
|
| 15 |
+
You will receive the script, error logs, and data info. You must fix the bug.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
RESPOND WITH ONLY A JSON OBJECT. No explanation. No markdown.
|
| 18 |
|
| 19 |
+
Actions:
|
| 20 |
+
{"action_type": "inspect_data", "payload": {}}
|
| 21 |
+
{"action_type": "run_script", "payload": {}}
|
| 22 |
+
{"action_type": "edit_script", "payload": {"old": "exact text in script", "new": "replacement"}}
|
| 23 |
+
{"action_type": "query_actor", "payload": {}}
|
| 24 |
+
{"action_type": "submit", "payload": {}}
|
| 25 |
|
| 26 |
+
CRITICAL RULES:
|
| 27 |
+
- Use inspect_data ONLY ONCE per stage, then move on
|
| 28 |
+
- After inspecting, use edit_script to fix the bug
|
| 29 |
+
- The "old" field must contain text that EXACTLY exists in the script
|
| 30 |
+
- After editing, use run_script to verify
|
| 31 |
+
- After a clean run, use submit
|
| 32 |
+
- NEVER repeat the same action twice in a row
|
| 33 |
|
| 34 |
+
Common fixes:
|
| 35 |
+
- KeyError 'age_years': the column was renamed to 'age'. Replace 'age_years' with 'age'. Also add df.dropna(inplace=True)
|
| 36 |
+
- Training diverges / loss NaN: features are unscaled. Add StandardScaler before the classifier
|
| 37 |
+
- Suspiciously high accuracy (0.98+): data leakage. Move scaler.fit() to AFTER train_test_split()
|
| 38 |
+
- Fairness rejection: add class_weight='balanced' to the classifier"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
|
| 41 |
def parse_action(raw_text: str) -> dict:
|
| 42 |
"""Extract a JSON action from the LLM's response."""
|
| 43 |
+
match = re.search(r'\{[^{}]*("payload"\s*:\s*\{[^{}]*\})?[^{}]*\}', raw_text, re.DOTALL)
|
|
|
|
| 44 |
if match:
|
| 45 |
try:
|
| 46 |
+
# Clean up common LLM mistakes
|
| 47 |
+
cleaned = match.group()
|
| 48 |
+
cleaned = cleaned.replace("'", '"')
|
| 49 |
+
action = json.loads(cleaned)
|
| 50 |
if "action_type" in action:
|
| 51 |
+
if "payload" not in action:
|
| 52 |
+
action["payload"] = {}
|
| 53 |
return action
|
| 54 |
except json.JSONDecodeError:
|
| 55 |
pass
|
| 56 |
+
|
| 57 |
+
# Try simpler extraction
|
| 58 |
+
for atype in ["edit_script", "run_script", "submit", "inspect_data", "query_actor"]:
|
| 59 |
+
if atype in raw_text:
|
| 60 |
+
return {"action_type": atype, "payload": {}}
|
| 61 |
+
|
| 62 |
+
return {"action_type": "run_script", "payload": {}}
|
| 63 |
|
| 64 |
|
| 65 |
def format_observation(obs: dict, reward: dict = None) -> str:
|
| 66 |
+
"""Format observation concisely for the LLM."""
|
| 67 |
+
parts = [f"Stage {obs.get('current_stage', '?')} | Step {obs.get('stage_step_number', '?')}"]
|
|
|
|
|
|
|
| 68 |
|
| 69 |
+
error = obs.get("last_run_error")
|
| 70 |
+
if error:
|
| 71 |
+
parts.append(f"\nERROR:\n{str(error)[:400]}")
|
| 72 |
|
| 73 |
+
output = obs.get("last_run_output")
|
| 74 |
+
if output:
|
| 75 |
+
parts.append(f"\nOUTPUT:\n{str(output)[:300]}")
|
| 76 |
|
| 77 |
+
preview = obs.get("data_preview")
|
| 78 |
+
if preview:
|
| 79 |
+
parts.append(f"\nDATA:\n{str(preview)[:400]}")
|
| 80 |
|
| 81 |
+
feedback = obs.get("actor_feedback")
|
| 82 |
+
if feedback:
|
| 83 |
+
parts.append(f"\nREVIEWER:\n{str(feedback)[:300]}")
|
| 84 |
|
| 85 |
+
script = obs.get("script_content")
|
| 86 |
+
if script:
|
| 87 |
+
parts.append(f"\nSCRIPT:\n{script[:800]}")
|
| 88 |
|
| 89 |
if reward:
|
| 90 |
+
parts.append(f"\nREWARD: {reward.get('score', 0):.2f} | {reward.get('message', '')}")
|
| 91 |
|
| 92 |
return "\n".join(parts)
|
| 93 |
|
|
|
|
| 102 |
obs = reset_resp if "current_stage" in reset_resp else reset_resp.get("observation", reset_resp)
|
| 103 |
|
| 104 |
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
| 105 |
+
|
| 106 |
+
initial_prompt = format_observation(obs)
|
| 107 |
+
initial_prompt += "\n\nStart by inspecting the data, then fix the bug. What is your first action?"
|
| 108 |
+
messages.append({"role": "user", "content": initial_prompt})
|
| 109 |
|
| 110 |
total_steps = 0
|
| 111 |
final_score = 0.0
|
| 112 |
+
last_action_type = None
|
| 113 |
+
repeat_count = 0
|
| 114 |
|
| 115 |
print(f"\n{'='*60}")
|
| 116 |
print(f" PIPELINEOPS ARENA — LLM AGENT (Llama 3.1 8B)")
|
|
|
|
| 119 |
for step in range(60):
|
| 120 |
# Ask the LLM what to do
|
| 121 |
try:
|
| 122 |
+
# Keep context manageable — system + first + last 4 messages
|
| 123 |
+
if len(messages) > 8:
|
| 124 |
+
trimmed = [messages[0], messages[1]] + messages[-4:]
|
| 125 |
+
else:
|
| 126 |
+
trimmed = messages
|
| 127 |
+
|
| 128 |
response = client.chat.completions.create(
|
| 129 |
model="llama-3.1-8b-instant",
|
| 130 |
messages=trimmed,
|
| 131 |
+
max_tokens=400,
|
| 132 |
+
temperature=0.2,
|
| 133 |
)
|
| 134 |
raw = response.choices[0].message.content.strip()
|
| 135 |
action = parse_action(raw)
|
| 136 |
except Exception as e:
|
| 137 |
print(f" LLM error: {e}")
|
| 138 |
+
action = {"action_type": "run_script", "payload": {}}
|
| 139 |
+
|
| 140 |
+
# Anti-loop: if repeating same action, force progression
|
| 141 |
+
if action["action_type"] == last_action_type:
|
| 142 |
+
repeat_count += 1
|
| 143 |
+
if repeat_count >= 2:
|
| 144 |
+
if last_action_type == "inspect_data":
|
| 145 |
+
# Already inspected, try running the script
|
| 146 |
+
action = {"action_type": "run_script", "payload": {}}
|
| 147 |
+
elif last_action_type == "run_script":
|
| 148 |
+
# Already ran, need to edit
|
| 149 |
+
# Give LLM one more chance with explicit instruction
|
| 150 |
+
messages.append({"role": "user", "content":
|
| 151 |
+
"STOP repeating. You must use edit_script now to fix the bug. "
|
| 152 |
+
"Look at the error and the script. Find the exact text to replace. "
|
| 153 |
+
"Respond with: {\"action_type\": \"edit_script\", \"payload\": {\"old\": \"...\", \"new\": \"...\"}}"
|
| 154 |
+
})
|
| 155 |
+
try:
|
| 156 |
+
response = client.chat.completions.create(
|
| 157 |
+
model="llama-3.1-8b-instant",
|
| 158 |
+
messages=[messages[0]] + messages[-3:],
|
| 159 |
+
max_tokens=400,
|
| 160 |
+
temperature=0.3,
|
| 161 |
+
)
|
| 162 |
+
raw = response.choices[0].message.content.strip()
|
| 163 |
+
action = parse_action(raw)
|
| 164 |
+
except:
|
| 165 |
+
action = {"action_type": "submit", "payload": {}}
|
| 166 |
+
elif last_action_type == "edit_script":
|
| 167 |
+
action = {"action_type": "run_script", "payload": {}}
|
| 168 |
+
else:
|
| 169 |
+
action = {"action_type": "submit", "payload": {}}
|
| 170 |
+
repeat_count = 0
|
| 171 |
+
else:
|
| 172 |
+
repeat_count = 0
|
| 173 |
+
last_action_type = action["action_type"]
|
| 174 |
|
| 175 |
# Take the action
|
| 176 |
try:
|
|
|
|
| 179 |
reward = result.get("reward", {})
|
| 180 |
total_steps += 1
|
| 181 |
|
|
|
|
| 182 |
stage = obs.get("current_stage", "?")
|
| 183 |
score = reward.get("score", 0)
|
| 184 |
msg = reward.get("message", "")
|
| 185 |
print(f" Step {total_steps:02d} | Stage {stage} | {action['action_type']:<15} | reward={score:.2f} | {msg[:50]}")
|
| 186 |
|
| 187 |
+
# Build next prompt based on what just happened
|
| 188 |
messages.append({"role": "assistant", "content": json.dumps(action)})
|
| 189 |
+
|
| 190 |
+
next_prompt = format_observation(obs, reward)
|
| 191 |
+
if action["action_type"] == "inspect_data":
|
| 192 |
+
next_prompt += "\n\nYou have inspected the data. Now use edit_script to fix the bug. What exact text in the script needs to change?"
|
| 193 |
+
elif action["action_type"] == "run_script" and obs.get("last_run_error"):
|
| 194 |
+
next_prompt += "\n\nThe script has an error. Use edit_script to fix it. The 'old' field must match text exactly from the SCRIPT above."
|
| 195 |
+
elif action["action_type"] == "run_script" and not obs.get("last_run_error"):
|
| 196 |
+
next_prompt += "\n\nScript ran successfully. Use submit to submit your fix."
|
| 197 |
+
elif action["action_type"] == "edit_script":
|
| 198 |
+
next_prompt += "\n\nScript edited. Now use run_script to verify the fix works."
|
| 199 |
+
messages.append({"role": "user", "content": next_prompt})
|
| 200 |
|
| 201 |
if obs.get("done", False):
|
| 202 |
final_score = reward.get("score", 0.0)
|
|
|
|
| 226 |
|
| 227 |
|
| 228 |
if __name__ == "__main__":
|
| 229 |
+
print("Running PipelineOps Arena — LLM Agent...")
|
| 230 |
results = run_episode()
|
| 231 |
|
| 232 |
with open("baseline/results.json", "w") as f:
|