Coverage for integrations / remote_desktop / agent_tools.py: 32.9%

158 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-12 04:49 +0000

1""" 

2Remote Desktop Agent Tools — AutoGen tool definitions for remote desktop. 

3 

4Agents can programmatically: 

5 - Offer remote help (start hosting, share device ID + password) 

6 - Request screen view (connect view-only, take screenshot) 

7 - Execute remote actions (click, type, key via connected session) 

8 - Transfer files between devices 

9 - Manage sessions 

10 

11Follows core/agent_tools.py pattern: 

12 build_remote_desktop_tools(ctx) -> List[(name, desc, func)] 

13 register_remote_desktop_tools(tools, helper, executor) 

14""" 

15import logging 

16from typing import Annotated, Any, List, Optional, Tuple 

17 

18logger = logging.getLogger('hevolve.remote_desktop') 

19 

20 

21def build_remote_desktop_tools(ctx) -> List[Tuple[str, str, Any]]: 

22 """Build remote desktop tool closures for AutoGen agents. 

23 

24 Args: 

25 ctx: dict with session variables (user_id, prompt_id, etc.) 

26 

27 Returns: 

28 List of (name, description, func) tuples. 

29 """ 

30 user_id = ctx.get('user_id', 'agent') 

31 tools: List[Tuple[str, str, Any]] = [] 

32 

33 # ── offer_remote_help ───────────────────────────────────── 

34 

35 def offer_remote_help( 

36 allow_control: Annotated[bool, "Allow remote control (False=view-only)"] = True, 

37 ) -> str: 

38 """Start hosting this device for remote desktop. Returns Device ID + password.""" 

39 try: 

40 from integrations.remote_desktop.orchestrator import get_orchestrator 

41 result = get_orchestrator().start_hosting( 

42 allow_control=allow_control, 

43 user_id=user_id, 

44 ) 

45 if result.get('status') == 'error': 

46 return f"Failed to start hosting: {result.get('error')}" 

47 return ( 

48 f"Remote desktop hosting started.\n" 

49 f"Device ID: {result.get('formatted_id', 'Unknown')}\n" 

50 f"Password: {result.get('password', 'N/A')}\n" 

51 f"Mode: {result.get('mode', 'full_control')}\n" 

52 f"Engine: {result.get('engine', 'auto')}\n" 

53 f"Share Device ID + Password with the viewer to connect." 

54 ) 

55 except Exception as e: 

56 return f"Failed to start hosting: {e}" 

57 

58 tools.append(( 

59 "offer_remote_help", 

60 "Start hosting this device for remote desktop access. Returns Device ID and password to share.", 

61 offer_remote_help, 

62 )) 

63 

64 # ── request_screen_view ─────────────────────────────────── 

65 

66 def request_screen_view( 

67 device_id: Annotated[str, "Remote device ID to connect to"], 

68 password: Annotated[str, "Access password for the remote device"], 

69 ) -> str: 

70 """Connect to a remote device in view-only mode.""" 

71 try: 

72 from integrations.remote_desktop.orchestrator import get_orchestrator 

73 result = get_orchestrator().connect( 

74 device_id=device_id, 

75 password=password, 

76 mode='view_only', 

77 gui=False, 

78 user_id=user_id, 

79 ) 

80 if result.get('status') == 'connected': 

81 return (f"Connected to {device_id} (view-only) " 

82 f"via {result.get('engine', 'auto')}") 

83 return f"Connection failed: {result.get('error', 'Unknown error')}" 

84 except Exception as e: 

85 return f"Connection error: {e}" 

86 

87 tools.append(( 

88 "request_screen_view", 

89 "Connect to a remote device in view-only mode to observe the screen.", 

90 request_screen_view, 

91 )) 

92 

93 # ── remote_execute_action ───────────────────────────────── 

94 

95 def remote_execute_action( 

96 action: Annotated[str, "Action type: click, type, key, hotkey, scroll"], 

97 x: Annotated[Optional[int], "X coordinate (for click/move)"] = None, 

98 y: Annotated[Optional[int], "Y coordinate (for click/move)"] = None, 

99 text: Annotated[Optional[str], "Text to type or key name"] = None, 

100 ) -> str: 

101 """Execute a mouse/keyboard action on the connected remote device.""" 

