Ghostgim commited on
Commit
9eb8ea7
·
verified ·
1 Parent(s): ebeff3e

Initial deploy: ghostloop v1.0.0 demo

Browse files
Files changed (3) hide show
  1. README.md +56 -8
  2. app.py +244 -0
  3. requirements.txt +2 -0
README.md CHANGED
@@ -1,13 +1,61 @@
1
  ---
2
- title: Ghostloop Demo
3
- emoji: 🏆
4
- colorFrom: pink
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.14.0
8
- python_version: '3.13'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: ghostloop demo
3
+ emoji: 🤖
4
+ colorFrom: green
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 4.44.0
 
8
  app_file: app.py
9
+ pinned: true
10
+ license: mit
11
+ short_description: Drive any robot through a fail-closed safety pipeline.
12
+ tags:
13
+ - robotics
14
+ - embodied-ai
15
+ - agent
16
+ - mcp
17
+ - safety
18
+ - mujoco
19
+ - ros2
20
  ---
21
 
22
+ # ghostloop · live demo
23
+
24
+ The agent loop, embodied. Tool-using runtime + fail-closed safety
25
+ pipeline + sim-first execution + post-hoc analysis layer for
26
+ embodied AI / robotics. Sister project to
27
+ [GhostLM](https://github.com/joemunene-by/GhostLM).
28
+
29
+ This Space lets you pick a robot profile (Franka arm / Spot quadruped /
30
+ Tello drone / Stretch mobile arm / humanoid / TurtleBot) and dispatch
31
+ Intents through the safety pipeline that ships in the library.
32
+
33
+ - **GitHub:** https://github.com/joemunene-by/ghostloop
34
+ - **PyPI:** `pip install ghostloop`
35
+ - **arXiv:** _[link to be added once preprint is up]_
36
+
37
+ ## What you can try here
38
+
39
+ 1. Switch profiles to see how the same Runtime + safety pipeline shape
40
+ covers totally different morphologies.
41
+ 2. Send `move_to {"x": 5, "y": 0, "z": 0}` on the Franka profile to
42
+ watch the GeofenceGate reject it with a structured reason.
43
+ 3. Send `takeoff {"altitude": 1}` on the Tello profile to see the HITL
44
+ gate escalate.
45
+ 4. Read the trace event JSON for any call — that's the same shape the
46
+ library emits for replay, diff, query, energy ledger, and judge
47
+ scoring.
48
+
49
+ ## Beyond the demo
50
+
51
+ The full library does much more than this Space exposes:
52
+
53
+ - 6 backends (Mock / MuJoCo / PyBullet / Gymnasium / ROS 2 / Randomized).
54
+ - 12 policy gates including STL temporal properties.
55
+ - Counterfactual trace replay, causal failure attribution, LLM-as-judge.
56
+ - VLA-on-MuJoCo benchmark harness vs OpenVLA / π0 / RT-2 / Octo numbers.
57
+ - Safe-RL training loop with Lagrangian multiplier + HER.
58
+ - Production fleet dashboard with auth + rate limit + alarms + Prometheus.
59
+ - MCP server for Claude Desktop / Cursor / Continue / Cline / Zed / Gemini CLI.
60
+
61
+ `pip install ghostloop` and clone the repo for the full kit.
app.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HuggingFace Space — interactive ghostloop demo.
2
+
3
+ Live URL: https://huggingface.co/spaces/Ghostgim/ghostloop-demo (after deploy).
4
+
5
+ Lets a visitor:
6
+ 1. Pick a robot profile (franka_arm / spot / tello / humanoid_demo).
7
+ 2. See the safety pipeline (gates active for that profile).
8
+ 3. Send an Intent and watch the runtime dispatch + record the trace.
9
+ 4. Replay the trace as JSON.
10
+ 5. Read each profile's instructions block to understand what the LLM
11
+ "knows" about that robot.
12
+
13
+ Why a Space: GitHub repos take 30 seconds to clone + install. A Space
14
+ loads in 5 seconds, no install. That's where most reputation conversions
15
+ happen — the moment somebody clicks a link and sees Claude reasoning
16
+ about a robot in the browser.
17
+
18
+ Stack:
19
+ gradio ≥ 4.0 (UI)
20
+ ghostloop ≥ 1.0 (the library)
21
+
22
+ Zero hardware deps — runs against MockBackend so the Space starts
23
+ instantly on the free CPU tier.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ from typing import Any
30
+
31
+ import gradio as gr
32
+
33
+ from ghostloop import Intent
34
+ from ghostloop.profiles import (
35
+ build_runtime_from_profile,
36
+ franka_arm,
37
+ humanoid_demo,
38
+ spot_quadruped,
39
+ stretch_mobile_arm,
40
+ tello_drone,
41
+ turtlebot_base,
42
+ )
43
+
44
+
45
+ PRESETS = {
46
+ "franka_arm — 7-DOF arm": franka_arm,
47
+ "spot — Boston Dynamics quadruped": spot_quadruped,
48
+ "tello — quadcopter drone": tello_drone,
49
+ "stretch — mobile arm": stretch_mobile_arm,
50
+ "humanoid_demo — stationary humanoid": humanoid_demo,
51
+ "turtlebot — wheeled mobile base": turtlebot_base,
52
+ }
53
+
54
+
55
+ def load_profile(profile_label: str) -> tuple[str, str, str, str]:
56
+ """Build a runtime + return its description / primitives / gates / instructions."""
57
+ factory = PRESETS[profile_label]
58
+ profile = factory()
59
+ runtime = build_runtime_from_profile(profile)
60
+ primitives_md = "\n".join(
61
+ f"- **`{name}`** — {runtime.registry.get(name).description}"
62
+ for name in runtime.registry.names()
63
+ )
64
+ gates_md = "\n".join(
65
+ f"- {g.__class__.__name__}" for g in runtime.policy_pipeline.gates
66
+ )
67
+ summary = (
68
+ f"**Profile:** `{profile.name}` \n"
69
+ f"**Morphology:** `{profile.morphology}` \n"
70
+ f"**Backend:** `{runtime.backend.name}` (mock) \n"
71
+ f"**Workspace:** `{profile.workspace_bounds}` \n"
72
+ f"**Max velocity:** `{profile.max_velocity}` m/s \n"
73
+ f"**HITL primitives:** `{profile.hitl_primitives}`"
74
+ )
75
+ return summary, primitives_md, gates_md, profile.instructions or "(no instructions block)"
76
+
77
+
78
+ def step_runtime(profile_label: str, primitive_name: str, args_json: str) -> tuple[str, str]:
79
+ """Dispatch one Intent against a fresh runtime, return result + last trace event."""
80
+ factory = PRESETS[profile_label]
81
+ runtime = build_runtime_from_profile(factory())
82
+ try:
83
+ args = json.loads(args_json) if args_json.strip() else {}
84
+ if not isinstance(args, dict):
85
+ raise ValueError("args must be a JSON object")
86
+ except (ValueError, json.JSONDecodeError) as e:
87
+ return f"❌ args JSON parse error: {e}", "—"
88
+ if primitive_name not in runtime.registry.names():
89
+ return (
90
+ f"❌ Unknown primitive {primitive_name!r}. Available for "
91
+ f"this profile: {runtime.registry.names()}",
92
+ "—",
93
+ )
94
+ intent = Intent(name=primitive_name, args=args)
95
+ result = runtime.step(intent)
96
+ event = runtime.trace.events[-1]
97
+ decision = event.decision
98
+ status_icon = {"ok": "✅", "blocked": "🚫", "error": "❌"}.get(
99
+ result.status.value, "⚠️"
100
+ )
101
+ summary = (
102
+ f"{status_icon} **status:** `{result.status.value}` \n"
103
+ f"**decision:** `{decision.action.value}`"
104
+ f" (gate: `{decision.gate_name}`) \n"
105
+ f"**reason:** {decision.reason} \n"
106
+ f"**message:** {result.message}"
107
+ )
108
+ trace_json = json.dumps(event.to_json(), indent=2)
109
+ return summary, trace_json
110
+
111
+
112
+ def example_args(primitive_name: str) -> str:
113
+ """Convenience: a sensible default args dict for common primitives."""
114
+ examples = {
115
+ "move_to": '{"x": 0.4, "y": 0.0, "z": 0.5}',
116
+ "pick": '{"object_id": "widget-7"}',
117
+ "place": '{}',
118
+ "scan": '{"radius": 0.3}',
119
+ "drive": '{"linear_x": 0.2, "angular_z": 0.0}',
120
+ "stop": '{}',
121
+ "goto": '{"x": 1.5, "y": -0.5, "theta": 0.0}',
122
+ "rotate": '{"dtheta": 1.57}',
123
+ "sit": '{}',
124
+ "stand": '{}',
125
+ "lie_down": '{}',
126
+ "walk_to": '{"x": 2.0, "y": 1.0, "theta": 0.0}',
127
+ "wave": '{"hand": "right"}',
128
+ "look_at": '{"x": 1.0, "y": 0.0, "z": 1.5}',
129
+ "point_at": '{"x": 1.0, "y": 0.0, "z": 1.5}',
130
+ "nod": '{"direction": "yes"}',
131
+ "takeoff": '{"altitude": 1.0}',
132
+ "land": '{}',
133
+ "fly_to": '{"x": 1.0, "y": 0.0, "z": 1.5, "yaw": 0.0}',
134
+ "hover": '{"seconds": 2.0}',
135
+ "set_joint": '{"joint_name": "shoulder", "angle": 0.5, "duration": 1.0}',
136
+ "set_gripper": '{"state": "open", "force": 0.0}',
137
+ "sense": '{"modality": "rgb"}',
138
+ "scan_360": '{}',
139
+ "take_photo": '{}',
140
+ "read_battery": '{}',
141
+ "wait": '{"seconds": 1.0}',
142
+ "emit_event": '{"kind": "note", "message": "demo event"}',
143
+ }
144
+ return examples.get(primitive_name, "{}")
145
+
146
+
147
+ with gr.Blocks(
148
+ title="ghostloop — the agent loop, embodied",
149
+ theme=gr.themes.Soft(primary_hue="teal"),
150
+ ) as demo:
151
+ gr.Markdown(
152
+ """
153
+ # ghostloop · live demo
154
+
155
+ The agent loop, embodied. A tool-using runtime + fail-closed safety
156
+ pipeline + sim-first execution + post-hoc analysis layer for embodied AI.
157
+
158
+ This Space runs against `MockBackend` so it starts instantly. Every
159
+ primitive call goes through the **same fail-closed safety pipeline** that
160
+ ships in the library (geofence + force cap + action smoothing + rate
161
+ limit + HITL) — try sending a `move_to` outside the workspace and watch
162
+ the geofence reject it.
163
+
164
+ [GitHub](https://github.com/joemunene-by/ghostloop) · [PyPI](https://pypi.org/project/ghostloop/) · [arXiv preprint](#) · [Sister project: GhostLM](https://github.com/joemunene-by/GhostLM)
165
+ """
166
+ )
167
+
168
+ with gr.Row():
169
+ with gr.Column(scale=1):
170
+ profile_dd = gr.Dropdown(
171
+ label="Robot profile",
172
+ choices=list(PRESETS.keys()),
173
+ value="franka_arm — 7-DOF arm",
174
+ )
175
+ summary_md = gr.Markdown(label="Profile summary")
176
+ instructions_md = gr.Markdown(label="LLM instructions")
177
+
178
+ with gr.Column(scale=1):
179
+ primitives_md = gr.Markdown(label="Available primitives")
180
+ gates_md = gr.Markdown(label="Active safety gates")
181
+
182
+ profile_dd.change(
183
+ load_profile,
184
+ inputs=[profile_dd],
185
+ outputs=[summary_md, primitives_md, gates_md, instructions_md],
186
+ )
187
+
188
+ gr.Markdown("## Dispatch a primitive")
189
+
190
+ with gr.Row():
191
+ primitive_in = gr.Textbox(
192
+ label="Primitive name",
193
+ value="move_to",
194
+ placeholder="e.g. move_to / drive / takeoff / wave",
195
+ )
196
+ args_in = gr.Textbox(
197
+ label="Args (JSON)",
198
+ value='{"x": 0.4, "y": 0.0, "z": 0.5}',
199
+ lines=2,
200
+ )
201
+ primitive_in.change(
202
+ example_args, inputs=[primitive_in], outputs=[args_in],
203
+ )
204
+
205
+ run_btn = gr.Button("▶ runtime.step(intent)", variant="primary")
206
+ result_md = gr.Markdown(label="Result")
207
+ trace_json = gr.Code(label="Trace event (JSON)", language="json")
208
+ run_btn.click(
209
+ step_runtime,
210
+ inputs=[profile_dd, primitive_in, args_in],
211
+ outputs=[result_md, trace_json],
212
+ )
213
+
214
+ gr.Markdown(
215
+ """
216
+ ### Try these:
217
+
218
+ 1. **Geofence violation:** keep `franka_arm` profile, send
219
+ `move_to` with `{"x": 5.0, "y": 0.0, "z": 0.5}`. The Geofence gate
220
+ blocks it; the trace records exactly why.
221
+ 2. **HITL escalation:** switch to `tello`, send `takeoff` with
222
+ `{"altitude": 1.0}`. The HumanInTheLoopGate is wired to `takeoff`
223
+ for this profile — in a real terminal it'd prompt for approval; in
224
+ the Space it returns an "approver declined" since there's no human.
225
+ 3. **Cross-morphology:** switch to `spot`, send `walk_to` with
226
+ `{"x": 2.0, "y": 1.0, "theta": 0.0}`. The same Runtime, the same
227
+ safety pipeline pattern, completely different primitive set.
228
+
229
+ Every call records a typed `TraceEvent` you can replay, diff, query
230
+ with the trace DSL, score with the LLM judge, or attribute causally
231
+ when something fails.
232
+ """
233
+ )
234
+
235
+ # Initial population.
236
+ demo.load(
237
+ load_profile,
238
+ inputs=[profile_dd],
239
+ outputs=[summary_md, primitives_md, gates_md, instructions_md],
240
+ )
241
+
242
+
243
+ if __name__ == "__main__":
244
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio>=4.44
2
+ ghostloop>=1.0.0