Skip to content

Commit d6f9f64

Browse files
authored
Merge pull request #2 from Leo-Expose/main
Update codebase with various modifications
2 parents 4021174 + 38c0058 commit d6f9f64

13 files changed

Lines changed: 560 additions & 25 deletions

File tree

.agent/rules/graphify.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
## graphify
2+
3+
This project has a graphify knowledge graph at graphify-out/.
4+
5+
Rules:
6+
- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
7+
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
8+
- If the graphify MCP server is active, utilize tools like `query_graph`, `get_node`, and `shortest_path` for precise architecture navigation instead of falling back to `grep`
9+
- After modifying code files in this session, run `python3 -c "from graphify.watch import _rebuild_code; from pathlib import Path; _rebuild_code(Path('.'))"` to keep the graph current

.agent/workflows/graphify.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Workflow: graphify
2+
**Command:** /graphify
3+
**Description:** Turn any folder of files into a navigable knowledge graph
4+
5+
## Steps
6+
Follow the graphify skill installed at ~/.agent/skills/graphify/SKILL.md to run the full pipeline.
7+
8+
If no path argument is given, use `.` (current directory).

.cursor/rules/graphify.mdc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
description: graphify knowledge graph context
3+
alwaysApply: true
4+
---
5+
6+
This project has a graphify knowledge graph at graphify-out/.
7+
8+
- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
9+
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
10+
- After modifying code files in this session, run `python3 -c "from graphify.watch import _rebuild_code; from pathlib import Path; _rebuild_code(Path('.'))"` to keep the graph current

.opencode/plugins/graphify.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// graphify OpenCode plugin
2+
// Injects a knowledge graph reminder before bash tool calls when the graph exists.
3+
import { existsSync } from "fs";
4+
import { join } from "path";
5+
6+
export const GraphifyPlugin = async ({ directory }) => {
7+
let reminded = false;
8+
9+
return {
10+
"tool.execute.before": async (input, output) => {
11+
if (reminded) return;
12+
if (!existsSync(join(directory, "graphify-out", "graph.json"))) return;
13+
14+
if (input.tool === "bash") {
15+
output.args.command =
16+
'echo "[graphify] Knowledge graph available. Read graphify-out/GRAPH_REPORT.md for god nodes and architecture context before searching files." && ' +
17+
output.args.command;
18+
reminded = true;
19+
}
20+
},
21+
};
22+
};

Dockerfile

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ ENV PYTHONPATH="/app"
1919
ENV PYTHONUNBUFFERED=1
2020

2121
# Expose port
22-
EXPOSE 8000
22+
EXPOSE 7860
2323

24-
# Health check
25-
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
26-
CMD curl -f http://localhost:8000/health || exit 1
24+
# Health check - generous timeouts for Hugging Face Spaces
25+
HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=10 \
26+
CMD curl -f http://localhost:7860/health || exit 1
2727

