Spaces:
Sleeping
Sleeping
Merge branch 'main' of https://github.com/LO-Kyu/gridmind
Browse files- Dockerfile +15 -13
- README.md +15 -0
- dashboard/static/dashboard.js +2 -2
- dashboard/static/index.html +4 -4
- main.go +114 -3
- python/inference.py +2 -0
Dockerfile
CHANGED
|
@@ -32,9 +32,14 @@ COPY dashboard/ ./dashboard/
|
|
| 32 |
COPY data/ ./data/
|
| 33 |
COPY openenv.yaml ./
|
| 34 |
|
| 35 |
-
# Configure Supervisor
|
| 36 |
-
RUN echo "[
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
echo "nodaemon=true" >> /etc/supervisor/conf.d/supervisord.conf && \
|
|
|
|
| 38 |
echo "[program:go-env]" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 39 |
echo "command=/usr/local/bin/gridmind-server" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 40 |
echo "environment=PORT=7860" >> /etc/supervisor/conf.d/supervisord.conf && \
|
|
@@ -42,6 +47,7 @@ RUN echo "[supervisord]" > /etc/supervisor/conf.d/supervisord.conf && \
|
|
| 42 |
echo "stdout_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 43 |
echo "stderr_logfile=/dev/stderr" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 44 |
echo "stderr_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf && \
|
|
|
|
| 45 |
echo "[program:dashboard]" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 46 |
echo "command=python -m uvicorn dashboard.server:app --host 0.0.0.0 --port 7861" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 47 |
echo "stdout_logfile=/dev/stdout" >> /etc/supervisor/conf.d/supervisord.conf && \
|
|
@@ -49,17 +55,13 @@ RUN echo "[supervisord]" > /etc/supervisor/conf.d/supervisord.conf && \
|
|
| 49 |
echo "stderr_logfile=/dev/stderr" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 50 |
echo "stderr_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf
|
| 51 |
|
| 52 |
-
#
|
| 53 |
-
|
| 54 |
-
|
| 55 |
|
| 56 |
# Add a non-root user (good practice and required for some HF Spaces configs)
|
| 57 |
-
RUN useradd -m -u 1000 user &&
|
| 58 |
-
chown -R user:user /app && \
|
| 59 |
-
chown -R user:user /var/run/supervisor /var/log/supervisor
|
| 60 |
|
| 61 |
-
#
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
# Run supervisor as root to manage both services (required for multi-process supervision)
|
| 65 |
-
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf", "-n"]
|
|
|
|
| 32 |
COPY data/ ./data/
|
| 33 |
COPY openenv.yaml ./
|
| 34 |
|
| 35 |
+
# Configure Supervisor to use /tmp for socket and pid files (writable by any user)
|
| 36 |
+
RUN echo "[unix_http_server]" > /etc/supervisor/conf.d/supervisord.conf && \
|
| 37 |
+
echo "file=/tmp/supervisor.sock" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 38 |
+
echo "" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 39 |
+
echo "[supervisord]" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 40 |
+
echo "pidfile=/tmp/supervisord.pid" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 41 |
echo "nodaemon=true" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 42 |
+
echo "" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 43 |
echo "[program:go-env]" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 44 |
echo "command=/usr/local/bin/gridmind-server" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 45 |
echo "environment=PORT=7860" >> /etc/supervisor/conf.d/supervisord.conf && \
|
|
|
|
| 47 |
echo "stdout_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 48 |
echo "stderr_logfile=/dev/stderr" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 49 |
echo "stderr_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 50 |
+
echo "" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 51 |
echo "[program:dashboard]" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 52 |
echo "command=python -m uvicorn dashboard.server:app --host 0.0.0.0 --port 7861" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 53 |
echo "stdout_logfile=/dev/stdout" >> /etc/supervisor/conf.d/supervisord.conf && \
|
|
|
|
| 55 |
echo "stderr_logfile=/dev/stderr" >> /etc/supervisor/conf.d/supervisord.conf && \
|
| 56 |
echo "stderr_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf
|
| 57 |
|
| 58 |
+
# EXPOSE 7860 only - this is the main OpenEnv API endpoint (reverse proxy + /dashboard)
|
| 59 |
+
# Port 7861 (dashboard) runs internally only and is accessed via /dashboard proxy
|
| 60 |
+
EXPOSE 7860
|
| 61 |
|
| 62 |
# Add a non-root user (good practice and required for some HF Spaces configs)
|
| 63 |
+
RUN useradd -m -u 1000 user && chown -R user:user /app
|
|
|
|
|
|
|
| 64 |
|
| 65 |
+
# Run supervisord to manage both Go server and Python dashboard
|
| 66 |
+
# Using /tmp for socket and pid files (writable by any user, including uid 1000)
|
| 67 |
+
CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
|
|
|
|
|
README.md
CHANGED
|
@@ -21,6 +21,21 @@ license: mit
|
|
| 21 |
|
| 22 |
---
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
## Overview
|
| 25 |
|
| 26 |
GridMind-RL is a reinforcement learning environment for training and evaluating intelligent control policies in industrial building energy management. The environment simulates realistic HVAC control, thermal storage management, batch job scheduling, and demand response scenarios under stochastic electricity pricing and grid stress events.
|
|
|
|
| 21 |
|
| 22 |
---
|
| 23 |
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
## 🚀 Live Demo
|
| 27 |
+
|
| 28 |
+
| | URL |
|
| 29 |
+
|--|-----|
|
| 30 |
+
| **Environment API** | https://lo-kyu-gridmind.hf.space |
|
| 31 |
+
| **Live Dashboard** | https://lo-kyu-gridmind.hf.space/dashboard |
|
| 32 |
+
|
| 33 |
+
**Quick test:**
|
| 34 |
+
```bash
|
| 35 |
+
curl https://lo-kyu-gridmind.hf.space/health
|
| 36 |
+
curl https://lo-kyu-gridmind.hf.space/tasks
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
## Overview
|
| 40 |
|
| 41 |
GridMind-RL is a reinforcement learning environment for training and evaluating intelligent control policies in industrial building energy management. The environment simulates realistic HVAC control, thermal storage management, batch job scheduling, and demand response scenarios under stochastic electricity pricing and grid stress events.
|
dashboard/static/dashboard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
/**
|
| 2 |
* GridMind-RL Dashboard — Premium Chart.js real-time visualization
|
| 3 |
-
* Polls /api/state every 500ms and updates all charts + KPIs.
|
| 4 |
*/
|
| 5 |
|
| 6 |
'use strict';
|
|
@@ -10,7 +10,7 @@ const POLL_MS = 500;
|
|
| 10 |
const EPISODE_STEPS = 96; // 24h × 4 steps/h (15-min)
|
| 11 |
const HISTORY_LEN = EPISODE_STEPS;
|
| 12 |
const CURVE_POINTS = 24; // hourly downsample (EpisodeSteps/4)
|
| 13 |
-
const API_BASE = '/api';
|
| 14 |
const TASK_NAMES = {
|
| 15 |
1: 'Task 1 — Cost Minimization (Easy)',
|
| 16 |
2: 'Task 2 — Temperature Management (Medium)',
|
|
|
|
| 1 |
/**
|
| 2 |
* GridMind-RL Dashboard — Premium Chart.js real-time visualization
|
| 3 |
+
* Polls /dashboard/api/state every 500ms and updates all charts + KPIs.
|
| 4 |
*/
|
| 5 |
|
| 6 |
'use strict';
|
|
|
|
| 10 |
const EPISODE_STEPS = 96; // 24h × 4 steps/h (15-min)
|
| 11 |
const HISTORY_LEN = EPISODE_STEPS;
|
| 12 |
const CURVE_POINTS = 24; // hourly downsample (EpisodeSteps/4)
|
| 13 |
+
const API_BASE = '/dashboard/api';
|
| 14 |
const TASK_NAMES = {
|
| 15 |
1: 'Task 1 — Cost Minimization (Easy)',
|
| 16 |
2: 'Task 2 — Temperature Management (Medium)',
|
dashboard/static/index.html
CHANGED
|
@@ -1035,7 +1035,7 @@
|
|
| 1035 |
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
| 1036 |
Grade Episode
|
| 1037 |
</button>
|
| 1038 |
-
<button class="btn" onclick="window.open('/api/replay')">
|
| 1039 |
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
| 1040 |
Export Replay
|
| 1041 |
</button>
|
|
@@ -1047,10 +1047,10 @@
|
|
| 1047 |
|
| 1048 |
<footer>
|
| 1049 |
GridMind-RL · OpenEnv-compliant RL environment for industrial demand response ·
|
| 1050 |
-
<a href="/api/health" target="_blank">API Health</a> ·
|
| 1051 |
-
<a href="/api/metrics" target="_blank">Metrics</a>
|
| 1052 |
</footer>
|
| 1053 |
|
| 1054 |
-
<script src="/static/dashboard.js"></script>
|
| 1055 |
</body>
|
| 1056 |
</html>
|
|
|
|
| 1035 |
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
| 1036 |
Grade Episode
|
| 1037 |
</button>
|
| 1038 |
+
<button class="btn" onclick="window.open('/dashboard/api/replay')">
|
| 1039 |
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
| 1040 |
Export Replay
|
| 1041 |
</button>
|
|
|
|
| 1047 |
|
| 1048 |
<footer>
|
| 1049 |
GridMind-RL · OpenEnv-compliant RL environment for industrial demand response ·
|
| 1050 |
+
<a href="/dashboard/api/health" target="_blank">API Health</a> ·
|
| 1051 |
+
<a href="/dashboard/api/metrics" target="_blank">Metrics</a>
|
| 1052 |
</footer>
|
| 1053 |
|
| 1054 |
+
<script src="/dashboard/static/dashboard.js"></script>
|
| 1055 |
</body>
|
| 1056 |
</html>
|
main.go
CHANGED
|
@@ -8,9 +8,13 @@ import (
|
|
| 8 |
"fmt"
|
| 9 |
"log"
|
| 10 |
"math"
|
|
|
|
| 11 |
"net/http"
|
|
|
|
|
|
|
| 12 |
"os"
|
| 13 |
"strconv"
|
|
|
|
| 14 |
"sync"
|
| 15 |
"sync/atomic"
|
| 16 |
"time"
|
|
@@ -32,8 +36,8 @@ type Metrics struct {
|
|
| 32 |
rewardMin float64
|
| 33 |
rewardMax float64
|
| 34 |
// Histograms
|
| 35 |
-
actionBuckets
|
| 36 |
-
errorCount
|
| 37 |
}
|
| 38 |
|
| 39 |
var metrics = &Metrics{
|
|
@@ -134,6 +138,7 @@ func newServer() *Server {
|
|
| 134 |
|
| 135 |
func (s *Server) routes() *http.ServeMux {
|
| 136 |
mux := http.NewServeMux()
|
|
|
|
| 137 |
mux.HandleFunc("/health", s.handleHealth)
|
| 138 |
mux.HandleFunc("/ping", s.handlePing)
|
| 139 |
mux.HandleFunc("/reset", s.handleReset)
|
|
@@ -143,9 +148,63 @@ func (s *Server) routes() *http.ServeMux {
|
|
| 143 |
mux.HandleFunc("/grade", s.handleGrade)
|
| 144 |
mux.HandleFunc("/tasks", s.handleTasks)
|
| 145 |
mux.HandleFunc("/metrics", s.handleMetrics)
|
|
|
|
|
|
|
|
|
|
| 146 |
return mux
|
| 147 |
}
|
| 148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
// ── /health ──────────────────────────────────────────────────────────────────
|
| 150 |
|
| 151 |
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|
@@ -333,6 +392,58 @@ func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) {
|
|
| 333 |
fmt.Fprint(w, metrics.prometheus())
|
| 334 |
}
|
| 335 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
// ──────────────────────────────────────────────
|
| 337 |
// Entry point
|
| 338 |
// ──────────────────────────────────────────────
|
|
@@ -354,7 +465,7 @@ func main() {
|
|
| 354 |
srv.envMgr.Reset(env.ResetRequest{Seed: &seed, TaskID: 1, NumBuildings: 1})
|
| 355 |
|
| 356 |
log.Printf("GridMind-RL environment server starting on :%s", port)
|
| 357 |
-
log.Printf("Endpoints: GET /health /ping /state /replay /grade /tasks /metrics | POST /reset /step")
|
| 358 |
|
| 359 |
mux := withCORS(withLogging(srv.routes()))
|
| 360 |
if err := http.ListenAndServe(":"+port, mux); err != nil {
|
|
|
|
| 8 |
"fmt"
|
| 9 |
"log"
|
| 10 |
"math"
|
| 11 |
+
"net"
|
| 12 |
"net/http"
|
| 13 |
+
"net/http/httputil"
|
| 14 |
+
"net/url"
|
| 15 |
"os"
|
| 16 |
"strconv"
|
| 17 |
+
"strings"
|
| 18 |
"sync"
|
| 19 |
"sync/atomic"
|
| 20 |
"time"
|
|
|
|
| 36 |
rewardMin float64
|
| 37 |
rewardMax float64
|
| 38 |
// Histograms
|
| 39 |
+
actionBuckets map[string]int64 // hvac bucket counts
|
| 40 |
+
errorCount int64
|
| 41 |
}
|
| 42 |
|
| 43 |
var metrics = &Metrics{
|
|
|
|
| 138 |
|
| 139 |
func (s *Server) routes() *http.ServeMux {
|
| 140 |
mux := http.NewServeMux()
|
| 141 |
+
mux.HandleFunc("/", s.handleRoot)
|
| 142 |
mux.HandleFunc("/health", s.handleHealth)
|
| 143 |
mux.HandleFunc("/ping", s.handlePing)
|
| 144 |
mux.HandleFunc("/reset", s.handleReset)
|
|
|
|
| 148 |
mux.HandleFunc("/grade", s.handleGrade)
|
| 149 |
mux.HandleFunc("/tasks", s.handleTasks)
|
| 150 |
mux.HandleFunc("/metrics", s.handleMetrics)
|
| 151 |
+
// Reverse proxy for dashboard (runs on port 7861 internally)
|
| 152 |
+
mux.HandleFunc("/dashboard", s.handleDashboardProxy)
|
| 153 |
+
mux.HandleFunc("/dashboard/", s.handleDashboardProxy)
|
| 154 |
return mux
|
| 155 |
}
|
| 156 |
|
| 157 |
+
// ── / (Root) ────────────────────────────────────────────────────────────────
|
| 158 |
+
|
| 159 |
+
func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
|
| 160 |
+
if r.URL.Path != "/" {
|
| 161 |
+
http.NotFound(w, r)
|
| 162 |
+
return
|
| 163 |
+
}
|
| 164 |
+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
| 165 |
+
html := `<!DOCTYPE html>
|
| 166 |
+
<html>
|
| 167 |
+
<head><title>GridMind-RL</title>
|
| 168 |
+
<style>
|
| 169 |
+
body { font-family: monospace; background: #0d1117; color: #e6edf3; padding: 40px; }
|
| 170 |
+
a { color: #58a6ff; text-decoration: none; }
|
| 171 |
+
a:hover { text-decoration: underline; }
|
| 172 |
+
.badge { background: #238636; padding: 4px 10px; border-radius: 4px; color: white; display: inline-block; }
|
| 173 |
+
ul { line-height: 1.8; }
|
| 174 |
+
pre { background: #0d1117; border: 1px solid #30363d; padding: 16px; border-radius: 6px; overflow-x: auto; }
|
| 175 |
+
</style>
|
| 176 |
+
</head>
|
| 177 |
+
<body>
|
| 178 |
+
<h1>⚡ GridMind-RL</h1>
|
| 179 |
+
<p><span class="badge">● RUNNING</span></p>
|
| 180 |
+
<p>Industrial building energy management RL environment — OpenEnv compliant.</p>
|
| 181 |
+
<h3>🔗 Quick Links</h3>
|
| 182 |
+
<ul>
|
| 183 |
+
<li><a href="/dashboard">→ Live Dashboard</a></li>
|
| 184 |
+
<li><a href="/health">→ API Health Check</a></li>
|
| 185 |
+
<li><a href="/tasks">→ Available Tasks</a></li>
|
| 186 |
+
<li><a href="/metrics">→ Prometheus Metrics</a></li>
|
| 187 |
+
</ul>
|
| 188 |
+
<h3>📡 API Endpoints</h3>
|
| 189 |
+
<pre>GET /health → health check
|
| 190 |
+
GET /ping → ping pong
|
| 191 |
+
GET /state → current environment state
|
| 192 |
+
GET /replay → episode replay data
|
| 193 |
+
GET /grade → episode grade score
|
| 194 |
+
GET /tasks → list of tasks
|
| 195 |
+
GET /metrics → prometheus metrics
|
| 196 |
+
POST /reset {task_id} → start new episode
|
| 197 |
+
POST /step {action} → take action</pre>
|
| 198 |
+
<h3>📚 Links</h3>
|
| 199 |
+
<ul>
|
| 200 |
+
<li><a href="https://github.com/LO-Kyu/gridmind">GitHub Repository</a></li>
|
| 201 |
+
<li><a href="https://openenv.org">OpenEnv Specification</a></li>
|
| 202 |
+
</ul>
|
| 203 |
+
</body>
|
| 204 |
+
</html>`
|
| 205 |
+
w.Write([]byte(html))
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
// ── /health ──────────────────────────────────────────────────────────────────
|
| 209 |
|
| 210 |
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|
|
|
| 392 |
fmt.Fprint(w, metrics.prometheus())
|
| 393 |
}
|
| 394 |
|
| 395 |
+
// ── /dashboard (Reverse Proxy) ────────────────────────────────────────────
|
| 396 |
+
|
| 397 |
+
func (s *Server) handleDashboardProxy(w http.ResponseWriter, r *http.Request) {
|
| 398 |
+
// Target URL for the dashboard service (running on localhost:7861)
|
| 399 |
+
target, err := url.Parse("http://localhost:7861")
|
| 400 |
+
if err != nil {
|
| 401 |
+
http.Error(w, "proxy configuration error", http.StatusInternalServerError)
|
| 402 |
+
return
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
// Create a custom director to modify the request
|
| 406 |
+
director := func(req *http.Request) {
|
| 407 |
+
// Strip /dashboard prefix
|
| 408 |
+
path := req.URL.Path
|
| 409 |
+
if strings.HasPrefix(path, "/dashboard") {
|
| 410 |
+
path = strings.TrimPrefix(path, "/dashboard")
|
| 411 |
+
if path == "" {
|
| 412 |
+
path = "/"
|
| 413 |
+
}
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
// Set up the new request
|
| 417 |
+
req.URL.Scheme = target.Scheme
|
| 418 |
+
req.URL.Host = target.Host
|
| 419 |
+
if target.Path != "" {
|
| 420 |
+
req.URL.Path = target.Path + path
|
| 421 |
+
} else {
|
| 422 |
+
req.URL.Path = path
|
| 423 |
+
}
|
| 424 |
+
req.RequestURI = ""
|
| 425 |
+
|
| 426 |
+
// Preserve original host header for dashboard API calls
|
| 427 |
+
if req.Header.Get("X-Forwarded-Host") == "" {
|
| 428 |
+
req.Header.Set("X-Forwarded-For", getClientIP(r))
|
| 429 |
+
req.Header.Set("X-Forwarded-Proto", "https")
|
| 430 |
+
}
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
// Use ReverseProxy with custom director
|
| 434 |
+
proxy := &httputil.ReverseProxy{Director: director}
|
| 435 |
+
proxy.ServeHTTP(w, r)
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
// Helper: extract client IP from request
|
| 439 |
+
func getClientIP(r *http.Request) string {
|
| 440 |
+
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
| 441 |
+
if err != nil {
|
| 442 |
+
return r.RemoteAddr
|
| 443 |
+
}
|
| 444 |
+
return ip
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
// ──────────────────────────────────────────────
|
| 448 |
// Entry point
|
| 449 |
// ──────────────────────────────────────────────
|
|
|
|
| 465 |
srv.envMgr.Reset(env.ResetRequest{Seed: &seed, TaskID: 1, NumBuildings: 1})
|
| 466 |
|
| 467 |
log.Printf("GridMind-RL environment server starting on :%s", port)
|
| 468 |
+
log.Printf("Endpoints: GET / (landing) | GET /health /ping /state /replay /grade /tasks /metrics /dashboard | POST /reset /step")
|
| 469 |
|
| 470 |
mux := withCORS(withLogging(srv.routes()))
|
| 471 |
if err := http.ListenAndServe(":"+port, mux); err != nil {
|
python/inference.py
CHANGED
|
@@ -376,6 +376,8 @@ def run_episode(
|
|
| 376 |
grade = env_client.grade()
|
| 377 |
|
| 378 |
# Emit [END] with required fields
|
|
|
|
|
|
|
| 379 |
print(
|
| 380 |
f"[END] success={'true' if success else 'false'} steps={total_steps} rewards={rewards_str}",
|
| 381 |
flush=True
|
|
|
|
| 376 |
grade = env_client.grade()
|
| 377 |
|
| 378 |
# Emit [END] with required fields
|
| 379 |
+
success = last_error is None and step_resp.get("done", False)
|
| 380 |
+
rewards_str = ",".join(f"{r:.2f}" for r in step_rewards)
|
| 381 |
print(
|
| 382 |
f"[END] success={'true' if success else 'false'} steps={total_steps} rewards={rewards_str}",
|
| 383 |
flush=True
|