Skip to content

Commit ca511f0

Browse files
committed
fix: add missing OpenEnv files + fix HF Spaces logging
- Add client.py, models.py, uv.lock (were gitignored/untracked) - Fix .gitignore: stop excluding uv.lock (required by OpenEnv) - Fix server logging: use stderr for HF Spaces container log capture - Root '/' now redirects to /dashboard for better Spaces preview - Add boot timestamp and endpoint URLs to startup logs
1 parent 4021174 commit ca511f0

7 files changed

Lines changed: 3261 additions & 7 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ __pycache__/
44
*.egg-info/
55
.env
66
.DS_Store
7-
*.lock
7+
# Keep uv.lock tracked (required by OpenEnv)
8+
package-lock.json
89
dist/
910
build/
1011
missionctrl_checkpoints/

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
---
2+
title: MissionCtrl
3+
emoji: 🛡️
4+
colorFrom: indigo
5+
colorTo: blue
6+
sdk: docker
7+
app_port: 8000
8+
pinned: false
9+
---
10+
111
# 🛡️ MissionCtrl — AI Oversight Fleet Environment
212

313
> *Every LLM agent fleet will hallucinate. MissionCtrl trains the overseer to catch them.*

__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
from a fleet of 5 specialist AI sub-agents.
55
"""
66

7+
from .models import MissionCtrlAction, MissionCtrlObservation
8+
from .client import MissionCtrlEnv
9+
710
__all__ = [
811
"MissionCtrlAction",
912
"MissionCtrlObservation",
13+
"MissionCtrlEnv",
1014
]

client.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""MissionCtrl Environment Client.
2+
3+
Typed EnvClient for the MissionCtrl AI Oversight Fleet Environment.
4+
Connects to the FastAPI server and provides a structured Python interface
5+
for reset / step / state interactions.
6+
7+
Example:
8+
>>> with MissionCtrlEnv(base_url="http://localhost:8000") as client:
9+
... result = client.reset()
10+
... print(result.observation.tasks)
11+
...
12+
... result = client.step(MissionCtrlAction(action="APPROVE(task_01)"))
13+
... print(result.observation.time_step, result.reward)
14+
"""
15+
16+
from typing import Dict
17+
18+
from openenv.core import EnvClient
19+
from openenv.core.client_types import StepResult
20+
from openenv.core.env_server.types import State
21+
22+
from .models import MissionCtrlAction, MissionCtrlObservation
23+
24+
25+
class MissionCtrlEnv(
26+
EnvClient[MissionCtrlAction, MissionCtrlObservation, State]
27+
):
28+
"""
29+
Client for the MissionCtrl AI Oversight Fleet Environment.
30+
31+
Maintains a persistent connection to the environment server, enabling
32+
efficient multi-step interactions with lower latency. Each client
33+
instance has its own dedicated environment session on the server.
34+
35+
Example:
36+
>>> with MissionCtrlEnv(base_url="http://localhost:8000") as client:
37+
... result = client.reset()
38+
... obs = result.observation
39+
... for task in obs.tasks:
40+
... print(task["task_id"], task["status"])
41+
...
42+
... result = client.step(MissionCtrlAction(action="FLAG(task_01, 'fabricated citation')"))
43+
... print(f"reward={result.reward}, done={result.done}")
44+
45+
Example with Docker:
46+
>>> client = MissionCtrlEnv.from_docker_image("missionctrl-env:latest")
47+
>>> try:
48+
... result = client.reset()
49+
... result = client.step(MissionCtrlAction(action="APPROVE(task_02)"))
50+
... finally:
51+
... client.close()
52+
"""
53+
54+
def _step_payload(self, action: MissionCtrlAction) -> Dict:
55+
"""
56+
Convert MissionCtrlAction to JSON payload for step message.
57+
58+
Args:
59+
action: MissionCtrlAction instance with an ``action`` string.
60+
61+
Returns:
62+
Dictionary suitable for JSON encoding and POSTing to /step.
63+
"""
64+
return {
65+
"action": action.action,
66+
}
67+
68+
def _parse_result(self, payload: Dict) -> StepResult[MissionCtrlObservation]:
69+
"""
70+
Parse server response into StepResult[MissionCtrlObservation].
71+
72+
Args:
73+
payload: JSON response data from server.
74+
75+
Returns:
76+
StepResult wrapping a MissionCtrlObservation.
77+
"""
78+
obs_data = payload.get("observation", {})
79+
observation = MissionCtrlObservation(
80+
time_step=obs_data.get("time_step", 0),
81+
max_steps=obs_data.get("max_steps", 5),
82+
difficulty=obs_data.get("difficulty", "easy"),
83+
tasks=obs_data.get("tasks", []),
84+
hallucination_stats=obs_data.get("hallucination_stats", {}),
85+
num_injected=obs_data.get("num_injected", 0),
86+
available_actions=obs_data.get("available_actions", []),
87+
done=payload.get("done", False),
88+
reward=payload.get("reward"),
89+
metadata=obs_data.get("metadata", {}),
90+
)
91+
92+
return StepResult(
93+
observation=observation,
94+
reward=payload.get("reward"),
95+
done=payload.get("done", False),
96+
)
97+
98+
def _parse_state(self, payload: Dict) -> State:
99+
"""
100+
Parse server response into State object.
101+
102+
Args:
103+
payload: JSON response from state request.
104+
105+
Returns:
106+
State object with episode_id and step_count.
107+
"""
108+
return State(
109+
episode_id=payload.get("episode_id"),
110+
step_count=payload.get("step_count", 0),
111+
)