2828
# Run server (the app.py lifespan prints the full banner)
29-
CMD ["python", "-m", "uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
29+
CMD ["python", "-m", "uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860"]

README.md

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
---
2+
title: Mission Control
3+
emoji: "☁️"
4+
colorFrom: blue
5+
colorTo: indigo
6+
sdk: docker
7+
tags:
8+
- finops
9+
- openenv
10+
- fastapi
11+
- observability
12+
- dashboard
13+
---
14+
115
# 🛡️ MissionCtrl — AI Oversight Fleet Environment
216

317
> *Every LLM agent fleet will hallucinate. MissionCtrl trains the overseer to catch them.*
@@ -204,7 +218,7 @@ docker build -t missionctrl .
204218
docker run -p 8000:8000 --name missionctrl missionctrl
205219

206220
# 3. Run the baseline agent (in another terminal)
207-
docker exec -it missionctrl python inference.py
221+
docker exec -it missionctrl python client.py
208222

209223
# 4. Watch the dashboard
210224
open http://localhost:8000/dashboard
@@ -222,8 +236,8 @@ python -m uvicorn server.app:app --host 0.0.0.0 --port 8000
222236
# Configure API keys
223237
cp .env.example .env # fill in your LLM provider keys
224238

225-
# Run inference
226-
python inference.py
239+
# Run inference (OpenEnv canonical entrypoint)
240+
python client.py
227241

228242
# Run tests
229243
pytest tests/ -v
@@ -301,7 +315,8 @@ missionctrl/
301315
├── openenv.yaml # OpenEnv manifest
302316
├── pyproject.toml # Python project config
303317
├── Dockerfile # Single-container deployment
304-
├── inference.py # Baseline LLM agent + cross-episode memory
318+
├── client.py # OpenEnv-required baseline evaluator entrypoint
319+
├── inference.py # Backward-compatible wrapper to client.main()
305320
├── .env.example # API key template
306321
├── server/
307322
│ ├── app.py # FastAPI server (6 endpoints + dashboard)
@@ -343,15 +358,53 @@ missionctrl/
343358
| Method | Endpoint | Description |
344359
|--------|----------|-------------|
345360
| `GET` | `/` | Status heartbeat |
346-
| `GET` | `/health` | Readiness check (`{"status": "ok"}`) |
361+
| `GET` | `/health` | Readiness check (`{"healthy": true, "env": "missionctrl"}`) |
347362
| `POST` | `/reset` | `{"task_id": "easy"}` → Reset environment for tier |
348363
| `POST` | `/step` | `{"action": "FLAG(task_01, \"evidence\")"}` → Execute action |
349-
| `GET` | `/state` | Current observation + live hallucination stats |
364+
| `GET` | `/state` | Runtime-aware observation payload + build/container metadata |
365+
| `GET` | `/logs` | Structured logs summary (status/path counters + recent requests) |
350366
| `GET` | `/history` | Full action/reward timeline (JSON array) |
351367
| `GET` | `/dashboard` | Live visualization UI |
352368

353369
---
354370

371+
## HF Spaces Health and Logs
372+
373+
The Space now exposes two `200 OK` observability endpoints intended for build/runtime diagnostics:
374+
375+
- `GET /state` returns:
376+
- `status`
377+
- `build` metadata (`container_id`, `build_id`, `git_sha`, `started_at`)
378+
- current environment `observation`
379+
- `GET /logs` returns:
380+
- `status`
381+
- `build` metadata
382+
- aggregate `totals`, `statuses`, and `paths`
383+
- recent request `entries` with `method`, `path`, `status_code`, and `duration_ms`
384+
385+
Quick check:
386+
387+
```bash
388+
python scripts.py
389+
```
390+
391+
---
392+
393+
## OpenEnv Required Files
394+
395+
OpenEnv validation expects a root-level `client.py`. This repository now provides:
396+
397+
- `client.py` as the canonical OpenEnv evaluator script
398+
- `inference.py` as a compatibility wrapper for legacy commands
399+
400+
Preferred command:
401+
402+
```bash
403+
python client.py
404+
```
405+
406+
---
407+
355408
## 🧪 Testing
356409

357410
```bash

client.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Client library for MissionCtrl environment.
2+
3+
Provides HTTP client functions for interacting with the MissionCtrl environment API.
4+
Contains environment payload structures and example scenarios.
5+
"""
6+
7+
import os
8+
from typing import Any, Dict, Optional
9+
10+
import httpx
11+
from dotenv import load_dotenv
12+
13+
# ---------------------------------------------------------------------------
14+
# Load .env file automatically
15+
# ---------------------------------------------------------------------------
16+
load_dotenv()
17+
18+
# ---------------------------------------------------------------------------
19+
# Environment configuration
20+
# ---------------------------------------------------------------------------
21+
ENV_BASE_URL: str = os.environ.get("ENV_BASE_URL", "http://localhost:7860")
22+
23+
# Known agent types in the environment
24+
KNOWN_AGENTS = (
25+
"PlannerAgent",
26+
"ResearchAgent",
27+
"CoderAgent",
28+
"TesterAgent",
29+
"CommAgent",
30+
)
31+
32+
# Available task tiers
33+
TASKS = ["easy", "medium", "hard", "special"]
34+
35+
# Default max steps per episode
36+
MAX_STEPS = 5
37+
38+
39+
# ---------------------------------------------------------------------------
40+
# HTTP Client
41+
# ---------------------------------------------------------------------------
42+
http = httpx.Client(timeout=60.0)
43+
44+
45+
# ---------------------------------------------------------------------------
46+
# Environment API Functions
47+
# ---------------------------------------------------------------------------
48+
def reset_env(task_id: str, seed: Optional[int] = None) -> Dict[str, Any]:
49+
"""Reset the environment for a specific task.
50+
51+
Args:
52+
task_id: The task tier to run (easy, medium, hard, special)
53+
seed: Optional random seed for reproducibility
54+
55+
Returns:
56+
Dictionary containing the initial observation
57+
"""
58+
payload = {"task_id": task_id}
59+
if seed is not None:
60+
payload["seed"] = seed
61+
62+
resp = http.post(f"{ENV_BASE_URL}/reset", json=payload)
63+
resp.raise_for_status()
64+
return resp.json()
65+
66+
67+
def step_env(action: str) -> Dict[str, Any]:
68+
"""Execute one action in the environment.
69+
70+
Args:
71+
action: The action string to execute (e.g., "APPROVE(task_1)")
72+
73+
Returns:
74+
Dictionary containing the new observation, reward, done flag, and info
75+
"""
76+
resp = http.post(f"{ENV_BASE_URL}/step", json={"action": action})
77+
resp.raise_for_status()
78+
return resp.json()
79+
80+
81+
def get_state() -> Dict[str, Any]:
82+
"""Get the current environment state (read-only).
83+
84+
Returns:
85+
Dictionary containing the current observation
86+
"""
87+
resp = http.get(f"{ENV_BASE_URL}/state")
88+
resp.raise_for_status()
89+
return resp.json()
90+
91+
92+
def get_history() -> list:
93+
"""Get the action history for the current episode.
94+
95+
Returns:
96+
List of past actions and their results
97+
"""
98+
resp = http.get(f"{ENV_BASE_URL}/history")
99+
resp.raise_for_status()
100+
return resp.json()
101+
102+
103+
def record_result(tier: str, score: float, steps: int, history: list,
104+
score_breakdown: Optional[Dict] = None,
105+
hallucination_stats: Optional[Dict] = None) -> Dict[str, str]:
106+
"""Push a completed episode result to the dashboard.
107+
108+
Args:
109+
tier: Task tier (easy, medium, hard, special)
110+
score: Final score for the episode
111+
steps: Number of steps taken
112+
history: Action history for the episode
113+
score_breakdown: Optional detailed score breakdown
114+
hallucination_stats: Optional hallucination detection statistics
115+
116+
Returns:
117+
Confirmation response
118+
"""
119+
payload = {
120+
"tier": tier,
121+
"score": score,
122+
"steps": steps,
123+
"history": history,
124+
"score_breakdown": score_breakdown or {},
125+
"hallucination_stats": hallucination_stats or {},
126+
}
127+
resp = http.post(f"{ENV_BASE_URL}/record", json=payload)
128+
resp.raise_for_status()
129+
return resp.json()
130+
131+
132+
# ---------------------------------------------------------------------------
133+
# Example Usage
134+
# ---------------------------------------------------------------------------
135+
def example_basic_usage():
136+
"""Example showing basic environment interaction."""
137+
# Reset environment for easy task
138+
result = reset_env("easy")
139+
obs = result["observation"]
140+
print(f"Started task with {len(obs['tasks'])} tasks")
141+
142+
# Take a step
143+
step_result = step_env("NOOP")
144+
print(f"Reward: {step_result['reward']}, Done: {step_result['done']}")
145+
146+
# Get current state
147+
state = get_state()
148+
print(f"Current step: {state['time_step']}")
149+
150+
151+
if __name__ == "__main__":
152+
# For backward compatibility, delegate to inference script
153+
from inference import main
154+
main()

inference.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
MODEL_NAME: str = os.environ.get("MODEL_NAME", "openai/gpt-oss-120b")
4747
HF_TOKEN: str = os.environ.get("HF_TOKEN", "")
4848

49-
ENV_BASE_URL: str = os.environ.get("ENV_BASE_URL", "http://localhost:8000")
49+
ENV_BASE_URL: str = os.environ.get("ENV_BASE_URL", "http://localhost:7860")
5050

5151
MAX_STEPS: int = int(os.environ.get("MAX_STEPS", "5"))
5252
TASKS: List[str] = ["easy", "medium", "hard", "special"]
@@ -100,7 +100,6 @@ class PromptTooLargeError(RuntimeError):
100100
"""Raised when provider rejects a request as permanently oversized."""
101101

102102

103-
104103
def _append_bounded_unique(bucket: List[str], value: str, limit: int) -> None:
105104
value = value.strip()
106105
if not value:

0 commit comments

Comments
 (0)