Nanny7 commited on
Commit
8f493ae
·
1 Parent(s): 5088334

file fixing

Browse files
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
- This is the full cognitive context the agent receives at each step.
5
- The agent outputs XML tags which the env parses back into FabAction.
6
  """
7
  import re
8
- from typing import Optional, Dict
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
- Construct the full text prompt from a FabObservation.
40
- This is what gets passed to the LLM at each step.
41
  """
42
  lines = []
43
 
44
  # ── Header ────────────────────────────────────────────────────────────────
45
- lines.append(f"CURRENT STATE:")
46
- lines.append(f"- Step: {obs.step + 1}/{12}")
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}, {hi}]")
58
  lines.append("")
59
 
60
  # ── Experiment history ────────────────────────────────────────────────────
61
- if obs.experiment_history:
62
- lines.append("EXPERIMENT HISTORY:")
 
 
63
  for r in obs.experiment_history:
64
- param_str = ", ".join(
65
- f"{k}={v:.4g}" for k, v in r.params.items()
66
- )
67
  lines.append(
68
- f" Exp {r.step}: {param_str}"
69
- f"Yield: {r.yield_pct}%, Defect: {r.defect}"
70
  )
71
- lines.append("")
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"⚠ REVIEWER FEEDBACK: {obs.reviewer_feedback}")
81
  lines.append("")
82
 
83
- # ── Phase-specific instruction ────────────────────────────────────────────
84
  phase_instructions = {
85
  "exploration": (
86
  "PHASE: EXPLORATION — Vary parameters broadly. "
87
- "Your goal is to understand which parameters matter most. "
88
  "Try experiments that differ significantly from each other."
89
  ),
90
  "hypothesis": (
91
- "PHASE: HYPOTHESIS — You have data. Now test your causal theory. "
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 so far. "
98
- "You're close — don't explore, exploit."
99
  ),
100
  "submission": (
101
- "PHASE: SUBMISSION — This is your final experiment. "
102
- "Submit your best process recipe. "
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
- # ── Action request ────────────────────────────────────────────────────────
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:.4g}")
 
124
  lines.append("</experiment>")
125
  lines.append("<diagnosis>")
126
  lines.append(f" primary_bottleneck: {obs.active_params[0]}")
127
- lines.append(" reasoning: [your one-sentence causal 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 the LLM's XML output into a FabAction.
137
- Robust to extra whitespace and minor formatting variations.
138
  """
139
  params = {}
 
 
 
140
 
141
- # Extract experiment block
142
- exp_match = re.search(r"<experiment>(.*?)</experiment>", text, re.DOTALL)
143
  if exp_match:
144
- exp_block = exp_match.group(1)
145
- for p in active_params:
146
- # Match "param: value" patterns
147
- m = re.search(rf"{re.escape(p)}\s*:\s*([0-9eE+\-.]+)", exp_block)
148
- if m:
 
 
 
 
149
  try:
150
- params[p] = float(m.group(1))
151
  except ValueError:
152
  pass
153
 
154
- # Extract diagnosis block
155
- primary_bottleneck = active_params[0] # fallback
156
- reasoning = ""
157
- submit = False
158
-
159
- diag_match = re.search(r"<diagnosis>(.*?)</diagnosis>", text, re.DOTALL)
160
  if diag_match:
161
- diag_block = diag_match.group(1)
162
-
163
- pb_match = re.search(r"primary_bottleneck\s*:\s*(\S+)", diag_block)
164
- if pb_match:
165
- primary_bottleneck = pb_match.group(1).strip().lower()
166
-
167
- r_match = re.search(r"reasoning\s*:\s*(.+)", diag_block)
168
- if r_match:
169
- reasoning = r_match.group(1).strip()
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