adithya9903 commited on
Commit
b451b97
·
1 Parent(s): ad43b05
.dockerignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gitignore
3
+ **/__pycache__
4
+ **/*.pyc
5
+ **/.pytest_cache
6
+ **/.mypy_cache
7
+ **/.ruff_cache
8
+ **/node_modules
9
+ **/dist
10
+ **/.env
11
+ **/.env.*
12
+ !openenv-polypharmacy/.env.example
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -1,6 +1,35 @@
 
1
  venv/
2
- .DS_Store
 
 
 
 
 
3
  __pycache__/
 
 
 
 
 
 
 
4
  node_modules/
5
- dist/
6
- .env
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Python ---
2
  venv/
3
+ .venv/
4
+ env/
5
+ .env
6
+ .env.*
7
+ !openenv-polypharmacy/.env.example
8
+ *.py[cod]
9
  __pycache__/
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .coverage
14
+ coverage.xml
15
+
16
+ # --- Node / frontend ---
17
  node_modules/
18
+ **/node_modules/
19
+ frontend/dist/
20
+ **/dist/
21
+ npm-debug.log*
22
+ yarn-debug.log*
23
+ yarn-error.log*
24
+ pnpm-debug.log*
25
+
26
+ # --- Build / temp ---
27
+ *.log
28
+ *.tmp
29
+ *.swp
30
+ .DS_Store
31
+
32
+ # --- Project-specific nested paths ---
33
+ openenv-polypharmacy/frontend/node_modules/
34
+ openenv-polypharmacy/frontend/dist/
35
+ openenv-polypharmacy/.pytest_cache/
.gitignore copy ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Python ---
2
+ venv/
3
+ .venv/
4
+ env/
5
+ .env
6
+ .env.*
7
+ !openenv-polypharmacy/.env.example
8
+ *.py[cod]
9
+ __pycache__/
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .coverage
14
+ coverage.xml
15
+
16
+ # --- Node / frontend ---
17
+ node_modules/
18
+ **/node_modules/
19
+ frontend/dist/
20
+ **/dist/
21
+ npm-debug.log*
22
+ yarn-debug.log*
23
+ yarn-error.log*
24
+ pnpm-debug.log*
25
+
26
+ # --- Build / temp ---
27
+ *.log
28
+ *.tmp
29
+ *.swp
30
+ .DS_Store
31
+
32
+ # --- Project-specific nested paths ---
33
+ openenv-polypharmacy/frontend/node_modules/
34
+ openenv-polypharmacy/frontend/dist/
35
+ openenv-polypharmacy/.pytest_cache/
Dockerfile ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine AS frontend-builder
2
+ WORKDIR /app/frontend
3
+ COPY openenv-polypharmacy/frontend/package*.json ./
4
+ RUN npm ci
5
+ COPY openenv-polypharmacy/frontend/ ./
6
+ RUN npm run build
7
+
8
+ FROM python:3.11-slim
9
+
10
+ RUN apt-get update && \
11
+ apt-get install -y --no-install-recommends build-essential curl && \
12
+ rm -rf /var/lib/apt/lists/*
13
+
14
+ WORKDIR /app
15
+
16
+ COPY openenv-polypharmacy/backend/requirements.txt /app/backend/requirements.txt
17
+ RUN pip install --no-cache-dir -r /app/backend/requirements.txt
18
+
19
+ COPY openenv-polypharmacy/backend /app/backend
20
+ COPY openenv-polypharmacy/data /app/data
21
+ COPY openenv-polypharmacy/scripts /app/scripts
22
+ COPY openenv-polypharmacy/openenv.yaml /app/openenv.yaml
23
+ COPY openenv-polypharmacy/.env.example /app/.env.example
24
+ COPY openenv-polypharmacy/inference.py /app/inference.py
25
+
26
+ COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
27
+
28
+ RUN python3 /app/scripts/preprocess_data.py
29
+
30
+ ENV PORT=7860
31
+ ENV PYTHONPATH="/app/backend/src:${PYTHONPATH}"
32
+ ENV PYTHONUNBUFFERED=1
33
+
34
+ EXPOSE 7860
35
+
36
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=15s --retries=3 \
37
+ CMD curl -f http://localhost:7860/health || exit 1
38
+
39
+ CMD ["sh", "-c", "uvicorn backend.main:app --host 0.0.0.0 --port ${PORT:-7860}"]
openenv-polypharmacy/README.md → README.MD RENAMED
File without changes
openenv-polypharmacy/PROMPT.md DELETED
@@ -1,571 +0,0 @@
1
- You are an expert Python backend, ML, and infrastructure engineer.
2
- Your task is to implement a complete, production-ready OpenEnv environment called **PolypharmacyEnv** for training and evaluating agentic RL policies that act as an "elderly polypharmacy safety agent" (clinical pharmacist assistant).
3
-
4
- The deliverable MUST satisfy all of the following:
5
- - Fully compliant with the OpenEnv spec (typed models, `step()` / `reset()` / `state()`, `openenv.yaml`, HTTP server, Dockerfile).
6
- - Simulates a realistic healthcare workflow around elderly polypharmacy and dangerous drug combinations.
7
- - Defines at least **3 tasks** (easy → medium → hard) with deterministic agent graders producing scores in (0.0, 1.0).
8
- - Provides shaped rewards over the trajectory (not just sparse terminal rewards).
9
- - Includes a baseline LLM-based inference script `inference.py` in the repo root, following the evaluation requirements:
10
- - Uses the OpenAI Python client.
11
- - Reads `OPENAI_API_KEY`, `API_BASE_URL`, `MODEL_NAME`, and `HF_TOKEN` from the environment.
12
- - Emits structured stdout logs in the exact `[START]`, `[STEP]`, `[END]` format from the OpenEnv sample inference script.
13
- - Is containerized and deployable as a **Hugging Face Space** tagged with `openenv` that responds to OpenEnv-style `reset` / `step` / `state` HTTP calls.
14
-
15
- Implement everything described below.
16
-
17
- =================================================
18
- 1. Repository and folder structure
19
- =================================================
20
-
21
- Create a Python package repository with this structure (names are important unless clearly labeled as examples):
22
-
23
- - `openenv-polypharmacy/`
24
- - `openenv.yaml`
25
- - `README.md`
26
- - `requirements.txt`
27
- - `Dockerfile`
28
- - `inference.py` # baseline LLM agent per spec
29
- - `pyproject.toml` or `setup.cfg` (optional but recommended)
30
- - `src/`
31
- - `polypharmacy_env/`
32
- - `__init__.py`
33
- - `config.py`
34
- - `models.py` # Action, Observation, State, helper models
35
- - `env_core.py` # PolypharmacyEnv implementation
36
- - `tasks.py` # task setup utilities
37
- - `graders.py` # deterministic graders for each task
38
- - `rewards.py` # reward shaping logic
39
- - `data_loader.py` # load/preprocess patient and lookup data
40
- - `ddi_simulator.py` # local DDI / guideline simulator
41
- - `api/`
42
- - `__init__.py`
43
- - `schemas.py` # HTTP request/response schemas
44
- - `server.py` # FastAPI app exposing OpenEnv endpoints
45
- - `baselines/`
46
- - `__init__.py`
47
- - `heuristic_agent.py` # simple rule-based baseline agent
48
- - `random_agent.py` # trivial random baseline (optional)
49
- - `tests/`
50
- - `__init__.py`
51
- - `test_env_core.py`
52
- - `test_api.py`
53
- - `data/`
54
- - `raw/` # placeholder for real/synthetic source data
55
- - `processed/`
56
- - `lookups/`
57
- - `ddi_rules.csv`
58
- - `beers_criteria.csv`
59
- - `drug_metadata.csv`
60
- - `scripts/`
61
- - `preprocess_data.py`
62
- - `run_validation.sh` # optional; runs OpenEnv validator, tests, etc.
63
-
64
- Use Python 3.10+ with full type hints, and keep the code black/isort-compatible.
65
-
66
- =================================================
67
- 2. Domain, data, and clinical abstraction
68
- =================================================
69
-
70
- 2.1. Core scenario
71
-
72
- Model an elderly patient (age ≥ 65) with:
73
- - Demographics: age, sex.
74
- - Comorbidities: e.g., hypertension, diabetes, heart failure, CKD, dementia.
75
- - Basic labs: kidney function (eGFR category), liver function category.
76
- - A current medication list (polypharmacy, e.g., 3–15 drugs depending on task).
77
-
78
- Each **episode** is one medication-review session where the agent:
79
- - Observes patient info and current meds.
80
- - Optionally **queries** a DDI/guideline tool for specific drug pairs.
81
- - Proposes **interventions**:
82
- - `stop`: discontinue a drug.
83
- - `dose_reduce`: lower dose of a drug.
84
- - `substitute`: swap to a safer alternative.
85
- - `add_monitoring`: keep the drug but flag extra monitoring.
86
- - Calls `finish_review` when it decides the regimen is acceptable or budgets are exhausted.
87
-
88
- No external PHI, EHRs, or online APIs: all data is **synthetic** or de-identified and local to the container (CSV files).
89
-
90
- 2.2. Data files and CSV schemas
91
-
92
- Implement local CSVs under `data/lookups/`:
93
-
94
- **`drug_metadata.csv`**
95
- - `drug_id` (string; unique key)
96
- - `generic_name` (string)
97
- - `atc_class` (string)
98
- - `is_high_risk_elderly` (0/1)
99
- - `default_dose_mg` (float)
100
- - `min_dose_mg` (float)
101
- - `max_dose_mg` (float)
102
-
103
- **`beers_criteria.csv`**
104
- - `drug_id` (string)
105
- - `criterion_type` (enum string: `avoid`, `caution`, `dose_adjust`, `avoid_in_condition`)
106
- - `condition` (nullable string; e.g., `CKD`, `dementia`)
107
- - `rationale` (brief text)
108
-
109
- **`ddi_rules.csv`**
110
- - `drug_id_1` (string; normalized so `drug_id_1 < drug_id_2` lexicographically)
111
- - `drug_id_2` (string)
112
- - `severity` (enum string: `mild`, `moderate`, `severe`)
113
- - `mechanism` (short text)
114
- - `recommendation` (enum string: `avoid_combination`, `monitor_closely`, `dose_adjust`, `no_action`)
115
- - `base_risk_score` (float in [0.0, 1.0])
116
-
117
- Implement a synthetic patient-episode dataset under `data/processed/`:
118
-
119
- **`patients_polypharmacy.csv`**
120
- - `episode_id` (string)
121
- - `age` (int)
122
- - `sex` (enum: `M`, `F`, `O`)
123
- - `conditions` (semicolon-separated; e.g., `HTN;DM;CKD`)
124
- - `eGFR_category` (enum: `normal`, `mild`, `moderate`, `severe`)
125
- - `liver_function_category` (enum: `normal`, `impaired`)
126
- - `medication_ids` (semicolon-separated list of `drug_id`)
127
- - `baseline_risk_score` (float in [0.0, 1.0])
128
-
129
- 2.3. Preprocessing script
130
-
131
- In `scripts/preprocess_data.py`:
132
- - If real data is not provided, procedurally generate synthetic but plausible data using:
133
- - Random combinations of conditions and drugs constrained by simple rules (e.g., CKD + renally-cleared drugs).
134
- - Controlled distribution of high-risk DDIs and Beers violations.
135
- - Explicitly tag episodes as easy/medium/hard (e.g., via number of drugs, number/severity of DDIs, and number of Beers issues).
136
- - Save `patients_polypharmacy.csv` ready for the environment to consume.
137
-
138
- =================================================
139
- 3. OpenEnv models and environment implementation
140
- =================================================
141
-
142
- 3.1. Models
143
-
144
- In `models.py`, define dataclasses or Pydantic models that extend the appropriate OpenEnv base types (`Action`, `Observation`, `State`) and are JSON-compatible.
145
-
146
- Auxiliary models:
147
-
148
- **`MedicationEntry`**
149
- - `drug_id: str`
150
- - `generic_name: str`
151
- - `atc_class: str`
152
- - `dose_mg: float`
153
- - `frequency: str` # e.g., `qd`, `bid`
154
- - `route: str` # e.g., `po`
155
- - `is_high_risk_elderly: bool`
156
- - `beers_flags: list[str]` # e.g., `["avoid", "dose_adjust_CKD"]`
157
-
158
- **`InteractionQueryRecord`**
159
- - `drug_id_1: str`
160
- - `drug_id_2: str`
161
- - `severity: str | None`
162
- - `recommendation: str | None`
163
- - `risk_score: float | None`
164
- - `step_index: int`
165
-
166
- **`InterventionRecord`**
167
- - `target_drug_id: str`
168
- - `action_type: Literal["stop", "dose_reduce", "substitute", "add_monitoring"]`
169
- - `proposed_new_drug_id: str | None`
170
- - `rationale: str`
171
- - `step_index: int`
172
-
173
- Core wire models:
174
-
175
- **`PolypharmacyObservation`** (extends OpenEnv `Observation`)
176
- - `episode_id: str`
177
- - `task_id: Literal["easy_screening", "budgeted_screening", "complex_tradeoff"]`
178
- - `age: int`
179
- - `sex: str`
180
- - `conditions: list[str]`
181
- - `eGFR_category: str`
182
- - `liver_function_category: str`
183
- - `current_medications: list[MedicationEntry]`
184
- - `interaction_queries: list[InteractionQueryRecord]`
185
- - `interventions: list[InterventionRecord]`
186
- - `step_index: int`
187
- - `remaining_query_budget: int`
188
- - `remaining_intervention_budget: int`
189
- - `shaped_reward: float` # reward from last step
190
- - `done: bool`
191
-
192
- **`PolypharmacyAction`** (extends OpenEnv `Action`)
193
- - `action_type: Literal["query_ddi", "propose_intervention", "finish_review"]`
194
- - `drug_id_1: str | None` # for DDI queries or some interventions
195
- - `drug_id_2: str | None` # for DDI queries
196
- - `target_drug_id: str | None` # for interventions
197
- - `intervention_type: Literal["stop", "dose_reduce", "substitute", "add_monitoring", "none"] | None`
198
- - `proposed_new_drug_id: str | None`
199
- - `rationale: str | None`
200
-
201
- **`PolypharmacyState`** (extends OpenEnv `State`)
202
- - `episode_id: str`
203
- - `task_id: str`
204
- - `step_count: int`
205
- - `max_steps: int`
206
- - `num_query_actions: int`
207
- - `num_interventions: int`
208
-
209
- 3.2. Environment core
210
-
211
- In `env_core.py`, implement `PolypharmacyEnv` extending the appropriate OpenEnv environment base class. It must implement:
212
-
213
- **`reset(task_id: str | None = None) -> PolypharmacyObservation`**
214
- - If `task_id` is `None`, default to medium (`budgeted_screening`).
215
- - Sample an episode from `patients_polypharmacy.csv` filtered by difficulty.
216
- - Initialize:
217
- - `episode_id`
218
- - `step_count = 0`
219
- - task-specific budgets (query, interventions, max_steps)
220
- - baseline regime and risk
221
- - empty `interaction_queries` and `interventions`
222
- - Return the initial `PolypharmacyObservation` with:
223
- - `step_index = 0`
224
- - `shaped_reward = 0.0`
225
- - `done = False`
226
-
227
- **`step(action: PolypharmacyAction) -> dict`**
228
- - Validate the action; if invalid:
229
- - Apply a negative reward.
230
- - Do not modify regimen, but log error in `info`.
231
- - If `action_type == "query_ddi"`:
232
- - If query budget exhausted, apply penalty and do not query.
233
- - Else:
234
- - Use `ddi_simulator.lookup_ddi(drug_id_1, drug_id_2)` to get severity, recommendation, base_risk_score.
235
- - Append an `InteractionQueryRecord`.
236
- - Apply a small negative reward for query cost.
237
- - If `action_type == "propose_intervention"`:
238
- - If intervention budget exhausted, apply penalty and ignore change.
239
- - Else:
240
- - Update `current_medications` according to `intervention_type`:
241
- - `stop`: remove medication.
242
- - `dose_reduce`: adjust dose downward within [min_dose_mg, default_dose_mg].
243
- - `substitute`: replace with a safer alternative from same `atc_class`.
244
- - `add_monitoring`: keep drug but tag in internal state.
245
- - Append an `InterventionRecord`.
246
- - Recompute current regimen risk using the risk model (see 3.3).
247
- - Compute shaped reward = (previous_risk - new_risk) - small intervention cost.
248
- - If `action_type == "finish_review"`:
249
- - Mark `done = True`.
250
- - Call the task’s grader to get episode-level score in [0.0, 1.0].
251
- - Add this as a terminal bonus to the current step reward.
252
-
253
- - In all cases:
254
- - Increment `step_count`.
255
- - Check `max_steps`; if exceeded, auto-terminate:
256
- - `done = True`
257
- - apply time-out penalty
258
- - call grader with current trajectory for a final score if appropriate.
259
- - Construct next `PolypharmacyObservation` with updated fields.
260
- - Return a dict:
261
- - `observation`: `PolypharmacyObservation`
262
- - `reward`: float shaped reward for this step
263
- - `done`: bool
264
- - `info`: dict with fields like `current_risk`, `baseline_risk`, `grader_score_if_terminal`, and debug flags.
265
-
266
- **`state` property**
267
- - Returns `PolypharmacyState` reflecting the current internal state.
268
-
269
- 3.3. DDI simulator and risk model
270
-
271
- In `ddi_simulator.py`:
272
- - Load `ddi_rules.csv` once via `data_loader`.
273
- - Implement `lookup_ddi(drug_id_1, drug_id_2) -> tuple[severity, recommendation, base_risk_score]`:
274
- - Normalize the pair ordering.
275
- - Look up row; if missing, return:
276
- - severity = `"none"`
277
- - recommendation = `"no_action"`
278
- - base_risk_score = 0.0
279
-
280
- In `rewards.py` (or a dedicated module), implement:
281
- - `compute_regimen_risk(current_drug_ids, patient_context, ddi_rules, beers_rules, drug_metadata) -> float`
282
- - Aggregate contributions from:
283
- - Beers violations (weighted by `criterion_type` and relevant conditions).
284
- - DDI base risk scores for all present drug pairs.
285
- - High-risk elderly drugs.
286
- - Normalize and clip to [0.0, 1.0].
287
-
288
- Use this function to compute:
289
- - `baseline_risk` at episode start.
290
- - Risk after each intervention step.
291
-
292
- Also implement:
293
- - `compute_shaped_reward(previous_risk, new_risk, action, context, partial_metrics) -> float`
294
- - Positive component: `previous_risk - new_risk`.
295
- - Negative components: per-query cost, per-intervention cost, invalid-action penalty, time-out penalty.
296
-
297
- =================================================
298
- 4. Tasks and graders (3 difficulty levels)
299
- =================================================
300
-
301
- Define three task IDs and semantics in `tasks.py` and `graders.py`:
302
-
303
- Task IDs:
304
- - `easy_screening`
305
- - `budgeted_screening`
306
- - `complex_tradeoff`
307
-
308
- 4.1. `easy_screening` (easy)
309
-
310
- - Small regimen: 3–5 drugs.
311
- - Exactly one **severe** DDI pair and possibly one simple Beers violation.
312
- - Budgets:
313
- - query_budget ≈ 4
314
- - intervention_budget ≈ 2
315
- - max_steps ≈ 10
316
-
317
- Grader:
318
- - Input: full trajectory, baseline risk, final risk, list of interventions.
319
- - Compute:
320
- - `risk_reduction = max(0.0, baseline_risk - final_risk) / max(baseline_risk, ε)` (normalized).
321
- - `targeted_intervention_flag = 1.0` if at least one intervention affects one of the drugs in the known severe DDI pair, else 0.0.
322
- - Score:
323
- - `score = 0.5 * risk_reduction + 0.5 * targeted_intervention_flag`
324
- - Clip to [0.0, 1.0].
325
-
326
- 4.2. `budgeted_screening` (medium)
327
-
328
- - Medium regimen: 6–10 drugs.
329
- - Multiple DDIs (mild/moderate/severe) and multiple Beers issues.
330
- - Budgets:
331
- - query_budget ≈ 8
332
- - intervention_budget ≈ 3
333
- - max_steps ≈ 20
334
-
335
- Grader:
336
- - Compute:
337
- - `risk_reduction_score` as normalized risk drop.
338
- - `intervention_precision_score` = fraction of interventions that actually reduce risk or fix guideline violations.
339
- - `query_efficiency_score` = (number of severe/moderate DDIs discovered) / (number of queries used), normalized.
340
- - Weighted score, for example:
341
- - `score = 0.5 * risk_reduction_score + 0.3 * intervention_precision_score + 0.2 * query_efficiency_score`
342
- - Clip to [0.0, 1.0].
343
-
344
- 4.3. `complex_tradeoff` (hard)
345
-
346
- - Larger regimen: 10–15 drugs.
347
- - Some drugs are **clinically critical** (e.g., anticoagulants, insulin analogues) and encoded as such in `drug_metadata` or a small internal map.
348
- - Episodes contain:
349
- - multiple DDIs and Beers issues, including ones involving critical drugs.
350
- - safer substitutes for some risky drugs.
351
-
352
- Budgets:
353
- - query_budget ≈ 12
354
- - intervention_budget ≈ 5
355
- - max_steps ≈ 30
356
-
357
- Grader adds a **regimen disruption penalty** component:
358
- - Metrics:
359
- - `risk_reduction_score` (as above).
360
- - `critical_drug_penalty` = penalty if a critical drug is stopped without substitution to another suitable agent.
361
- - `total_drug_changes` = number of drugs stopped or substituted.
362
- - `regimen_disruption_penalty` derived from `total_drug_changes` and `critical_drug_penalty`.
363
-
364
- Example scoring:
365
- - `base = risk_reduction_score`
366
- - `penalty = α * regimen_disruption_penalty`
367
- - `score = clamp(base - penalty, 0.0, 1.0)`
368
-
369
- 4.4. Reward shaping
370
-
371
- In `rewards.py`, define a consistent shaping scheme:
372
- - On each query:
373
- - Small negative reward (e.g., −0.01) plus any small bonus if it discovers a severe DDI, if desired.
374
- - On each intervention:
375
- - Reward ≈ (previous_risk - new_risk) − small intervention cost.
376
- - On invalid actions:
377
- - Larger negative reward (e.g., −0.1) and no state change.
378
- - On `finish_review`:
379
- - Add the task-level `score` ∈ [0.0, 1.0] from the corresponding grader to that step’s shaped reward.
380
-
381
- Ensure the sum of step rewards per episode remains in a reasonable numeric range (e.g., roughly -5 to +5) while still allowing meaningful differentiation by graders.
382
-
383
- =================================================
384
- 5. HTTP API server and openenv.yaml
385
- =================================================
386
-
387
- 5.1. HTTP server (FastAPI)
388
-
389
- In `api/server.py`:
390
- - Implement a FastAPI app that maintains a `PolypharmacyEnv` instance (or a multiplexing scheme if needed).
391
- - Endpoints:
392
- - `POST /reset`:
393
- - Request body: may include `task_id` (string).
394
- - Response: serialized `PolypharmacyObservation`.
395
- - `POST /step`:
396
- - Request body: serialized `PolypharmacyAction`.
397
- - Response: dict with:
398
- - `observation`: `PolypharmacyObservation`
399
- - `reward`: float
400
- - `done`: bool
401
- - `info`: dict
402
- - `GET /state`:
403
- - Response: `PolypharmacyState`.
404
-
405
- Provide a module-level `app = FastAPI(...)` object for use with uvicorn and Hugging Face Spaces. Ensure the JSON schema is consistent with OpenEnv clients (simple, flat JSON for observation/action/state).
406
-
407
- 5.2. `openenv.yaml`
408
-
409
- At repo root, define `openenv.yaml` consistent with the latest OpenEnv spec. At minimum, include:
410
- - `name`: `polypharmacy_env`
411
- - `version`: e.g., `0.1.0`
412
- - `description`: human-readable description.
413
- - `author`: your details.
414
- - `tags`: e.g., `["healthcare", "polypharmacy", "openenv"]`
415
- - `tasks`:
416
- - One entry per task:
417
- - `id`: `"easy_screening"` / `"budgeted_screening"` / `"complex_tradeoff"`
418
- - `description`: one-line description
419
- - `difficulty`: `"easy"`, `"medium"`, `"hard"`
420
-
421
- Ensure `openenv validate` (or equivalent validator) passes once implemented.
422
-
423
- =================================================
424
- 6. Baseline heuristic (non-LLM) agent
425
- =================================================
426
-
427
- In `baselines/heuristic_agent.py`, implement a simple, deterministic baseline agent that:
428
-
429
- For each episode:
430
- - Iterates through all unordered medication pairs within query budget:
431
- - Calls `query_ddi` via the environment for each pair until the query budget is exhausted or all pairs are examined.
432
- - Records severe and moderate interactions.
433
- - After querying:
434
- - For each severe DDI pair:
435
- - Try `substitute` one of the drugs using `drug_metadata`:
436
- - Prefer substitute within same `atc_class` that:
437
- - is not marked high-risk elderly.
438
- - does not participate in known severe DDIs with the rest of the regimen.
439
- - If no substitute exists, propose `stop` for the higher-risk drug.
440
- - Respect intervention budget limits.
441
- - Finally, call `finish_review`.
442
-
443
- This baseline should be callable as a simple Python function that interacts with `PolypharmacyEnv` directly (without HTTP).
444
-
445
- =================================================
446
- 7. Baseline LLM inference script (inference.py)
447
- =================================================
448
-
449
- At repo root, create `inference.py` that:
450
-
451
- 7.1. Uses the OpenAI Python client
452
-
453
- - Import and configure the official OpenAI Python client.
454
- - Read environment variables:
455
- - `OPENAI_API_KEY` (required).
456
- - `API_BASE_URL` (base URL for LLM; default to OpenAI standard if not set).
457
- - `MODEL_NAME` (e.g., `gpt-4.1` or similar).
458
- - `HF_TOKEN` (if needed for HF auth; do not hardcode).
459
- - Read `POLYPHARMACY_ENV_URL` (or similar) for the environment’s HTTP base URL.
460
-
461
- 7.2. Implements the required logging format
462
-
463
- - For each **run** across all tasks:
464
- - Emit a `[START]` line with a JSON payload exactly matching the evaluation specification:
465
- - Fields such as `run_id`, `task_id`, `model`, etc., in the same order and naming as the sample OpenEnv inference script.
466
- - For each **step** in an episode:
467
- - Emit a `[STEP]` line with JSON fields including:
468
- - `run_id`
469
- - `task_id`
470
- - `episode_id`
471
- - `step_index`
472
- - `observation_summary` (brief, machine-readable summary)
473
- - `action_payload` (the action sent to the env)
474
- - `reward`
475
- - `done`
476
- - After finishing an episode for a task:
477
- - Emit an `[END]` line summarizing:
478
- - `run_id`
479
- - `task_id`
480
- - per-episode statistics (e.g., total reward, grader score from last step’s `info`).
481
- - The stdout format MUST follow the sample exactly:
482
- - Same tags: `[START]`, `[STEP]`, `[END]`.
483
- - Same JSON field names and ordering as the provided reference.
484
- - No extra prints except these structured logs (and necessary error messages to stderr).
485
-
486
- 7.3. LLM agent loop
487
-
488
- - For each task (`easy_screening`, `budgeted_screening`, `complex_tradeoff`):
489
- - Run a fixed small number of episodes (e.g., 5–10 per task) for baseline scoring.
490
- - For each episode:
491
- - Call `/reset` with the task id.
492
- - At each step:
493
- - Summarize the observation into a concise prompt for the LLM:
494
- - Include age, sex, conditions, high-risk flags, budgets, and a compressed view of meds and previous actions.
495
- - Ask the model to output a **strict JSON** representing `PolypharmacyAction` fields.
496
- - Parse and validate the JSON; if invalid, fall back to a safe default (e.g., `finish_review` or a no-op) and penalize in evaluation.
497
- - Send this action to `/step` and log `[STEP]`.
498
- - End when `done=True` or max_steps is reached.
499
- - At the end, print aggregate scores per task and overall.
500
-
501
- Make sure runtime < 20 minutes and that the script can run within 2 vCPUs and 8 GB RAM.
502
-
503
- =================================================
504
- 8. Dockerfile and Hugging Face Space
505
- =================================================
506
-
507
- 8.1. Dockerfile
508
-
509
- Create a `Dockerfile` that:
510
- - Starts from a slim Python image (e.g., `python:3.11-slim`).
511
- - Installs system dependencies as needed (e.g., `build-essential`, `curl`).
512
- - Copies the project into the container.
513
- - Installs Python dependencies from `requirements.txt`.
514
- - Sets appropriate environment variables for the app (e.g., `PORT=7860`).
515
- - Exposes port 7860.
516
- - Uses a `CMD` or `ENTRYPOINT` that runs the FastAPI server, for example:
517
- - `uvicorn polypharmacy_env.api.server:app --host 0.0.0.0 --port 7860`
518
-
519
- 8.2. Hugging Face Space
520
-
521
- Ensure the repository is ready to be used as a Hugging Face Space:
522
- - Space type: `docker`.
523
- - Tag: `openenv`.
524
- - On container start, the server must listen on the correct port and respond to:
525
- - `POST /reset`
526
- - `POST /step`
527
- - `GET /state`
528
- - The environment must start cleanly with `docker build` + `docker run` locally.
529
-
530
- =================================================
531
- 9. README and documentation
532
- =================================================
533
-
534
- In `README.md`, include:
535
-
536
- - **Environment description & motivation**:
537
- - What PolypharmacyEnv simulates.
538
- - Why elderly polypharmacy safety matters.
539
- - **Action and observation spaces**:
540
- - Describe `PolypharmacyAction`, `PolypharmacyObservation`, and `PolypharmacyState` fields and semantics.
541
- - **Task descriptions**:
542
- - `easy_screening`, `budgeted_screening`, `complex_tradeoff`, their difficulty and goals.
543
- - **Reward structure**:
544
- - Summarize shaping and terminal rewards.
545
- - **Setup & usage**:
546
- - How to install dependencies.
547
- - How to run the API server locally (uvicorn command).
548
- - How to run the heuristic baseline.
549
- - How to run `inference.py` with environment variables.
550
- - **Baseline scores**:
551
- - Document reproducible baseline scores for each task (heuristic agent, and LLM baseline if available).
552
-
553
- =================================================
554
- 10. Validation and quality gates
555
- =================================================
556
-
557
- - Ensure:
558
- - `openenv.yaml` and the HTTP server pass the OpenEnv validation script.
559
- - `docker build` and `docker run` work without errors.
560
- - `inference.py` completes under 20 minutes, within 2 vCPUs / 8 GB RAM.
561
- - All graders:
562
- - Are deterministic.
563
- - Return scores strictly in [0.0, 1.0].
564
- - No grader returns a constant score irrespective of behavior.
565
-
566
- Aim for clean, well-structured, well-documented code with clear separation of concerns between:
567
- - Data loading,
568
- - Environment state & dynamics,
569
- - Reward/grade logic,
570
- - HTTP serving,
571
- - Baseline agents and inference.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv-polypharmacy/frontend/src/App.jsx CHANGED
@@ -1,7 +1,22 @@
1
  import { useEffect, useMemo, useRef, useState } from "react";
2
 
3
- const API_BASE = "http://localhost:7860";
4
- const WS_URL = "ws://localhost:7860/ws";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  const TASKS = ["easy_screening", "budgeted_screening", "complex_tradeoff"];
6
 
7
  async function apiPost(path, body) {
@@ -225,6 +240,7 @@ export default function App() {
225
  <header className="topbar glass">
226
  <div className="title-wrap">
227
  <h1>Polypharmacy Control Center</h1>
 
228
  </div>
229
  <div className={`status-chip ${hasValidEpisode ? "live" : "idle"}`}>
230
  {hasValidEpisode ? "Session Live" : "Waiting for reset"}
 
1
  import { useEffect, useMemo, useRef, useState } from "react";
2
 
3
+ function resolveApiBase() {
4
+ const explicitBase = import.meta.env.VITE_API_BASE;
5
+ if (explicitBase) return explicitBase.replace(/\/$/, "");
6
+
7
+ const host = window.location.hostname;
8
+ const isLocal =
9
+ host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0";
10
+
11
+ // In local Vite dev, backend runs on :7860. In Spaces/prod, serve same-origin.
12
+ if (isLocal && window.location.port === "5173") {
13
+ return "http://localhost:7860";
14
+ }
15
+ return window.location.origin.replace(/\/$/, "");
16
+ }
17
+
18
+ const API_BASE = resolveApiBase();
19
+ const WS_URL = `${API_BASE.replace(/^http/, "ws")}/ws`;
20
  const TASKS = ["easy_screening", "budgeted_screening", "complex_tradeoff"];
21
 
22
  async function apiPost(path, body) {
 
240
  <header className="topbar glass">
241
  <div className="title-wrap">
242
  <h1>Polypharmacy Control Center</h1>
243
+ <p>Metaverse Clinical Ops Console</p>
244
  </div>
245
  <div className={`status-chip ${hasValidEpisode ? "live" : "idle"}`}>
246
  {hasValidEpisode ? "Session Live" : "Waiting for reset"}
openenv-polypharmacy/frontend/src/styles.css CHANGED
@@ -1,15 +1,18 @@
1
  :root {
2
- --bg: #eef5ff;
3
- --panel: rgba(255, 255, 255, 0.82);
4
- --panel-solid: #ffffff;
5
- --text: #0b2445;
6
- --muted: #5b7596;
7
- --primary: #1f8bff;
8
- --primary-2: #69beff;
9
- --accent: #0dd3ff;
10
- --border: rgba(93, 156, 219, 0.22);
11
- --shadow: 0 20px 50px rgba(25, 83, 143, 0.12);
12
- --shadow-strong: 0 20px 42px rgba(31, 112, 182, 0.24);
 
 
 
13
  }
14
 
15
  * {
@@ -18,156 +21,197 @@
18
 
19
  body {
20
  margin: 0;
21
- font-family: "Inter", "SF Pro Text", "Segoe UI", sans-serif;
22
- background:
23
- radial-gradient(circle at 8% 0%, #cce7ff 0%, rgba(204, 231, 255, 0) 42%),
24
- radial-gradient(circle at 92% 100%, #d5efff 0%, rgba(213, 239, 255, 0) 42%),
25
- var(--bg);
26
  color: var(--text);
 
 
 
 
 
 
 
27
  }
28
 
29
  .shell {
30
  min-height: 100vh;
31
  position: relative;
32
- padding: 28px 22px;
33
  overflow: hidden;
 
34
  }
35
 
36
  .container {
37
- width: min(1300px, 100%);
38
  margin: 0 auto;
39
  position: relative;
40
- z-index: 1;
41
  }
42
 
43
  .bg-orb {
44
  position: absolute;
45
  border-radius: 50%;
46
- filter: blur(18px);
47
  opacity: 0.9;
 
48
  }
 
49
  .orb-a {
50
- width: 420px;
51
- height: 420px;
52
- right: -120px;
53
- top: -100px;
54
- background: radial-gradient(circle, rgba(72, 168, 255, 0.5), rgba(72, 168, 255, 0.1));
55
  }
 
56
  .orb-b {
57
- width: 360px;
58
- height: 360px;
59
- left: -100px;
60
- bottom: -120px;
61
- background: radial-gradient(circle, rgba(110, 200, 255, 0.4), rgba(141, 205, 255, 0.06));
62
  }
63
 
64
  .glass {
65
- backdrop-filter: blur(12px);
 
 
66
  border: 1px solid var(--border);
67
- background: var(--panel);
68
  box-shadow: var(--shadow);
 
69
  }
70
 
71
  .topbar {
72
- border-radius: 20px;
73
- padding: 18px;
74
  display: grid;
75
- grid-template-columns: 1.2fr auto 1fr;
76
- justify-content: space-between;
77
  align-items: center;
78
- gap: 12px;
79
  }
80
 
81
  .title-wrap h1 {
82
  margin: 0;
83
- font-size: clamp(1.1rem, 1.5vw, 1.45rem);
84
- letter-spacing: 0.01em;
 
 
85
  }
86
 
87
  .title-wrap p {
88
- margin: 4px 0 0;
 
89
  color: var(--muted);
90
- font-size: 0.92rem;
 
91
  }
92
 
93
  .status-chip {
94
  justify-self: center;
 
95
  border-radius: 999px;
96
- padding: 7px 12px;
97
- font-size: 0.76rem;
98
  font-weight: 700;
99
- letter-spacing: 0.04em;
100
  text-transform: uppercase;
101
  border: 1px solid transparent;
102
  }
103
 
104
  .status-chip.live {
105
- color: #0d6a3f;
106
- background: rgba(130, 245, 195, 0.18);
107
- border-color: rgba(70, 199, 142, 0.3);
108
  }
109
 
110
  .status-chip.idle {
111
- color: #24527f;
112
- background: rgba(114, 194, 255, 0.18);
113
- border-color: rgba(62, 152, 223, 0.28);
114
  }
115
 
116
  .actions {
117
  display: flex;
118
  justify-content: flex-end;
119
- gap: 10px;
120
  flex-wrap: wrap;
 
121
  }
122
 
123
  button,
124
  select,
125
  input {
126
- border: 1px solid var(--border);
 
127
  border-radius: 12px;
128
- padding: 10px 13px;
129
  font-size: 0.92rem;
130
- background: #fff;
131
  color: var(--text);
132
- min-height: 42px;
 
 
 
 
 
 
 
 
 
 
 
 
133
  }
134
 
135
  button {
136
  cursor: pointer;
137
- background: linear-gradient(135deg, var(--primary) 0%, var(--primary-2) 78%, var(--accent) 100%);
138
- color: #fff;
139
- border: none;
140
  font-weight: 700;
 
 
141
  box-shadow: var(--shadow-strong);
142
- transition: transform 120ms ease, filter 120ms ease;
143
  }
144
 
145
  button:hover {
146
  transform: translateY(-1px);
147
- filter: brightness(1.02);
 
 
 
 
 
148
  }
149
 
150
  button.secondary {
151
- background: linear-gradient(135deg, #68c2ff, #9dd9ff);
152
  }
153
 
154
  button:disabled {
155
- opacity: 0.58;
156
  cursor: not-allowed;
 
 
157
  transform: none;
158
  }
159
 
160
  .layout {
161
- margin-top: 18px;
162
  display: grid;
163
- gap: 16px;
164
- grid-template-columns: 1.15fr 0.85fr;
165
  align-items: start;
166
  }
167
 
168
  .panel {
169
- border-radius: 18px;
170
- padding: 18px;
 
 
 
 
 
 
 
 
 
 
171
  }
172
 
173
  .panel-wide {
@@ -177,31 +221,36 @@ button:disabled {
177
  .panel h2 {
178
  margin: 0 0 12px;
179
  font-size: 1rem;
180
- letter-spacing: 0.01em;
 
 
181
  }
182
 
183
  .kpi-grid {
184
  display: grid;
 
185
  grid-template-columns: repeat(3, minmax(0, 1fr));
186
- gap: 12px;
187
  }
188
 
189
  .kpi-grid div {
190
- background: rgba(255, 255, 255, 0.9);
191
  border: 1px solid var(--border);
192
- border-radius: 14px;
193
- padding: 12px;
194
  }
195
 
196
  .kpi-grid span {
197
  display: block;
198
- font-size: 0.74rem;
199
- color: var(--muted);
200
  margin-bottom: 4px;
 
 
 
 
201
  }
202
 
203
  .kpi-grid strong {
204
- font-size: 1.05rem;
 
205
  }
206
 
207
  .action-row,
@@ -211,6 +260,13 @@ button:disabled {
211
  margin-bottom: 12px;
212
  }
213
 
 
 
 
 
 
 
 
214
  .stack-two {
215
  grid-template-columns: repeat(2, minmax(0, 1fr));
216
  }
@@ -219,22 +275,22 @@ button:disabled {
219
  display: grid;
220
  grid-template-columns: repeat(3, minmax(0, 1fr));
221
  gap: 10px;
222
- max-height: 420px;
223
  overflow: auto;
224
- padding-right: 2px;
225
  }
226
 
227
  .med-card {
228
- border: 1px solid var(--border);
229
  border-radius: 14px;
230
- padding: 12px;
231
  background: var(--panel-solid);
232
- transition: transform 120ms ease, box-shadow 120ms ease;
 
233
  }
234
 
235
  .med-card:hover {
236
  transform: translateY(-1px);
237
- box-shadow: 0 10px 25px rgba(44, 105, 165, 0.12);
238
  }
239
 
240
  .med-card p {
@@ -243,37 +299,42 @@ button:disabled {
243
  text-transform: capitalize;
244
  }
245
 
 
 
 
 
246
  .logs {
247
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
248
- font-size: 0.85rem;
249
  max-height: 300px;
250
  overflow: auto;
 
251
  display: grid;
252
- gap: 6px;
253
- padding-right: 2px;
 
254
  }
255
 
256
  .logs div {
257
- background: rgba(255, 255, 255, 0.78);
258
- border: 1px solid var(--border);
259
  border-radius: 10px;
 
 
260
  padding: 8px 10px;
 
261
  }
262
 
263
  .muted {
264
- color: var(--muted);
265
  margin: 0;
 
266
  }
267
 
268
  .budget-note {
269
  margin-top: 10px;
270
- padding: 10px 12px;
271
  border: 1px solid var(--border);
272
  border-radius: 12px;
273
- background: rgba(255, 255, 255, 0.78);
 
274
  }
275
 
276
- @media (max-width: 1120px) {
277
  .layout {
278
  grid-template-columns: 1fr;
279
  }
@@ -293,7 +354,21 @@ button:disabled {
293
 
294
  @media (max-width: 760px) {
295
  .shell {
296
- padding: 18px 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  }
298
 
299
  .kpi-grid,
@@ -301,4 +376,8 @@ button:disabled {
301
  .stack-two {
302
  grid-template-columns: 1fr;
303
  }
 
 
 
 
304
  }
 
1
  :root {
2
+ --bg: #070814;
3
+ --bg-layer: #0a1026;
4
+ --panel: rgba(14, 22, 44, 0.72);
5
+ --panel-solid: rgba(20, 28, 52, 0.92);
6
+ --text: #e8f1ff;
7
+ --muted: #9ab2db;
8
+ --primary: #37d4ff;
9
+ --primary-2: #5a8dff;
10
+ --accent: #9d59ff;
11
+ --success: #6dfbcf;
12
+ --border: rgba(122, 162, 255, 0.28);
13
+ --line: rgba(109, 143, 225, 0.18);
14
+ --shadow: 0 16px 45px rgba(5, 8, 23, 0.6);
15
+ --shadow-strong: 0 14px 32px rgba(44, 105, 255, 0.4);
16
  }
17
 
18
  * {
 
21
 
22
  body {
23
  margin: 0;
 
 
 
 
 
24
  color: var(--text);
25
+ font-family: "Segoe UI", "SF Pro Text", "Helvetica Neue", sans-serif;
26
+ background:
27
+ radial-gradient(circle at 8% 12%, rgba(121, 87, 255, 0.22), transparent 38%),
28
+ radial-gradient(circle at 88% 20%, rgba(59, 204, 255, 0.26), transparent 34%),
29
+ radial-gradient(circle at 50% 100%, rgba(43, 128, 255, 0.26), transparent 40%),
30
+ linear-gradient(145deg, var(--bg) 0%, var(--bg-layer) 60%, #04060f 100%);
31
+ background-attachment: fixed;
32
  }
33
 
34
  .shell {
35
  min-height: 100vh;
36
  position: relative;
 
37
  overflow: hidden;
38
+ padding: 24px 16px 34px;
39
  }
40
 
41
  .container {
42
+ width: min(1320px, 100%);
43
  margin: 0 auto;
44
  position: relative;
45
+ z-index: 2;
46
  }
47
 
48
  .bg-orb {
49
  position: absolute;
50
  border-radius: 50%;
51
+ pointer-events: none;
52
  opacity: 0.9;
53
+ filter: blur(18px);
54
  }
55
+
56
  .orb-a {
57
+ width: min(46vw, 530px);
58
+ aspect-ratio: 1 / 1;
59
+ right: -9%;
60
+ top: -10%;
61
+ background: radial-gradient(circle, rgba(52, 203, 255, 0.35), rgba(52, 203, 255, 0.04) 70%);
62
  }
63
+
64
  .orb-b {
65
+ width: min(40vw, 460px);
66
+ aspect-ratio: 1 / 1;
67
+ left: -9%;
68
+ bottom: -15%;
69
+ background: radial-gradient(circle, rgba(160, 102, 255, 0.3), rgba(160, 102, 255, 0.06) 72%);
70
  }
71
 
72
  .glass {
73
+ background:
74
+ linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.01)),
75
+ var(--panel);
76
  border: 1px solid var(--border);
 
77
  box-shadow: var(--shadow);
78
+ backdrop-filter: blur(12px);
79
  }
80
 
81
  .topbar {
82
+ border-radius: 24px;
83
+ padding: clamp(14px, 2vw, 20px);
84
  display: grid;
85
+ gap: 12px 16px;
86
+ grid-template-columns: minmax(220px, 1.2fr) auto minmax(280px, 1fr);
87
  align-items: center;
 
88
  }
89
 
90
  .title-wrap h1 {
91
  margin: 0;
92
+ font-size: clamp(1.15rem, 2.2vw, 1.95rem);
93
+ letter-spacing: 0.02em;
94
+ text-transform: uppercase;
95
+ text-shadow: 0 0 16px rgba(106, 192, 255, 0.3);
96
  }
97
 
98
  .title-wrap p {
99
+ margin: 6px 0 0;
100
+ font-size: 0.84rem;
101
  color: var(--muted);
102
+ letter-spacing: 0.03em;
103
+ text-transform: uppercase;
104
  }
105
 
106
  .status-chip {
107
  justify-self: center;
108
+ padding: 7px 14px;
109
  border-radius: 999px;
110
+ font-size: 0.72rem;
 
111
  font-weight: 700;
112
+ letter-spacing: 0.08em;
113
  text-transform: uppercase;
114
  border: 1px solid transparent;
115
  }
116
 
117
  .status-chip.live {
118
+ color: #052c24;
119
+ background: linear-gradient(90deg, rgba(126, 255, 220, 0.9), rgba(84, 244, 196, 0.95));
120
+ box-shadow: 0 0 14px rgba(96, 244, 198, 0.36);
121
  }
122
 
123
  .status-chip.idle {
124
+ color: #d8e8ff;
125
+ border-color: rgba(117, 186, 255, 0.48);
126
+ background: rgba(60, 106, 198, 0.25);
127
  }
128
 
129
  .actions {
130
  display: flex;
131
  justify-content: flex-end;
 
132
  flex-wrap: wrap;
133
+ gap: 10px;
134
  }
135
 
136
  button,
137
  select,
138
  input {
139
+ width: 100%;
140
+ min-height: 42px;
141
  border-radius: 12px;
142
+ border: 1px solid var(--border);
143
  font-size: 0.92rem;
144
+ padding: 10px 12px;
145
  color: var(--text);
146
+ background: rgba(11, 19, 38, 0.84);
147
+ }
148
+
149
+ select,
150
+ input {
151
+ transition: border-color 120ms ease, box-shadow 120ms ease;
152
+ }
153
+
154
+ select:focus,
155
+ input:focus {
156
+ outline: none;
157
+ border-color: rgba(119, 200, 255, 0.88);
158
+ box-shadow: 0 0 0 2px rgba(95, 187, 255, 0.18);
159
  }
160
 
161
  button {
162
  cursor: pointer;
163
+ border: 0;
164
+ width: auto;
 
165
  font-weight: 700;
166
+ letter-spacing: 0.02em;
167
+ background: linear-gradient(135deg, var(--primary), var(--primary-2) 55%, var(--accent));
168
  box-shadow: var(--shadow-strong);
169
+ transition: transform 140ms ease, filter 140ms ease, box-shadow 140ms ease;
170
  }
171
 
172
  button:hover {
173
  transform: translateY(-1px);
174
+ filter: brightness(1.04);
175
+ box-shadow: 0 18px 32px rgba(50, 141, 255, 0.48);
176
+ }
177
+
178
+ button:active {
179
+ transform: translateY(0);
180
  }
181
 
182
  button.secondary {
183
+ background: linear-gradient(135deg, rgba(95, 185, 255, 0.9), rgba(154, 102, 255, 0.86));
184
  }
185
 
186
  button:disabled {
187
+ opacity: 0.56;
188
  cursor: not-allowed;
189
+ filter: grayscale(0.2);
190
+ box-shadow: none;
191
  transform: none;
192
  }
193
 
194
  .layout {
195
+ margin-top: 16px;
196
  display: grid;
197
+ gap: 14px;
198
+ grid-template-columns: 1.12fr 0.88fr;
199
  align-items: start;
200
  }
201
 
202
  .panel {
203
+ border-radius: 20px;
204
+ padding: clamp(14px, 1.8vw, 20px);
205
+ position: relative;
206
+ }
207
+
208
+ .panel::after {
209
+ content: "";
210
+ position: absolute;
211
+ inset: 0;
212
+ border-radius: inherit;
213
+ border: 1px solid var(--line);
214
+ pointer-events: none;
215
  }
216
 
217
  .panel-wide {
 
221
  .panel h2 {
222
  margin: 0 0 12px;
223
  font-size: 1rem;
224
+ font-weight: 700;
225
+ letter-spacing: 0.05em;
226
+ text-transform: uppercase;
227
  }
228
 
229
  .kpi-grid {
230
  display: grid;
231
+ gap: 10px;
232
  grid-template-columns: repeat(3, minmax(0, 1fr));
 
233
  }
234
 
235
  .kpi-grid div {
236
+ border-radius: 13px;
237
  border: 1px solid var(--border);
238
+ background: var(--panel-solid);
239
+ padding: 11px 12px;
240
  }
241
 
242
  .kpi-grid span {
243
  display: block;
 
 
244
  margin-bottom: 4px;
245
+ font-size: 0.72rem;
246
+ color: var(--muted);
247
+ text-transform: uppercase;
248
+ letter-spacing: 0.05em;
249
  }
250
 
251
  .kpi-grid strong {
252
+ font-size: 1.06rem;
253
+ line-height: 1.2;
254
  }
255
 
256
  .action-row,
 
260
  margin-bottom: 12px;
261
  }
262
 
263
+ .action-row label {
264
+ color: var(--muted);
265
+ font-size: 0.78rem;
266
+ letter-spacing: 0.05em;
267
+ text-transform: uppercase;
268
+ }
269
+
270
  .stack-two {
271
  grid-template-columns: repeat(2, minmax(0, 1fr));
272
  }
 
275
  display: grid;
276
  grid-template-columns: repeat(3, minmax(0, 1fr));
277
  gap: 10px;
278
+ max-height: 430px;
279
  overflow: auto;
280
+ padding-right: 4px;
281
  }
282
 
283
  .med-card {
 
284
  border-radius: 14px;
285
+ border: 1px solid var(--border);
286
  background: var(--panel-solid);
287
+ padding: 11px 12px;
288
+ transition: transform 130ms ease, border-color 130ms ease;
289
  }
290
 
291
  .med-card:hover {
292
  transform: translateY(-1px);
293
+ border-color: rgba(109, 224, 255, 0.72);
294
  }
295
 
296
  .med-card p {
 
299
  text-transform: capitalize;
300
  }
301
 
302
+ .med-card small {
303
+ color: #c7d9ff;
304
+ }
305
+
306
  .logs {
 
 
307
  max-height: 300px;
308
  overflow: auto;
309
+ padding-right: 4px;
310
  display: grid;
311
+ gap: 7px;
312
+ font-size: 0.84rem;
313
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
314
  }
315
 
316
  .logs div {
 
 
317
  border-radius: 10px;
318
+ border: 1px solid var(--border);
319
+ background: rgba(10, 16, 31, 0.84);
320
  padding: 8px 10px;
321
+ color: #dbebff;
322
  }
323
 
324
  .muted {
 
325
  margin: 0;
326
+ color: var(--muted);
327
  }
328
 
329
  .budget-note {
330
  margin-top: 10px;
 
331
  border: 1px solid var(--border);
332
  border-radius: 12px;
333
+ padding: 10px 12px;
334
+ background: rgba(13, 22, 42, 0.82);
335
  }
336
 
337
+ @media (max-width: 1180px) {
338
  .layout {
339
  grid-template-columns: 1fr;
340
  }
 
354
 
355
  @media (max-width: 760px) {
356
  .shell {
357
+ padding: 14px 10px 24px;
358
+ }
359
+
360
+ .topbar,
361
+ .panel {
362
+ border-radius: 16px;
363
+ }
364
+
365
+ .actions {
366
+ width: 100%;
367
+ }
368
+
369
+ .actions button,
370
+ .actions select {
371
+ width: 100%;
372
  }
373
 
374
  .kpi-grid,
 
376
  .stack-two {
377
  grid-template-columns: 1fr;
378
  }
379
+
380
+ .logs {
381
+ max-height: 240px;
382
+ }
383
  }