Spaces:
Sleeping
Sleeping
file fixing
Browse files- server/__init__.py → .gitignore +0 -0
- __pycache__/server.cpython-313.pyc +0 -0
- agents/__pycache__/__init__.cpython-313.pyc +0 -0
- agents/__pycache__/prompt_builder.cpython-313.pyc +0 -0
- agents/__pycache__/senior_reviewer.cpython-313.pyc +0 -0
- agents/prompt_builder.py +74 -99
- agents/senior_reviewer.py +125 -0
- environment/__pycache__/__init__.cpython-313.pyc +0 -0
- environment/__pycache__/env.cpython-313.pyc +0 -0
- environment/__pycache__/rsm_simulator.cpython-313.pyc +0 -0
- environment/reviewer.py +0 -111
- server/server.py → server.py +0 -0
server/__init__.py → .gitignore
RENAMED
|
File without changes
|
__pycache__/server.cpython-313.pyc
ADDED
|
Binary file (5.5 kB). View file
|
|
|
agents/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (161 Bytes). View file
|
|
|
agents/__pycache__/prompt_builder.cpython-313.pyc
ADDED
|
Binary file (7.98 kB). View file
|
|
|
agents/__pycache__/senior_reviewer.cpython-313.pyc
ADDED
|
Binary file (5.22 kB). View file
|
|
|
agents/prompt_builder.py
CHANGED
|
@@ -1,50 +1,24 @@
|
|
| 1 |
"""
|
| 2 |
-
prompt_builder.py — Builds the structured text prompt for the LLM agent.
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
"""
|
| 7 |
import re
|
| 8 |
-
from
|
| 9 |
-
from environment.models import FabObservation, FabAction
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
SYSTEM_PROMPT = """You are an expert semiconductor process integration engineer.
|
| 13 |
-
Your goal is to optimize wafer yield by running experiments and identifying
|
| 14 |
-
the root cause of yield loss.
|
| 15 |
-
|
| 16 |
-
You have a limited experiment budget. Think like a scientist:
|
| 17 |
-
- In early steps (exploration): vary parameters broadly to map the response surface
|
| 18 |
-
- In middle steps (hypothesis): focus on your suspected bottleneck parameter
|
| 19 |
-
- In late steps (exploitation): converge on the optimum
|
| 20 |
-
- On step 12 or submit: propose your final process recipe
|
| 21 |
-
|
| 22 |
-
ALWAYS output your response in EXACTLY this XML format:
|
| 23 |
-
<experiment>
|
| 24 |
-
temp: [value]
|
| 25 |
-
etch_time: [value]
|
| 26 |
-
pressure: [value]
|
| 27 |
-
dopant: [value]
|
| 28 |
-
spin_speed: [value]
|
| 29 |
-
</experiment>
|
| 30 |
-
<diagnosis>
|
| 31 |
-
primary_bottleneck: [parameter name, exactly as listed in active params]
|
| 32 |
-
reasoning: [one sentence explaining your causal reasoning]
|
| 33 |
-
submit: [true/false — true only when ready to submit final recipe]
|
| 34 |
-
</diagnosis>"""
|
| 35 |
|
| 36 |
|
| 37 |
def build_prompt(obs: FabObservation) -> str:
|
| 38 |
"""
|
| 39 |
-
|
| 40 |
-
This is what gets passed to the LLM at each step.
|
| 41 |
"""
|
| 42 |
lines = []
|
| 43 |
|
| 44 |
# ── Header ────────────────────────────────────────────────────────────────
|
| 45 |
-
lines.append(
|
| 46 |
-
lines.append(f"- Step: {obs.step
|
| 47 |
-
lines.append(f"- Phase: {obs.phase.upper()}")
|
| 48 |
lines.append(f"- Budget remaining: {obs.budget_remaining} experiments")
|
| 49 |
lines.append(f"- Current best yield: {obs.current_best_yield:.1f}%")
|
| 50 |
lines.append(f"- Target yield: >{obs.target_yield:.0f}%")
|
|
@@ -54,78 +28,68 @@ def build_prompt(obs: FabObservation) -> str:
|
|
| 54 |
lines.append("ACTIVE PARAMETERS (with allowed ranges):")
|
| 55 |
for p in obs.active_params:
|
| 56 |
lo, hi = obs.param_ranges[p]
|
| 57 |
-
lines.append(f" {p}: [{lo}
|
| 58 |
lines.append("")
|
| 59 |
|
| 60 |
# ── Experiment history ────────────────────────────────────────────────────
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
| 63 |
for r in obs.experiment_history:
|
| 64 |
-
param_str = ", ".join(
|
| 65 |
-
|
| 66 |
-
|
| 67 |
lines.append(
|
| 68 |
-
f" Exp {r.step}: {param_str}
|
| 69 |
-
f"Yield: {r.yield_pct}%
|
| 70 |
)
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
# ── Agent's running hypothesis ────────────────────────────────────────────
|
| 74 |
-
if obs.current_hypothesis:
|
| 75 |
-
lines.append(f"YOUR CURRENT HYPOTHESIS: {obs.current_hypothesis}")
|
| 76 |
-
lines.append("")
|
| 77 |
|
| 78 |
# ── Reviewer feedback (if any) ────────────────────────────────────────────
|
| 79 |
if obs.reviewer_feedback:
|
| 80 |
-
lines.append(f"⚠
|
| 81 |
lines.append("")
|
| 82 |
|
| 83 |
-
# ── Phase
|
| 84 |
phase_instructions = {
|
| 85 |
"exploration": (
|
| 86 |
"PHASE: EXPLORATION — Vary parameters broadly. "
|
| 87 |
-
"
|
| 88 |
"Try experiments that differ significantly from each other."
|
| 89 |
),
|
| 90 |
"hypothesis": (
|
| 91 |
-
"PHASE: HYPOTHESIS — You have data.
|
| 92 |
"Isolate your suspected primary bottleneck by changing it while "
|
| 93 |
"holding others near their best-so-far values."
|
| 94 |
),
|
| 95 |
"exploitation": (
|
| 96 |
"PHASE: EXPLOITATION — Converge on the optimum. "
|
| 97 |
-
"Make fine adjustments around your best result
|
| 98 |
-
"
|
| 99 |
),
|
| 100 |
"submission": (
|
| 101 |
-
"PHASE: SUBMISSION —
|
| 102 |
-
"
|
| 103 |
-
"Set submit: true in your diagnosis. "
|
| 104 |
-
"The Senior Engineer will review your recipe against "
|
| 105 |
"production qualification constraints."
|
| 106 |
),
|
| 107 |
}
|
| 108 |
lines.append(phase_instructions.get(obs.phase, ""))
|
| 109 |
lines.append("")
|
| 110 |
|
| 111 |
-
# ──
|
| 112 |
-
lines.append("YOUR ACTION:")
|
| 113 |
-
lines.append(
|
| 114 |
-
"Output ONLY the XML tags below. No preamble, no explanation outside the tags."
|
| 115 |
-
)
|
| 116 |
-
|
| 117 |
-
# Build XML template with current best as defaults
|
| 118 |
lines.append("")
|
| 119 |
lines.append("<experiment>")
|
| 120 |
for p in obs.active_params:
|
| 121 |
lo, hi = obs.param_ranges[p]
|
| 122 |
default = (lo + hi) / 2
|
| 123 |
-
lines.append(f" {p}: {default
|
|
|
|
| 124 |
lines.append("</experiment>")
|
| 125 |
lines.append("<diagnosis>")
|
| 126 |
lines.append(f" primary_bottleneck: {obs.active_params[0]}")
|
| 127 |
-
lines.append(" reasoning: [
|
| 128 |
-
lines.append(" submit: false")
|
| 129 |
lines.append("</diagnosis>")
|
| 130 |
|
| 131 |
return "\n".join(lines)
|
|
@@ -133,48 +97,59 @@ def build_prompt(obs: FabObservation) -> str:
|
|
| 133 |
|
| 134 |
def parse_action(text: str, active_params: list) -> FabAction:
|
| 135 |
"""
|
| 136 |
-
Parse
|
| 137 |
-
Robust to
|
| 138 |
"""
|
| 139 |
params = {}
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
-
#
|
| 142 |
-
exp_match = re.search(r"<experiment>(.*?)</experiment>", text, re.DOTALL)
|
| 143 |
if exp_match:
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
try:
|
| 150 |
-
params[
|
| 151 |
except ValueError:
|
| 152 |
pass
|
| 153 |
|
| 154 |
-
#
|
| 155 |
-
|
| 156 |
-
reasoning = ""
|
| 157 |
-
submit = False
|
| 158 |
-
|
| 159 |
-
diag_match = re.search(r"<diagnosis>(.*?)</diagnosis>", text, re.DOTALL)
|
| 160 |
if diag_match:
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
s_match = re.search(r"submit\s*:\s*(true|false)", diag_block, re.IGNORECASE)
|
| 172 |
-
if s_match:
|
| 173 |
-
submit = s_match.group(1).lower() == "true"
|
| 174 |
|
| 175 |
return FabAction(
|
| 176 |
params=params,
|
| 177 |
primary_bottleneck=primary_bottleneck,
|
| 178 |
reasoning=reasoning,
|
| 179 |
submit=submit,
|
| 180 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
agents/prompt_builder.py — Builds the structured text prompt for the LLM agent.
|
| 3 |
|
| 4 |
+
Imports from environment.env (Pydantic models) — NOT environment.models.
|
| 5 |
+
Field names match env.py's FabObservation exactly.
|
| 6 |
"""
|
| 7 |
import re
|
| 8 |
+
from environment.env import FabObservation, FabAction
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
|
| 11 |
def build_prompt(obs: FabObservation) -> str:
|
| 12 |
"""
|
| 13 |
+
Build the full text prompt from a FabObservation (Pydantic model from env.py).
|
| 14 |
+
This is what gets passed to the LLM at each training step.
|
| 15 |
"""
|
| 16 |
lines = []
|
| 17 |
|
| 18 |
# ── Header ────────────────────────────────────────────────────────────────
|
| 19 |
+
lines.append("CURRENT STATE:")
|
| 20 |
+
lines.append(f"- Step: {obs.step}/{12}")
|
| 21 |
+
lines.append(f"- Phase: {obs.phase.upper()} — {obs.phase_hint}")
|
| 22 |
lines.append(f"- Budget remaining: {obs.budget_remaining} experiments")
|
| 23 |
lines.append(f"- Current best yield: {obs.current_best_yield:.1f}%")
|
| 24 |
lines.append(f"- Target yield: >{obs.target_yield:.0f}%")
|
|
|
|
| 28 |
lines.append("ACTIVE PARAMETERS (with allowed ranges):")
|
| 29 |
for p in obs.active_params:
|
| 30 |
lo, hi = obs.param_ranges[p]
|
| 31 |
+
lines.append(f" {p}: [{_fmt(lo)} — {_fmt(hi)}]")
|
| 32 |
lines.append("")
|
| 33 |
|
| 34 |
# ── Experiment history ────────────────────────────────────────────────────
|
| 35 |
+
lines.append("EXPERIMENT HISTORY:")
|
| 36 |
+
if not obs.experiment_history:
|
| 37 |
+
lines.append(" No experiments yet.")
|
| 38 |
+
else:
|
| 39 |
for r in obs.experiment_history:
|
| 40 |
+
param_str = ", ".join(f"{k}={_fmt(v)}" for k, v in r.params.items())
|
| 41 |
+
guess_str = f" Your guess: {r.primary_bottleneck_guess}" \
|
| 42 |
+
if r.primary_bottleneck_guess else ""
|
| 43 |
lines.append(
|
| 44 |
+
f" Exp {r.step}: {param_str}\n"
|
| 45 |
+
f" → Yield: {r.yield_pct:.1f}% Defect: {r.defect}{guess_str}"
|
| 46 |
)
|
| 47 |
+
lines.append("")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
# ── Reviewer feedback (if any) ────────────────────────────────────────────
|
| 50 |
if obs.reviewer_feedback:
|
| 51 |
+
lines.append(f"⚠ REVIEWER FEEDBACK: {obs.reviewer_feedback}")
|
| 52 |
lines.append("")
|
| 53 |
|
| 54 |
+
# ── Phase instruction ────────────────────────────────────────���────────────
|
| 55 |
phase_instructions = {
|
| 56 |
"exploration": (
|
| 57 |
"PHASE: EXPLORATION — Vary parameters broadly. "
|
| 58 |
+
"Goal: understand which parameters matter most. "
|
| 59 |
"Try experiments that differ significantly from each other."
|
| 60 |
),
|
| 61 |
"hypothesis": (
|
| 62 |
+
"PHASE: HYPOTHESIS — You have data. Test your causal theory. "
|
| 63 |
"Isolate your suspected primary bottleneck by changing it while "
|
| 64 |
"holding others near their best-so-far values."
|
| 65 |
),
|
| 66 |
"exploitation": (
|
| 67 |
"PHASE: EXPLOITATION — Converge on the optimum. "
|
| 68 |
+
"Make fine adjustments around your best result. "
|
| 69 |
+
"Don't explore — exploit."
|
| 70 |
),
|
| 71 |
"submission": (
|
| 72 |
+
"PHASE: SUBMISSION — Final experiment. Submit your best recipe. "
|
| 73 |
+
"Set submit: true. The Senior Engineer will review against "
|
|
|
|
|
|
|
| 74 |
"production qualification constraints."
|
| 75 |
),
|
| 76 |
}
|
| 77 |
lines.append(phase_instructions.get(obs.phase, ""))
|
| 78 |
lines.append("")
|
| 79 |
|
| 80 |
+
# ── XML action template ───────────────────────────────────────────────────
|
| 81 |
+
lines.append("YOUR ACTION — output ONLY the XML below, no text outside tags:")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
lines.append("")
|
| 83 |
lines.append("<experiment>")
|
| 84 |
for p in obs.active_params:
|
| 85 |
lo, hi = obs.param_ranges[p]
|
| 86 |
default = (lo + hi) / 2
|
| 87 |
+
lines.append(f" {p}: {_fmt(default)}")
|
| 88 |
+
lines.append(" submit: false")
|
| 89 |
lines.append("</experiment>")
|
| 90 |
lines.append("<diagnosis>")
|
| 91 |
lines.append(f" primary_bottleneck: {obs.active_params[0]}")
|
| 92 |
+
lines.append(" reasoning: [one sentence causal reasoning]")
|
|
|
|
| 93 |
lines.append("</diagnosis>")
|
| 94 |
|
| 95 |
return "\n".join(lines)
|
|
|
|
| 97 |
|
| 98 |
def parse_action(text: str, active_params: list) -> FabAction:
|
| 99 |
"""
|
| 100 |
+
Parse LLM XML output → FabAction (Pydantic model from env.py).
|
| 101 |
+
Robust to whitespace, minor formatting variation, missing tags.
|
| 102 |
"""
|
| 103 |
params = {}
|
| 104 |
+
primary_bottleneck = active_params[0] if active_params else "temp"
|
| 105 |
+
reasoning = ""
|
| 106 |
+
submit = False
|
| 107 |
|
| 108 |
+
# Parse <experiment> block
|
| 109 |
+
exp_match = re.search(r"<experiment>(.*?)</experiment>", text, re.DOTALL | re.IGNORECASE)
|
| 110 |
if exp_match:
|
| 111 |
+
for line in exp_match.group(1).strip().split("\n"):
|
| 112 |
+
line = line.strip()
|
| 113 |
+
if ":" not in line:
|
| 114 |
+
continue
|
| 115 |
+
key, _, val = line.partition(":")
|
| 116 |
+
key, val = key.strip().lower(), val.strip()
|
| 117 |
+
if key == "submit":
|
| 118 |
+
submit = val.lower() in ("true", "yes", "1")
|
| 119 |
+
elif key in active_params:
|
| 120 |
try:
|
| 121 |
+
params[key] = float(val)
|
| 122 |
except ValueError:
|
| 123 |
pass
|
| 124 |
|
| 125 |
+
# Parse <diagnosis> block
|
| 126 |
+
diag_match = re.search(r"<diagnosis>(.*?)</diagnosis>", text, re.DOTALL | re.IGNORECASE)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
if diag_match:
|
| 128 |
+
for line in diag_match.group(1).strip().split("\n"):
|
| 129 |
+
line = line.strip()
|
| 130 |
+
if ":" not in line:
|
| 131 |
+
continue
|
| 132 |
+
key, _, val = line.partition(":")
|
| 133 |
+
key, val = key.strip().lower(), val.strip()
|
| 134 |
+
if "bottleneck" in key:
|
| 135 |
+
primary_bottleneck = val.lower()
|
| 136 |
+
elif "reason" in key:
|
| 137 |
+
reasoning = val
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
return FabAction(
|
| 140 |
params=params,
|
| 141 |
primary_bottleneck=primary_bottleneck,
|
| 142 |
reasoning=reasoning,
|
| 143 |
submit=submit,
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _fmt(v: float) -> str:
|
| 148 |
+
"""Format a float cleanly for display in prompts."""
|
| 149 |
+
if v == 0:
|
| 150 |
+
return "0"
|
| 151 |
+
if abs(v) >= 1e13 or (abs(v) < 0.01 and v != 0):
|
| 152 |
+
return f"{v:.2e}"
|
| 153 |
+
if abs(v) >= 1000:
|
| 154 |
+
return f"{v:.0f}"
|
| 155 |
+
return f"{v:.2f}"
|
agents/senior_reviewer.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
agents/senior_reviewer.py — SeniorEngineerReviewer
|
| 3 |
+
|
| 4 |
+
Rule-based (NOT an LLM). Fast, deterministic.
|
| 5 |
+
Episode-varying qualification constraints = Snorkel bonus track.
|
| 6 |
+
|
| 7 |
+
API matches env.py exactly:
|
| 8 |
+
constraints = sample_episode_constraints(active_params, rng)
|
| 9 |
+
reviewer = SeniorEngineerReviewer(constraints)
|
| 10 |
+
result = reviewer.review(params_dict) ← plain dict, not FabAction
|
| 11 |
+
"""
|
| 12 |
+
import numpy as np
|
| 13 |
+
from typing import Dict, Optional, Tuple
|
| 14 |
+
|
| 15 |
+
from environment.rsm_simulator import PARAM_RANGES
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def sample_episode_constraints(
|
| 19 |
+
active_params: list,
|
| 20 |
+
rng=None,
|
| 21 |
+
tightness_lo: float = 0.50,
|
| 22 |
+
tightness_hi: float = 0.70,
|
| 23 |
+
) -> Dict[str, Tuple[float, float]]:
|
| 24 |
+
"""
|
| 25 |
+
Sample episode-varying qualified ranges for each active parameter.
|
| 26 |
+
Returns a dict of param → (q_lo, q_hi).
|
| 27 |
+
|
| 28 |
+
Called once per episode in env.reset(). Changes every episode (Snorkel bonus).
|
| 29 |
+
The agent doesn't see these values until it submits a recipe.
|
| 30 |
+
"""
|
| 31 |
+
if rng is None:
|
| 32 |
+
rng = np.random.default_rng()
|
| 33 |
+
|
| 34 |
+
constraints = {}
|
| 35 |
+
for param in active_params:
|
| 36 |
+
lo, hi = PARAM_RANGES[param]
|
| 37 |
+
span = hi - lo
|
| 38 |
+
window = span * rng.uniform(tightness_lo, tightness_hi)
|
| 39 |
+
offset = rng.uniform(0, span - window)
|
| 40 |
+
constraints[param] = (
|
| 41 |
+
round(lo + offset, 4),
|
| 42 |
+
round(lo + offset + window, 4),
|
| 43 |
+
)
|
| 44 |
+
return constraints
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class SeniorEngineerReviewer:
|
| 48 |
+
"""
|
| 49 |
+
Reviews submitted process recipes against episode-varying constraints.
|
| 50 |
+
|
| 51 |
+
Multi-agent bonus track:
|
| 52 |
+
- Snorkel AI: constraints change every episode
|
| 53 |
+
- Fleet AI: one agent monitors/evaluates another agent's outputs
|
| 54 |
+
|
| 55 |
+
Called from env.step() when action.submit=True.
|
| 56 |
+
review() takes a plain dict of {param: value} — same as env._clamp_params output.
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
def __init__(self, episode_constraints: Dict[str, Tuple[float, float]]):
|
| 60 |
+
self.qualified_ranges = episode_constraints
|
| 61 |
+
self.revision_budget = 2
|
| 62 |
+
self.reviews_done = 0
|
| 63 |
+
|
| 64 |
+
def review(self, recipe: Dict[str, float]) -> Dict:
|
| 65 |
+
"""
|
| 66 |
+
Check each param in the recipe against its qualified range.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
recipe: plain dict {param_name: float_value}
|
| 70 |
+
— exactly what env._clamp_params() returns
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
dict with keys: approved, feedback, violations, revision_budget_remaining
|
| 74 |
+
"""
|
| 75 |
+
self.reviews_done += 1
|
| 76 |
+
violations = []
|
| 77 |
+
|
| 78 |
+
for param, value in recipe.items():
|
| 79 |
+
if param not in self.qualified_ranges:
|
| 80 |
+
continue
|
| 81 |
+
q_lo, q_hi = self.qualified_ranges[param]
|
| 82 |
+
if not (q_lo <= value <= q_hi):
|
| 83 |
+
violations.append({
|
| 84 |
+
"param": param,
|
| 85 |
+
"submitted": round(value, 4),
|
| 86 |
+
"qualified_range": [round(q_lo, 4), round(q_hi, 4)],
|
| 87 |
+
"delta": round(min(abs(value - q_lo), abs(value - q_hi)), 4),
|
| 88 |
+
})
|
| 89 |
+
|
| 90 |
+
remaining = max(0, self.revision_budget - self.reviews_done)
|
| 91 |
+
|
| 92 |
+
if violations:
|
| 93 |
+
parts = [
|
| 94 |
+
f"{v['param']}={v['submitted']} outside "
|
| 95 |
+
f"[{v['qualified_range'][0]}, {v['qualified_range'][1]}]"
|
| 96 |
+
for v in violations
|
| 97 |
+
]
|
| 98 |
+
return {
|
| 99 |
+
"approved": False,
|
| 100 |
+
"feedback": (
|
| 101 |
+
f"Recipe REJECTED ({len(violations)} violation(s)). "
|
| 102 |
+
f"Revise: {'; '.join(parts)}. "
|
| 103 |
+
f"Revision budget remaining: {remaining} experiments."
|
| 104 |
+
),
|
| 105 |
+
"violations": violations,
|
| 106 |
+
"revision_budget_remaining": remaining,
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
return {
|
| 110 |
+
"approved": True,
|
| 111 |
+
"feedback": "Recipe APPROVED. Qualified for production.",
|
| 112 |
+
"violations": [],
|
| 113 |
+
"revision_budget_remaining": remaining,
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
def get_constraint_hint(self) -> str:
|
| 117 |
+
"""
|
| 118 |
+
Vague hint shown in agent prompt after step 8.
|
| 119 |
+
Forces agent to hedge toward center-of-range values.
|
| 120 |
+
"""
|
| 121 |
+
return (
|
| 122 |
+
"NOTE: Your final recipe will be reviewed against production "
|
| 123 |
+
"qualification constraints. Parameters far from their nominal "
|
| 124 |
+
"process window are at risk of rejection."
|
| 125 |
+
)
|
environment/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (166 Bytes). View file
|
|
|
environment/__pycache__/env.cpython-313.pyc
ADDED
|
Binary file (12.4 kB). View file
|
|
|
environment/__pycache__/rsm_simulator.cpython-313.pyc
ADDED
|
Binary file (9.54 kB). View file
|
|
|
environment/reviewer.py
DELETED
|
@@ -1,111 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
reviewer.py — SeniorEngineerReviewer
|
| 3 |
-
|
| 4 |
-
Rule-based (NOT an LLM). Fast, deterministic, no extra training needed.
|
| 5 |
-
Generates episode-varying qualification constraints — this is the Snorkel
|
| 6 |
-
"changing preferences" bonus track.
|
| 7 |
-
|
| 8 |
-
Each episode a new set of tighter-than-optimum qualified ranges is sampled.
|
| 9 |
-
The agent's submitted recipe must satisfy ALL of them to be approved.
|
| 10 |
-
"""
|
| 11 |
-
import numpy as np
|
| 12 |
-
from typing import Dict, Optional
|
| 13 |
-
from environment.models import ReviewerConstraints, FabAction
|
| 14 |
-
from rsm_simulator import PARAM_RANGES
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
def generate_episode_constraints(
|
| 18 |
-
active_params: list,
|
| 19 |
-
optimum_params: Dict[str, float],
|
| 20 |
-
seed: int,
|
| 21 |
-
tightness: float = 0.35,
|
| 22 |
-
) -> ReviewerConstraints:
|
| 23 |
-
"""
|
| 24 |
-
Sample episode-varying qualified ranges around (but not always centered on)
|
| 25 |
-
the true optimum. The agent doesn't know these until it submits.
|
| 26 |
-
|
| 27 |
-
tightness: fraction of full range that is "qualified"
|
| 28 |
-
"""
|
| 29 |
-
rng = np.random.default_rng(seed + 9999)
|
| 30 |
-
qualified_ranges = {}
|
| 31 |
-
|
| 32 |
-
for p in active_params:
|
| 33 |
-
lo_full, hi_full = PARAM_RANGES[p]
|
| 34 |
-
full_width = hi_full - lo_full
|
| 35 |
-
window = tightness * full_width
|
| 36 |
-
|
| 37 |
-
# Center the window near optimum but with some random offset
|
| 38 |
-
opt_val = optimum_params[p]
|
| 39 |
-
offset = rng.uniform(-0.15 * full_width, 0.15 * full_width)
|
| 40 |
-
center = np.clip(opt_val + offset, lo_full + window / 2, hi_full - window / 2)
|
| 41 |
-
|
| 42 |
-
q_lo = round(max(lo_full, center - window / 2), 4)
|
| 43 |
-
q_hi = round(min(hi_full, center + window / 2), 4)
|
| 44 |
-
qualified_ranges[p] = [q_lo, q_hi]
|
| 45 |
-
|
| 46 |
-
return ReviewerConstraints(qualified_ranges=qualified_ranges)
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
class SeniorEngineerReviewer:
|
| 50 |
-
"""
|
| 51 |
-
Reviews the agent's submitted recipe against episode-varying
|
| 52 |
-
qualification constraints. Returns approval or specific feedback.
|
| 53 |
-
|
| 54 |
-
This creates the multi-agent coordination dynamic:
|
| 55 |
-
the Process Engineer agent must read reviewer feedback and adapt.
|
| 56 |
-
"""
|
| 57 |
-
|
| 58 |
-
def __init__(self, constraints: ReviewerConstraints):
|
| 59 |
-
self.constraints = constraints
|
| 60 |
-
self.revision_budget = constraints.revision_budget
|
| 61 |
-
self.reviews_done = 0
|
| 62 |
-
|
| 63 |
-
def review(self, action: FabAction) -> Dict:
|
| 64 |
-
"""
|
| 65 |
-
Check each active parameter against its qualified range.
|
| 66 |
-
Returns approval status + specific violation feedback.
|
| 67 |
-
"""
|
| 68 |
-
self.reviews_done += 1
|
| 69 |
-
violations = []
|
| 70 |
-
|
| 71 |
-
for param, value in action.params.items():
|
| 72 |
-
if param not in self.constraints.qualified_ranges:
|
| 73 |
-
continue
|
| 74 |
-
lo, hi = self.constraints.qualified_ranges[param]
|
| 75 |
-
if not (lo <= value <= hi):
|
| 76 |
-
violations.append(
|
| 77 |
-
f"{param}={value:.4g} is outside qualified range "
|
| 78 |
-
f"[{lo:.4g}, {hi:.4g}]"
|
| 79 |
-
)
|
| 80 |
-
|
| 81 |
-
if violations:
|
| 82 |
-
remaining = max(0, self.revision_budget - self.reviews_done)
|
| 83 |
-
return {
|
| 84 |
-
"approved": False,
|
| 85 |
-
"feedback": (
|
| 86 |
-
f"Recipe REJECTED by Senior Engineer. "
|
| 87 |
-
f"Violations: {'; '.join(violations)}. "
|
| 88 |
-
f"Revision budget remaining: {remaining} experiments."
|
| 89 |
-
),
|
| 90 |
-
"violations": violations,
|
| 91 |
-
"revision_budget_remaining": remaining,
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
return {
|
| 95 |
-
"approved": True,
|
| 96 |
-
"feedback": "Recipe APPROVED. Qualified for production.",
|
| 97 |
-
"violations": [],
|
| 98 |
-
"revision_budget_remaining": self.revision_budget - self.reviews_done,
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
def get_constraint_hint(self) -> str:
|
| 102 |
-
"""
|
| 103 |
-
Returns a vague hint visible in the agent prompt after step 8.
|
| 104 |
-
The agent knows constraints EXIST but not exact values until submission.
|
| 105 |
-
This forces the agent to hedge toward center-of-range values.
|
| 106 |
-
"""
|
| 107 |
-
return (
|
| 108 |
-
"NOTE: Your final recipe will be reviewed against production "
|
| 109 |
-
"qualification constraints. Parameters far from their nominal "
|
| 110 |
-
"process window are at risk of rejection."
|
| 111 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/server.py → server.py
RENAMED
|
File without changes
|