Spaces:
Running
Running
deploy(S4): Blender headless pipeline + WebAR client + backend fixes
Browse files- backend/routes/mobile_bridge_routes.py +21 -7
- backend/ws/agent_ws.py +70 -0
backend/routes/mobile_bridge_routes.py
CHANGED
|
@@ -224,20 +224,34 @@ async def command(request: Request) -> dict[str, Any]:
|
|
| 224 |
#
|
| 225 |
# Falls through to record-only when no PC is listening, which is the honest
|
| 226 |
# answer when the machine is off rather than a failure.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
try:
|
| 228 |
from backend.ws.agent_ws import ws_manager as _ws_manager
|
| 229 |
-
|
| 230 |
-
"event": "system:execute",
|
| 231 |
-
"payload": {"cmd": cmd, "source": "guardian", "device_name": device_name},
|
| 232 |
-
})
|
| 233 |
except Exception as exc:
|
| 234 |
log.warning("PC relay unavailable: %s", exc)
|
| 235 |
-
|
| 236 |
|
|
|
|
| 237 |
if delivered:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
return {
|
| 239 |
-
"
|
| 240 |
-
|
|
|
|
|
|
|
| 241 |
"status": "relayed",
|
| 242 |
"action": "pc_relay",
|
| 243 |
"pc_count": delivered,
|
|
|
|
| 224 |
#
|
| 225 |
# Falls through to record-only when no PC is listening, which is the honest
|
| 226 |
# answer when the machine is off rather than a failure.
|
| 227 |
+
#
|
| 228 |
+
# We wait for the PC's actual answer rather than reporting success on
|
| 229 |
+
# hand-off. The desktop can refuse a command outright β an un-activated
|
| 230 |
+
# session hard-blocks every one of them β and reporting "Sent to your PC."
|
| 231 |
+
# in that case tells the user their command ran when it did not.
|
| 232 |
try:
|
| 233 |
from backend.ws.agent_ws import ws_manager as _ws_manager
|
| 234 |
+
outcome = await _ws_manager.execute_on_pcs(cmd, device_name=device_name)
|
|
|
|
|
|
|
|
|
|
| 235 |
except Exception as exc:
|
| 236 |
log.warning("PC relay unavailable: %s", exc)
|
| 237 |
+
outcome = {"delivered": 0, "ok": False, "spoken": [], "timed_out": False}
|
| 238 |
|
| 239 |
+
delivered = int(outcome.get("delivered") or 0)
|
| 240 |
if delivered:
|
| 241 |
+
if outcome.get("timed_out"):
|
| 242 |
+
return {
|
| 243 |
+
"ok": False,
|
| 244 |
+
"spoken": ["Your PC received the command but did not report back."],
|
| 245 |
+
"status": "no_response",
|
| 246 |
+
"action": "pc_relay",
|
| 247 |
+
"pc_count": delivered,
|
| 248 |
+
}
|
| 249 |
+
spoken = outcome.get("spoken") or []
|
| 250 |
return {
|
| 251 |
+
# The PC's own words when it has any β that is where "This PC is
|
| 252 |
+
# lockedβ¦" reaches the phone β and a plain confirmation otherwise.
|
| 253 |
+
"ok": bool(outcome.get("ok")),
|
| 254 |
+
"spoken": spoken or ["Done on your PC."],
|
| 255 |
"status": "relayed",
|
| 256 |
"action": "pc_relay",
|
| 257 |
"pc_count": delivered,
|
backend/ws/agent_ws.py
CHANGED
|
@@ -56,6 +56,12 @@ class ConnectionManager:
|
|
| 56 |
# idea which socket it was. That is why phone -> cloud -> PC control did
|
| 57 |
# not work: not a missing channel, an unread introduction.
|
| 58 |
self.pc_connections: list[WebSocket] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
from backend.agent.react_agent import Tool
|
| 60 |
self.tools = []
|
| 61 |
for name, func in TOOL_REGISTRY.items():
|
|
@@ -102,6 +108,49 @@ class ConnectionManager:
|
|
| 102 |
self.pc_connections.remove(connection)
|
| 103 |
return delivered
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
async def broadcast(self, message: dict):
|
| 106 |
for connection in self.active_connections:
|
| 107 |
try:
|
|
@@ -134,6 +183,27 @@ class ConnectionManager:
|
|
| 134 |
except Exception:
|
| 135 |
pass
|
| 136 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
# ββ WAKE WORD β PERSONA SWITCH (Cloud + Local path) ββββββββββββββββββββββ
|
| 138 |
# Mobile app / local PC sends: {"event": "voice:wake_word", "payload": {"agent": "hey jarvis"}}
|
| 139 |
# The server auto-switches the active persona and broadcasts UI + TTS confirmation.
|
|
|
|
| 56 |
# idea which socket it was. That is why phone -> cloud -> PC control did
|
| 57 |
# not work: not a missing channel, an unread introduction.
|
| 58 |
self.pc_connections: list[WebSocket] = []
|
| 59 |
+
# In-flight command_id -> Future, resolved when a PC reports the outcome.
|
| 60 |
+
# Without this the cloud answered the phone the instant it had *handed
|
| 61 |
+
# off* the command, so "Sent to your PC." was printed for commands the
|
| 62 |
+
# desktop then refused outright (locked session) or failed to run. The
|
| 63 |
+
# phone showed success for work that never happened.
|
| 64 |
+
self.pending_results: dict[str, asyncio.Future] = {}
|
| 65 |
from backend.agent.react_agent import Tool
|
| 66 |
self.tools = []
|
| 67 |
for name, func in TOOL_REGISTRY.items():
|
|
|
|
| 108 |
self.pc_connections.remove(connection)
|
| 109 |
return delivered
|
| 110 |
|
| 111 |
+
async def execute_on_pcs(self, cmd: str, device_name: str = "",
|
| 112 |
+
timeout: float = 20.0) -> dict:
|
| 113 |
+
"""Send a command to the PCs and wait for the first real outcome.
|
| 114 |
+
|
| 115 |
+
Returns {delivered, ok, spoken, timed_out}. `delivered == 0` means no
|
| 116 |
+
desktop was listening; `timed_out` means one was, but it never answered
|
| 117 |
+
(old relay build, or the command stack hung) β three states the caller
|
| 118 |
+
must be able to tell apart, because only one of them is success.
|
| 119 |
+
"""
|
| 120 |
+
command_id = str(uuid.uuid4())
|
| 121 |
+
loop = asyncio.get_running_loop()
|
| 122 |
+
future: asyncio.Future = loop.create_future()
|
| 123 |
+
self.pending_results[command_id] = future
|
| 124 |
+
try:
|
| 125 |
+
delivered = await self.send_to_pcs({
|
| 126 |
+
"event": "system:execute",
|
| 127 |
+
"payload": {
|
| 128 |
+
"cmd": cmd,
|
| 129 |
+
"source": "guardian",
|
| 130 |
+
"device_name": device_name,
|
| 131 |
+
"command_id": command_id,
|
| 132 |
+
},
|
| 133 |
+
})
|
| 134 |
+
if not delivered:
|
| 135 |
+
return {"delivered": 0, "ok": False, "spoken": [], "timed_out": False}
|
| 136 |
+
try:
|
| 137 |
+
result = await asyncio.wait_for(future, timeout=timeout)
|
| 138 |
+
except asyncio.TimeoutError:
|
| 139 |
+
logging.warning("PC relay did not report a result for %r", cmd)
|
| 140 |
+
return {"delivered": delivered, "ok": False, "spoken": [],
|
| 141 |
+
"timed_out": True}
|
| 142 |
+
spoken = result.get("spoken") or []
|
| 143 |
+
if isinstance(spoken, str):
|
| 144 |
+
spoken = [spoken]
|
| 145 |
+
return {
|
| 146 |
+
"delivered": delivered,
|
| 147 |
+
"ok": bool(result.get("ok")),
|
| 148 |
+
"spoken": [str(s) for s in spoken],
|
| 149 |
+
"timed_out": False,
|
| 150 |
+
}
|
| 151 |
+
finally:
|
| 152 |
+
self.pending_results.pop(command_id, None)
|
| 153 |
+
|
| 154 |
async def broadcast(self, message: dict):
|
| 155 |
for connection in self.active_connections:
|
| 156 |
try:
|
|
|
|
| 183 |
except Exception:
|
| 184 |
pass
|
| 185 |
return
|
| 186 |
+
|
| 187 |
+
# ββ WHAT HAPPENED? ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 188 |
+
# The PC reports back the outcome of a system:execute it was given, keyed
|
| 189 |
+
# by the command_id the cloud minted. This closes the loop that used to
|
| 190 |
+
# be open-ended: without it the phone was told "Sent to your PC." and
|
| 191 |
+
# never learned that the desktop had refused the command.
|
| 192 |
+
if data.get("event") == "relay:result":
|
| 193 |
+
payload = data.get("payload") or {}
|
| 194 |
+
command_id = str(payload.get("command_id") or "")
|
| 195 |
+
future = self.pending_results.get(command_id)
|
| 196 |
+
if future is not None and not future.done():
|
| 197 |
+
future.set_result({
|
| 198 |
+
"ok": bool(payload.get("ok")),
|
| 199 |
+
"spoken": payload.get("spoken") or [],
|
| 200 |
+
})
|
| 201 |
+
else:
|
| 202 |
+
# Late or duplicate answer β the waiter already gave up, or a
|
| 203 |
+
# second PC answered after the first. Never an error.
|
| 204 |
+
logging.debug("relay:result for unknown command_id %r", command_id)
|
| 205 |
+
return
|
| 206 |
+
|
| 207 |
# ββ WAKE WORD β PERSONA SWITCH (Cloud + Local path) ββββββββββββββββββββββ
|
| 208 |
# Mobile app / local PC sends: {"event": "voice:wake_word", "payload": {"agent": "hey jarvis"}}
|
| 209 |
# The server auto-switches the active persona and broadcasts UI + TTS confirmation.
|