AzraelH commited on
Commit
13d7e48
·
1 Parent(s): c9ead81

Prepare OpenEnv Space submission

Browse files
Files changed (6) hide show
  1. .gitignore +2 -0
  2. client.py +4 -1
  3. inference.py +277 -0
  4. pyproject.toml +1 -0
  5. requirements.txt +1 -0
  6. server/app.py +0 -2
.gitignore CHANGED
@@ -1,2 +1,4 @@
1
  __pycache__/
2
  *.py[cod]
 
 
 
1
  __pycache__/
2
  *.py[cod]
3
+ .tmp/
4
+ output/
client.py CHANGED
@@ -8,7 +8,10 @@ from openenv.core import EnvClient
8
  from openenv.core.client_types import StepResult
9
  from openenv.core.env_server.types import State
10
 
11
- from .models import EngineerManagerAction, EngineerManagerObservation
 
 
 
12
 
13
 
14
  class EngineerManagerEnv(
 
8
  from openenv.core.client_types import StepResult
9
  from openenv.core.env_server.types import State
10
 
11
+ try:
12
+ from .models import EngineerManagerAction, EngineerManagerObservation
13
+ except ImportError:
14
+ from models import EngineerManagerAction, EngineerManagerObservation
15
 
16
 
17
  class EngineerManagerEnv(
inference.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ import math
4
+ import os
5
+ import textwrap
6
+ from typing import Any
7
+
8
+ from openai import OpenAI
9
+ from openenv.core.generic_client import GenericEnvClient
10
+
11
+
12
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
13
+ MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
14
+ HF_TOKEN = os.getenv("HF_TOKEN")
15
+ LOCAL_IMAGE_NAME = os.getenv("LOCAL_IMAGE_NAME")
16
+ OPENENV_BASE_URL = os.getenv("OPENENV_BASE_URL")
17
+ TASK_NAME = os.getenv("TASK_NAME", "engineer-manager")
18
+ BENCHMARK = os.getenv("BENCHMARK", "openenv")
19
+ MAX_STEPS = int(os.getenv("MAX_STEPS", "32"))
20
+ TEMPERATURE = float(os.getenv("TEMPERATURE", "0.1"))
21
+ MAX_TOKENS = int(os.getenv("MAX_TOKENS", "120"))
22
+
23
+ SYSTEM_PROMPT = textwrap.dedent(
24
+ """
25
+ You are selecting actions for an environment that simulates an engineer-manager workday.
26
+ Return exactly one compact JSON object with integer keys:
27
+ {"target_slot": <int>, "operation": <int>}
28
+
29
+ Operations:
30
+ 0 = idle
31
+ 1 = schedule work at target_slot
32
+ 2 = reschedule a meeting at target_slot
33
+ 3 = toggle mute comms
34
+
35
+ Goals:
36
+ - Maximize sustained deep work flow_score.
37
+ - Avoid unnecessary social_debt and calendar_churn.
38
+ - Prefer scheduling work into future empty slots.
39
+ - Use reschedule_meeting only when it clearly helps.
40
+ - Toggle mute comms early if distractions are high and it is currently off.
41
+
42
+ Rules:
43
+ - target_slot must be within the timeline bounds.
44
+ - Return JSON only. No markdown. No explanation.
45
+ """
46
+ ).strip()
47
+
48
+
49
+ def _require_env(name: str, value: str | None) -> str:
50
+ if value:
51
+ return value
52
+ raise RuntimeError(f"Missing required environment variable: {name}")
53
+
54
+
55
+ def _sanitize_field(value: Any) -> str:
56
+ text = str(value)
57
+ return text.replace("\r", " ").replace("\n", " ").strip()
58
+
59
+
60
+ def log_start(task: str, env: str, model: str) -> None:
61
+ print(
62
+ f"[START] task={_sanitize_field(task)} env={_sanitize_field(env)} model={_sanitize_field(model)}",
63
+ flush=True,
64
+ )
65
+
66
+
67
+ def log_step(
68
+ step: int,
69
+ action: str,
70
+ reward: float,
71
+ done: bool,
72
+ error: str | None,
73
+ ) -> None:
74
+ error_text = "null" if error in (None, "") else _sanitize_field(error)
75
+ print(
76
+ f"[STEP] step={step} action={_sanitize_field(action)} reward={reward:.2f} "
77
+ f"done={str(done).lower()} error={error_text}",
78
+ flush=True,
79
+ )
80
+
81
+
82
+ def log_end(success: bool, steps: int, score: float, rewards: list[float]) -> None:
83
+ rewards_text = ",".join(f"{reward:.2f}" for reward in rewards)
84
+ print(
85
+ f"[END] success={str(success).lower()} steps={steps} score={score:.2f} rewards={rewards_text}",
86
+ flush=True,
87
+ )
88
+
89
+
90
+ def estimate_max_flow_score(timeline: list[int]) -> float:
91
+ slot_count = len(timeline)
92
+ if slot_count <= 0:
93
+ return 1.0
94
+ hours = slot_count * 0.5
95
+ return max(1.0, hours * hours)
96
+
97
+
98
+ def normalize_score(total_reward: float, observation: dict[str, Any]) -> float:
99
+ timeline = observation.get("timeline") or []
100
+ max_score = estimate_max_flow_score(timeline)
101
+ normalized = total_reward / max_score
102
+ return min(1.0, max(0.0, normalized))
103
+
104
+
105
+ def first_future_slot(observation: dict[str, Any], kind: int) -> int | None:
106
+ timeline = observation.get("timeline") or []
107
+ current_slot = int(observation.get("current_slot", 0))
108
+ for index in range(current_slot, len(timeline)):
109
+ if int(timeline[index]) == kind:
110
+ return index
111
+ return None
112
+
113
+
114
+ def first_future_empty_slot(observation: dict[str, Any]) -> int | None:
115
+ return first_future_slot(observation, 0)
116
+
117
+
118
+ def build_user_prompt(
119
+ step: int,
120
+ observation: dict[str, Any],
121
+ rewards: list[float],
122
+ history: list[str],
123
+ ) -> str:
124
+ timeline = observation.get("timeline") or []
125
+ metadata = observation.get("metadata") or {}
126
+ return textwrap.dedent(
127
+ f"""
128
+ step={step}
129
+ current_slot={int(observation.get("current_slot", 0))}
130
+ current_time={observation.get("current_time", "unknown")}
131
+ mute_comms={bool(observation.get("mute_comms", False))}
132
+ distraction_risk={float(observation.get("distraction_risk", 0.0))}
133
+ flow_score={float(observation.get("flow_score", 0.0)):.2f}
134
+ social_debt={float(observation.get("social_debt", 0.0)):.2f}
135
+ calendar_churn={int(observation.get("calendar_churn", 0))}
136
+ recovery_state={int(observation.get("recovery_state", 0))}
137
+ timeline={timeline}
138
+ task_buffer={json.dumps(observation.get("task_buffer", []), separators=(",", ":"))}
139
+ last_rewards={",".join(f"{reward:.2f}" for reward in rewards[-5:]) or "none"}
140
+ recent_history={json.dumps(history[-5:])}
141
+ last_metadata={json.dumps(metadata, separators=(",", ":"))}
142
+ Choose the single next action.
143
+ """
144
+ ).strip()
145
+
146
+
147
+ def choose_fallback_action(observation: dict[str, Any]) -> dict[str, int]:
148
+ current_slot = int(observation.get("current_slot", 0))
149
+ distraction_risk = float(observation.get("distraction_risk", 0.0))
150
+ mute_comms = bool(observation.get("mute_comms", False))
151
+ if distraction_risk >= 0.2 and not mute_comms:
152
+ return {"target_slot": current_slot, "operation": 3}
153
+
154
+ empty_slot = first_future_empty_slot(observation)
155
+ if empty_slot is not None and observation.get("task_buffer"):
156
+ return {"target_slot": empty_slot, "operation": 1}
157
+
158
+ meeting_slot = first_future_slot(observation, 2)
159
+ if meeting_slot is not None and current_slot <= meeting_slot:
160
+ return {"target_slot": meeting_slot, "operation": 2}
161
+
162
+ return {"target_slot": current_slot, "operation": 0}
163
+
164
+
165
+ def coerce_action(raw_text: str, observation: dict[str, Any]) -> dict[str, int]:
166
+ timeline = observation.get("timeline") or []
167
+ max_slot = max(0, len(timeline) - 1)
168
+ fallback = choose_fallback_action(observation)
169
+ try:
170
+ data = json.loads(raw_text)
171
+ target_slot = int(data["target_slot"])
172
+ operation = int(data["operation"])
173
+ except Exception:
174
+ return fallback
175
+
176
+ if operation not in {0, 1, 2, 3}:
177
+ return fallback
178
+ target_slot = min(max(target_slot, 0), max_slot)
179
+ return {"target_slot": target_slot, "operation": operation}
180
+
181
+
182
+ def get_model_action(
183
+ client: OpenAI,
184
+ step: int,
185
+ observation: dict[str, Any],
186
+ rewards: list[float],
187
+ history: list[str],
188
+ ) -> dict[str, int]:
189
+ user_prompt = build_user_prompt(step, observation, rewards, history)
190
+ try:
191
+ completion = client.chat.completions.create(
192
+ model=MODEL_NAME,
193
+ messages=[
194
+ {"role": "system", "content": SYSTEM_PROMPT},
195
+ {"role": "user", "content": user_prompt},
196
+ ],
197
+ temperature=TEMPERATURE,
198
+ max_tokens=MAX_TOKENS,
199
+ )
200
+ content = (completion.choices[0].message.content or "").strip()
201
+ return coerce_action(content, observation)
202
+ except Exception:
203
+ return choose_fallback_action(observation)
204
+
205
+
206
+ async def create_env() -> GenericEnvClient:
207
+ if OPENENV_BASE_URL:
208
+ env = GenericEnvClient(base_url=OPENENV_BASE_URL)
209
+ await env.connect()
210
+ return env
211
+
212
+ image_name = _require_env("LOCAL_IMAGE_NAME", LOCAL_IMAGE_NAME)
213
+ return await GenericEnvClient.from_docker_image(image_name)
214
+
215
+
216
+ async def main() -> None:
217
+ api_key = _require_env("HF_TOKEN", HF_TOKEN)
218
+ client = OpenAI(base_url=API_BASE_URL, api_key=api_key)
219
+ env = None
220
+ rewards: list[float] = []
221
+ history: list[str] = []
222
+ steps_taken = 0
223
+ success = False
224
+ score = 0.0
225
+
226
+ log_start(TASK_NAME, BENCHMARK, MODEL_NAME)
227
+
228
+ try:
229
+ env = await create_env()
230
+ result = await env.reset()
231
+ observation = dict(result.observation)
232
+
233
+ for step in range(1, MAX_STEPS + 1):
234
+ if result.done:
235
+ break
236
+
237
+ action = get_model_action(client, step, observation, rewards, history)
238
+ result = await env.step(action)
239
+ observation = dict(result.observation)
240
+
241
+ reward = float(result.reward or 0.0)
242
+ done = bool(result.done)
243
+ metadata = observation.get("metadata") or {}
244
+ error = metadata.get("last_action_error")
245
+
246
+ rewards.append(reward)
247
+ steps_taken = step
248
+
249
+ action_text = (
250
+ f"target_slot={int(action['target_slot'])},operation={int(action['operation'])}"
251
+ )
252
+ log_step(step, action_text, reward, done, error)
253
+
254
+ history.append(
255
+ f"step={step} action={action_text} reward={reward:.2f} "
256
+ f"flow={float(observation.get('flow_score', 0.0)):.2f} "
257
+ f"debt={float(observation.get('social_debt', 0.0)):.2f}"
258
+ )
259
+
260
+ if done:
261
+ break
262
+
263
+ total_reward = math.fsum(rewards)
264
+ score = normalize_score(total_reward, observation if "observation" in locals() else {})
265
+ score = round(score, 2)
266
+ success = score > 0.0
267
+ finally:
268
+ if env is not None:
269
+ try:
270
+ await env.close()
271
+ except Exception:
272
+ pass
273
+ log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
274
+
275
+
276
+ if __name__ == "__main__":
277
+ asyncio.run(main())
pyproject.toml CHANGED
@@ -11,6 +11,7 @@ requires-python = ">=3.11"
11
  dependencies = [
12
  "fastapi>=0.135.0",
13
  "numpy>=2.0.0",
 
14
  "openenv-core[core]>=0.2.0",
15
  "pydantic>=2.0.0",
16
  "streamlit>=1.40.0",
 
11
  dependencies = [
12
  "fastapi>=0.135.0",
13
  "numpy>=2.0.0",
14
+ "openai>=1.0.0",
15
  "openenv-core[core]>=0.2.0",
16
  "pydantic>=2.0.0",
17
  "streamlit>=1.40.0",
requirements.txt CHANGED
@@ -1,5 +1,6 @@
1
  fastapi>=0.135.0
2
  numpy>=2.0.0
 
3
  openenv-core[core]>=0.2.0
4
  pydantic>=2.0.0
5
  streamlit>=1.40.0
 
1
  fastapi>=0.135.0
2
  numpy>=2.0.0
3
+ openai>=1.0.0
4
  openenv-core[core]>=0.2.0
5
  pydantic>=2.0.0
6
  streamlit>=1.40.0
server/app.py CHANGED
@@ -436,8 +436,6 @@ def web_js() -> PlainTextResponse:
436
  @app.get("/favicon.ico", include_in_schema=False)
437
  def favicon() -> Response:
438
  return Response(status_code=204)
439
-
440
-
441
  @app.get("/manifest.json", include_in_schema=False)
442
  def manifest() -> JSONResponse:
443
  return JSONResponse(
 
436
  @app.get("/favicon.ico", include_in_schema=False)
437
  def favicon() -> Response:
438
  return Response(status_code=204)
 
 
439
  @app.get("/manifest.json", include_in_schema=False)
440
  def manifest() -> JSONResponse:
441
  return JSONResponse(