models.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""
2+
Data models for the MissionCtrl AI Oversight Fleet Environment.
3+
4+
Defines the typed Action and Observation classes used by the MissionCtrl
5+
EnvClient for structured interaction with the environment server.
6+
"""
7+
8+
from typing import Any, Dict, List, Optional
9+
10+
from openenv.core.env_server.types import Action, Observation
11+
from pydantic import Field
12+
13+
14+
class MissionCtrlAction(Action):
15+
"""Action for the MissionCtrl environment.
16+
17+
The overseer agent submits one action string per step. Valid formats:
18+
- APPROVE(task_id)
19+
- REJECT(task_id, "reason")
20+
- REDELEGATE(task_id, AgentName)
21+
- FLAG(task_id, "evidence")
22+
- ESCALATE(task_id)
23+
- SYNTHESIZE_REPORT()
24+
- NOOP
25+
"""
26+
27+
action: str = Field(
28+
...,
29+
description=(
30+
"The overseer action string, e.g. APPROVE(task_01) or "
31+
"FLAG(task_03, \"fabricated citation detected\")"
32+
),
33+
)
34+
35+
36+
class MissionCtrlObservation(Observation):
37+
"""Observation returned by the MissionCtrl environment after each step.
38+
39+
Contains the task board, hallucination statistics, and timing info
40+
needed by the overseer agent to decide its next action.
41+
"""
42+
43+
time_step: int = Field(default=0, description="Current step number in the episode")
44+
max_steps: int = Field(default=5, description="Maximum steps allowed in this episode")
45+
difficulty: str = Field(default="easy", description="Difficulty tier: easy, medium, hard, special")
46+
47+
tasks: List[Dict[str, Any]] = Field(
48+
default_factory=list,
49+
description="List of task dicts with id, title, status, output, etc.",
50+
)
51+
hallucination_stats: Dict[str, Any] = Field(
52+
default_factory=dict,
53+
description="Hallucination tracker: total_injected, total_caught, total_flags",
54+
)
55+
num_injected: int = Field(default=0, description="Number of injected hallucinations")
56+
available_actions: List[str] = Field(
57+
default_factory=list,
58+
description="List of valid action format strings",
59+
)
60+
metadata: Dict[str, Any] = Field(
61+
default_factory=dict,
62+
description="Additional environment metadata",
63+
)

server/app.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
import os
99
import sys
1010
from contextlib import asynccontextmanager
11+
from datetime import datetime, timezone
1112
from typing import Any, Dict, List, Optional
1213

1314
from fastapi import FastAPI, HTTPException
1415
from fastapi.middleware.cors import CORSMiddleware
15-
from fastapi.responses import HTMLResponse, JSONResponse, Response
16+
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, Response
1617
from pydantic import BaseModel
1718

1819
# ---------------------------------------------------------------------------
@@ -28,7 +29,7 @@
2829
# Logging — suppress noisy poll endpoints
2930
# ---------------------------------------------------------------------------
3031
class _PollFilter(logging.Filter):
31-
_SUPPRESSED = ("/state", "/history", "/results", "/dashboard", "/health", "/favicon", "/apple-touch-icon")
32+
_SUPPRESSED = ("/state", "/history", "/results", "/dashboard", "/health", "/logs", "/favicon", "/apple-touch-icon")
3233

3334
def filter(self, record: logging.LogRecord) -> bool:
3435
msg = record.getMessage()
@@ -48,6 +49,8 @@ def filter(self, record: logging.LogRecord) -> bool:
4849
# ---------------------------------------------------------------------------
4950
_env = MissionCtrlEnvironment()
5051
_completed_results: List[Dict[str, Any]] = [] # Accumulated episode results across tiers
52+
HF_SPACE_URL = os.getenv("HF_SPACE_URL", "https://huggingface.co/spaces/Jit-fnc/missionctrl_env")
53+
HF_SPACE_LOGS_URL = os.getenv("HF_SPACE_LOGS_URL", f"{HF_SPACE_URL}?logs=container")
5154

5255
# ---------------------------------------------------------------------------
5356
# Request / Response models
@@ -89,6 +92,7 @@ async def lifespan(_: FastAPI):
8992
║ GET /state → Current observation (read-only) ║
9093
║ GET /dashboard → Live visualization UI ║
9194
║ GET /history → Agent action history (JSON) ║
95+
║ GET /logs → Redirect to HF container logs ║
9296
║ ║
9397
║ Tasks: easy, medium, hard, special ║
9498
║ ║
@@ -98,8 +102,14 @@ async def lifespan(_: FastAPI):
98102
║ ║
99103
╚══════════════════════════════════════════════════════════════╝
100104
"""
101-
print(banner)
105+
# Use sys.stderr for HF Spaces container log capture
106+
sys.stderr.write(banner + "\n")
107+
sys.stderr.flush()
102108
log.info("Server started — using persistent singleton environment")
109+
log.info("Boot time: %s", datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC"))
110+
log.info("Health: http://0.0.0.0:8000/health")
111+
log.info("Dashboard: http://0.0.0.0:8000/dashboard")
112+
sys.stderr.flush()
103113
yield
104114

105115

@@ -123,13 +133,20 @@ async def lifespan(_: FastAPI):
123133
# GET /
124134
# ---------------------------------------------------------------------------
125135
@app.get("/")
126-
async def root() -> Dict[str, Any]:
127-
"""Root endpoint — heartbeat for OpenEnv platform probes."""
136+
async def root():
137+
"""Root endpoint — redirect to dashboard for HF Spaces preview."""
138+
return RedirectResponse(url="/dashboard", status_code=307)
139+
140+
141+
@app.get("/info")
142+
async def info() -> Dict[str, Any]:
143+
"""Info endpoint — heartbeat for OpenEnv platform probes."""
128144
return {
129145
"status": "ok",
130146
"name": "missionctrl",
131147
"version": "1.0.0",
132-
"endpoints": ["/health", "/reset", "/step", "/state", "/dashboard", "/history"],
148+
"endpoints": ["/health", "/reset", "/step", "/state", "/dashboard", "/history", "/logs"],
149+
"logs_url": HF_SPACE_LOGS_URL,
133150
}
134151

135152

@@ -228,6 +245,24 @@ async def dashboard() -> HTMLResponse:
228245
return HTMLResponse(content=f.read())
229246

230247

248+
# ---------------------------------------------------------------------------
249+
# GET|HEAD /logs
250+
# ---------------------------------------------------------------------------
251+
@app.api_route("/logs", methods=["GET", "HEAD"])
252+
async def logs(redirect: bool = False):
253+
"""Return logs URL info; optionally redirect to the HF container logs page."""
254+
if redirect:
255+
return RedirectResponse(url=HF_SPACE_LOGS_URL, status_code=307)
256+
257+
return JSONResponse(
258+
{
259+
"status": "ok",
260+
"logs_url": HF_SPACE_LOGS_URL,
261+
"hint": "Open logs_url in a browser, or call /logs?redirect=1 to auto-redirect.",
262+
}
263+
)
264+
265+
231266
# ---------------------------------------------------------------------------
232267
# Favicon & Apple Touch Icon — suppress 404 noise
233268
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)