102 try: 

103 from integrations.remote_desktop.input_handler import InputHandler 

104 handler = InputHandler() 

105 

106 event = {'type': action} 

107 if x is not None: 

108 event['x'] = x 

109 if y is not None: 

110 event['y'] = y 

111 if text is not None: 

112 if action in ('type',): 

113 event['text'] = text 

114 elif action in ('key', 'hotkey'): 

115 event['key'] = text 

116 

117 result = handler.handle_input_event(event) 

118 return f"Action executed: {result}" 

119 except Exception as e: 

120 return f"Action failed: {e}" 

121 

122 tools.append(( 

123 "remote_execute_action", 

124 "Execute a click, type, key, hotkey, or scroll action on the connected remote device.", 

125 remote_execute_action, 

126 )) 

127 

128 # ── remote_screenshot ───────────────────────────────────── 

129 

130 def remote_screenshot() -> str: 

131 """Capture a screenshot of the local or connected remote screen.""" 

132 try: 

133 from integrations.remote_desktop.frame_capture import FrameCapture 

134 capture = FrameCapture() 

135 frame = capture.capture_frame() 

136 if frame: 

137 return f"Screenshot captured ({len(frame)} bytes JPEG)" 

138 return "Screenshot capture failed — no frame returned" 

139 except Exception as e: 

140 return f"Screenshot failed: {e}" 

141 

142 tools.append(( 

143 "remote_screenshot", 

144 "Capture a screenshot of the current screen (local or remote session).", 

145 remote_screenshot, 

146 )) 

147 

148 # ── remote_transfer_file ────────────────────────────────── 

149 

150 def remote_transfer_file( 

151 device_id: Annotated[str, "Target device ID"], 

152 local_path: Annotated[str, "Local file path to transfer"], 

153 ) -> str: 

154 """Transfer a file to a connected remote device via RustDesk.""" 

155 try: 

156 import os 

157 if not os.path.exists(local_path): 

158 return f"File not found: {local_path}" 

159 

160 from integrations.remote_desktop.rustdesk_bridge import get_rustdesk_bridge 

161 bridge = get_rustdesk_bridge() 

162 if not bridge.available: 

163 return "RustDesk not installed (required for file transfer)" 

164 

165 ok, msg = bridge.connect(device_id, file_transfer=True) 

166 if ok: 

167 return f"File transfer session opened to {device_id}: {msg}" 

168 return f"File transfer failed: {msg}" 

169 except Exception as e: 

170 return f"File transfer error: {e}" 

171 

172 tools.append(( 

173 "remote_transfer_file", 

174 "Transfer a file to a remote device using RustDesk file transfer.", 

175 remote_transfer_file, 

176 )) 

177 

178 # ── get_remote_sessions ─────────────────────────────────── 

179 

180 def get_remote_sessions() -> str: 

181 """List all active remote desktop sessions.""" 

182 try: 

183 from integrations.remote_desktop.session_manager import get_session_manager 

184 sm = get_session_manager() 

185 sessions = sm.get_active_sessions() 

186 if not sessions: 

187 return "No active remote desktop sessions." 

188 lines = [f"Active sessions ({len(sessions)}):"] 

189 for s in sessions: 

190 lines.append( 

191 f" {s.session_id[:8]} host={s.host_device_id[:12]} " 

192 f"mode={s.mode.value} state={s.state.value} " 

193 f"viewers={len(s.viewer_device_ids)}" 

194 ) 

195 return "\n".join(lines) 

196 except Exception as e: 

197 return f"Error listing sessions: {e}" 

198 

199 tools.append(( 

200 "get_remote_sessions", 

201 "List all active remote desktop sessions with their status.", 

202 get_remote_sessions, 

203 )) 

204 

205 # ── disconnect_remote ───────────────────────────────────── 

206 

207 def disconnect_remote( 

208 session_id: Annotated[Optional[str], "Session ID to disconnect (all if empty)"] = None, 

209 ) -> str: 

210 """End a remote desktop session or all sessions.""" 

211 try: 

212 from integrations.remote_desktop.session_manager import get_session_manager 

213 sm = get_session_manager() 

214 if session_id: 

215 sm.disconnect_session(session_id) 

216 return f"Disconnected session {session_id[:8]}" 

217 else: 

218 sessions = sm.get_active_sessions() 

219 for s in sessions: 

220 sm.disconnect_session(s.session_id) 

221 return f"Disconnected {len(sessions)} session(s)" 

222 except Exception as e: 

223 return f"Disconnect error: {e}" 

224 

225 tools.append(( 

226 "disconnect_remote", 

227 "Disconnect a specific remote desktop session or all sessions.", 

228 disconnect_remote, 

229 )) 

230 

231 # ── list_remote_windows ────────────────────────────────── 

232 

233 def list_remote_windows() -> str: 

234 """List available application windows on this host for per-window streaming.""" 

235 try: 

236 from integrations.remote_desktop.orchestrator import get_orchestrator 

237 windows = get_orchestrator().list_remote_windows() 

238 if not windows: 

239 return "No application windows found." 

240 lines = [f"Available windows ({len(windows)}):"] 

241 for w in windows: 

242 lines.append( 

243 f" hwnd={w.get('hwnd')} \"{w.get('title', 'Untitled')}\" " 

244 f"({w.get('process_name', 'unknown')})" 

245 ) 

246 return "\n".join(lines) 

247 except Exception as e: 

248 return f"Error listing windows: {e}" 

249 

250 tools.append(( 

251 "list_remote_windows", 

252 "List available application windows on the host for per-window streaming (tab detach).", 

253 list_remote_windows, 

254 )) 

255 

256 # ── stream_remote_window ────────────────────────────────── 

257 

258 def stream_remote_window( 

259 window_title: Annotated[str, "Window title or pattern to stream"], 

260 ) -> str: 

261 """Start streaming a specific application window from the host.""" 

262 try: 

263 from integrations.remote_desktop.window_capture import WindowEnumerator 

264 enum = WindowEnumerator() 

265 winfo = enum.get_window_by_title(window_title) 

266 if not winfo: 

267 return f"No window matching '{window_title}' found." 

268 

269 from integrations.remote_desktop.orchestrator import get_orchestrator 

270 result = get_orchestrator().stream_window( 

271 window_hwnd=winfo.hwnd, 

272 window_title=winfo.title, 

273 ) 

274 if result.get('status') == 'error': 

275 return f"Failed: {result.get('error')}" 

276 return ( 

277 f"Streaming window: {result.get('window_title')}\n" 

278 f"Session: {result.get('session_id')}\n" 

279 f"Process: {result.get('process_name', 'unknown')}" 

280 ) 

281 except Exception as e: 

282 return f"Error streaming window: {e}" 

283 

284 tools.append(( 

285 "stream_remote_window", 

286 "Start streaming a specific application window from the host (tab detach).", 

287 stream_remote_window, 

288 )) 

289 

290 # ── list_peripherals ─────────────────────────────────────── 

291 

292 def list_peripherals( 

293 types: Annotated[Optional[str], "Filter by type: usb,bluetooth,gamepad (comma-sep, or None for all)"] = None, 

294 ) -> str: 

295 """List local peripheral devices available for forwarding to remote host.""" 

296 try: 

297 from integrations.remote_desktop.orchestrator import get_orchestrator 

298 type_list = [t.strip() for t in types.split(',')] if types else None 

299 peripherals = get_orchestrator().list_peripherals(types=type_list) 

300 if not peripherals: 

301 return "No peripherals found." 

302 lines = [f"Peripherals ({len(peripherals)}):"] 

303 for p in peripherals: 

304 fwd = " [FORWARDING]" if p.get('forwarded') else "" 

305 lines.append( 

306 f" {p.get('peripheral_id')} {p.get('name')} " 

307 f"type={p.get('type')}{fwd}" 

308 ) 

309 return "\n".join(lines) 

310 except Exception as e: 

311 return f"Error listing peripherals: {e}" 

312 

313 tools.append(( 

314 "list_peripherals", 

315 "List local peripheral devices (USB, Bluetooth, gamepad) available for remote forwarding.", 

316 list_peripherals, 

317 )) 

318 

319 # ── forward_peripheral ───────────────────────────────────── 

320 

321 def forward_peripheral( 

322 peripheral_id: Annotated[str, "Peripheral ID to forward"], 

323 session_id: Annotated[Optional[str], "Remote session ID (auto-detect if empty)"] = None, 

324 ) -> str: 

325 """Forward a local peripheral device to the connected remote host.""" 

326 try: 

327 from integrations.remote_desktop.orchestrator import get_orchestrator 

328 orch = get_orchestrator() 

329 

330 # Auto-detect session if not provided 

331 if not session_id: 

332 sessions = orch.get_status().get('active_sessions', []) 

333 if sessions: 

334 session_id = sessions[0].get('session_id', '') 

335 else: 

336 return "No active session. Connect to a remote host first." 

337 

338 result = orch.forward_peripheral(session_id, peripheral_id) 

339 if result.get('success'): 

340 return ( 

341 f"Forwarding {result.get('type', 'device')}: {result.get('name')}\n" 

342 f"Peripheral ID: {result.get('peripheral_id')}" 

343 ) 

344 return f"Forward failed: {result.get('error', 'Unknown error')}" 

345 except Exception as e: 

346 return f"Forward error: {e}" 

347 

348 tools.append(( 

349 "forward_peripheral", 

350 "Forward a local peripheral (USB, Bluetooth, gamepad) to the connected remote host.", 

351 forward_peripheral, 

352 )) 

353 

354 # ── discover_cast_targets ────────────────────────────────── 

355 

356 def discover_cast_targets() -> str: 

357 """Discover DLNA/UPnP renderers (smart TVs, speakers) on the local network.""" 

358 try: 

359 from integrations.remote_desktop.orchestrator import get_orchestrator 

360 targets = get_orchestrator().discover_cast_targets() 

361 if not targets: 

362 return "No DLNA/UPnP renderers found on the network." 

363 lines = [f"Cast targets ({len(targets)}):"] 

364 for t in targets: 

365 lines.append( 

366 f" {t.get('device_id', 'unknown')[:12]} " 

367 f"\"{t.get('friendly_name', 'Unknown')}\" " 

368 f"at {t.get('ip', '?')}:{t.get('port', '?')}" 

369 ) 

370 return "\n".join(lines) 

371 except Exception as e: 

372 return f"Error discovering cast targets: {e}" 

373 

374 tools.append(( 

375 "discover_cast_targets", 

376 "Discover DLNA/UPnP renderers (smart TVs, speakers) for screen casting.", 

377 discover_cast_targets, 

378 )) 

379 

380 # ── cast_to_tv ───────────────────────────────────────────── 

381 

382 def cast_to_tv( 

383 renderer_id: Annotated[str, "DLNA renderer device ID to cast to"], 

384 session_id: Annotated[Optional[str], "Session to cast (auto-detect if empty)"] = None, 

385 ) -> str: 

386 """Cast a remote desktop session to a DLNA/UPnP renderer (smart TV).""" 

387 try: 

388 from integrations.remote_desktop.orchestrator import get_orchestrator 

389 orch = get_orchestrator() 

390 

391 if not session_id: 

392 sessions = orch.get_status().get('active_sessions', []) 

393 if sessions: 

394 session_id = sessions[0].get('session_id', '') 

395 else: 

396 return "No active session to cast." 

397 

398 result = orch.cast_to_device(session_id, renderer_id) 

399 if result.get('success'): 

400 return ( 

401 f"Casting to: {result.get('renderer_name', 'Unknown')}\n" 

402 f"Stream URL: {result.get('stream_url', 'N/A')}\n" 

403 f"Cast session: {result.get('cast_session_id', 'N/A')}" 

404 ) 

405 return f"Cast failed: {result.get('error', 'Unknown error')}" 

406 except Exception as e: 

407 return f"Cast error: {e}" 

408 

409 tools.append(( 

410 "cast_to_tv", 

411 "Cast a remote desktop session to a smart TV or DLNA/UPnP renderer.", 

412 cast_to_tv, 

413 )) 

414 

415 return tools 

416 

417 

418def register_remote_desktop_tools(tools, helper, executor): 

419 """Register remote desktop tools on an AutoGen helper/executor pair. 

420 

421 Same pattern as core/agent_tools.py:register_core_tools(). 

422 """ 

423 for name, desc, func in tools: 

424 helper.register_for_llm(name=name, description=desc)(func) 

425 executor.register_for_execution(name=name)(func)