autoglm-gui 1.0.2__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. AutoGLM_GUI/adb_plus/__init__.py +12 -1
  2. AutoGLM_GUI/adb_plus/mdns.py +192 -0
  3. AutoGLM_GUI/adb_plus/pair.py +60 -0
  4. AutoGLM_GUI/adb_plus/qr_pair.py +372 -0
  5. AutoGLM_GUI/adb_plus/serial.py +61 -2
  6. AutoGLM_GUI/adb_plus/version.py +81 -0
  7. AutoGLM_GUI/api/__init__.py +16 -1
  8. AutoGLM_GUI/api/agents.py +329 -94
  9. AutoGLM_GUI/api/devices.py +304 -100
  10. AutoGLM_GUI/api/workflows.py +70 -0
  11. AutoGLM_GUI/device_manager.py +760 -0
  12. AutoGLM_GUI/exceptions.py +18 -0
  13. AutoGLM_GUI/phone_agent_manager.py +549 -0
  14. AutoGLM_GUI/phone_agent_patches.py +146 -0
  15. AutoGLM_GUI/schemas.py +380 -2
  16. AutoGLM_GUI/state.py +21 -0
  17. AutoGLM_GUI/static/assets/{about-BOnRPlKQ.js → about-PcGX7dIG.js} +1 -1
  18. AutoGLM_GUI/static/assets/chat-B0FKL2ne.js +124 -0
  19. AutoGLM_GUI/static/assets/dialog-BSNX0L1i.js +45 -0
  20. AutoGLM_GUI/static/assets/index-BjYIY--m.css +1 -0
  21. AutoGLM_GUI/static/assets/index-CnEYDOXp.js +11 -0
  22. AutoGLM_GUI/static/assets/index-DOt5XNhh.js +1 -0
  23. AutoGLM_GUI/static/assets/logo-Cyfm06Ym.png +0 -0
  24. AutoGLM_GUI/static/assets/workflows-B1hgBC_O.js +1 -0
  25. AutoGLM_GUI/static/favicon.ico +0 -0
  26. AutoGLM_GUI/static/index.html +9 -2
  27. AutoGLM_GUI/static/logo-192.png +0 -0
  28. AutoGLM_GUI/static/logo-512.png +0 -0
  29. AutoGLM_GUI/workflow_manager.py +181 -0
  30. {autoglm_gui-1.0.2.dist-info → autoglm_gui-1.2.0.dist-info}/METADATA +80 -35
  31. {autoglm_gui-1.0.2.dist-info → autoglm_gui-1.2.0.dist-info}/RECORD +34 -19
  32. AutoGLM_GUI/static/assets/chat-CGW6uMKB.js +0 -149
  33. AutoGLM_GUI/static/assets/index-CRFVU0eu.js +0 -1
  34. AutoGLM_GUI/static/assets/index-DH-Dl4tK.js +0 -10
  35. AutoGLM_GUI/static/assets/index-DzUQ89YC.css +0 -1
  36. {autoglm_gui-1.0.2.dist-info → autoglm_gui-1.2.0.dist-info}/WHEEL +0 -0
  37. {autoglm_gui-1.0.2.dist-info → autoglm_gui-1.2.0.dist-info}/entry_points.txt +0 -0
  38. {autoglm_gui-1.0.2.dist-info → autoglm_gui-1.2.0.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/exceptions.py CHANGED
@@ -5,3 +5,21 @@ class DeviceNotAvailableError(Exception):
5
5
  """Raised when device is not available (disconnected/offline)."""
6
6
 
7
7
  pass
8
+
9
+
10
+ class AgentNotInitializedError(Exception):
11
+ """Raised when attempting to access uninitialized agent."""
12
+
13
+ pass
14
+
15
+
16
+ class DeviceBusyError(Exception):
17
+ """Raised when device is currently processing a request."""
18
+
19
+ pass
20
+
21
+
22
+ class AgentInitializationError(Exception):
23
+ """Raised when agent initialization fails."""
24
+
25
+ pass
@@ -0,0 +1,549 @@
1
+ """PhoneAgent lifecycle and concurrency manager (singleton)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ import time
7
+ from contextlib import contextmanager
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from typing import TYPE_CHECKING, Callable, Optional
11
+
12
+ from AutoGLM_GUI.exceptions import (
13
+ AgentInitializationError,
14
+ AgentNotInitializedError,
15
+ DeviceBusyError,
16
+ )
17
+ from AutoGLM_GUI.logger import logger
18
+
19
+ if TYPE_CHECKING:
20
+ from phone_agent import PhoneAgent
21
+ from phone_agent.agent import AgentConfig
22
+ from phone_agent.model import ModelConfig
23
+
24
+
25
+ class AgentState(str, Enum):
26
+ """Agent runtime state."""
27
+
28
+ IDLE = "idle" # Agent initialized, not processing
29
+ BUSY = "busy" # Agent processing a request
30
+ ERROR = "error" # Agent encountered error
31
+ INITIALIZING = "initializing" # Agent being created
32
+
33
+
34
+ @dataclass
35
+ class AgentMetadata:
36
+ """Metadata for a PhoneAgent instance."""
37
+
38
+ device_id: str
39
+ state: AgentState
40
+ model_config: ModelConfig
41
+ agent_config: AgentConfig
42
+ created_at: float
43
+ last_used: float
44
+ error_message: Optional[str] = None
45
+
46
+
47
+ class PhoneAgentManager:
48
+ """
49
+ Singleton manager for PhoneAgent lifecycle and concurrency control.
50
+
51
+ Features:
52
+ - Thread-safe agent creation/destruction
53
+ - Per-device locking (device-level concurrency control)
54
+ - State management (IDLE/BUSY/ERROR/INITIALIZING)
55
+ - Integration with DeviceManager
56
+ - Configuration hot-reload support
57
+ - Connection switching detection
58
+
59
+ Design Principles:
60
+ - Uses state.agents and state.agent_configs as storage (backward compatible)
61
+ - Double-checked locking for device locks
62
+ - RLock for manager-level operations (supports reentrant calls)
63
+ - Context managers for automatic lock release
64
+
65
+ Example:
66
+ >>> manager = PhoneAgentManager.get_instance()
67
+ >>>
68
+ >>> # Initialize agent
69
+ >>> agent = manager.initialize_agent(device_id, model_config, agent_config)
70
+ >>>
71
+ >>> # Use agent with automatic locking
72
+ >>> with manager.use_agent(device_id) as agent:
73
+ >>> result = agent.run("Open WeChat")
74
+ """
75
+
76
+ _instance: Optional[PhoneAgentManager] = None
77
+ _instance_lock = threading.Lock()
78
+
79
+ def __init__(self):
80
+ """Private constructor. Use get_instance() instead."""
81
+ # Manager-level lock (protects internal state)
82
+ self._manager_lock = threading.RLock()
83
+
84
+ # Device-level locks (per-device concurrency control)
85
+ self._device_locks: dict[str, threading.Lock] = {}
86
+ self._device_locks_lock = threading.Lock()
87
+
88
+ # Agent metadata (indexed by device_id)
89
+ self._metadata: dict[str, AgentMetadata] = {}
90
+
91
+ # State tracking
92
+ self._states: dict[str, AgentState] = {}
93
+
94
+ @classmethod
95
+ def get_instance(cls) -> PhoneAgentManager:
96
+ """Get singleton instance (thread-safe, double-checked locking)."""
97
+ if cls._instance is None:
98
+ with cls._instance_lock:
99
+ if cls._instance is None:
100
+ cls._instance = cls()
101
+ logger.info("PhoneAgentManager singleton created")
102
+ return cls._instance
103
+
104
+ # ==================== Agent Lifecycle ====================
105
+
106
+ def initialize_agent(
107
+ self,
108
+ device_id: str,
109
+ model_config: ModelConfig,
110
+ agent_config: AgentConfig,
111
+ takeover_callback: Optional[Callable] = None,
112
+ force: bool = False,
113
+ ) -> PhoneAgent:
114
+ """
115
+ Initialize PhoneAgent for a device (thread-safe, idempotent).
116
+
117
+ Args:
118
+ device_id: Device identifier (USB serial / IP:port)
119
+ model_config: Model configuration
120
+ agent_config: Agent configuration
121
+ takeover_callback: Optional takeover callback
122
+ force: Force re-initialization even if agent exists
123
+
124
+ Returns:
125
+ PhoneAgent: Initialized agent instance
126
+
127
+ Raises:
128
+ AgentInitializationError: If initialization fails
129
+ DeviceBusyError: If device is currently processing
130
+
131
+ Transactional Guarantee:
132
+ - On failure, state is rolled back
133
+ - state.agents and state.agent_configs remain consistent
134
+ """
135
+ from phone_agent import PhoneAgent
136
+
137
+ from AutoGLM_GUI.state import agent_configs, agents, non_blocking_takeover
138
+
139
+ with self._manager_lock:
140
+ # Check if already initialized
141
+ if device_id in agents and not force:
142
+ logger.debug(f"Agent already initialized for {device_id}")
143
+ return agents[device_id]
144
+
145
+ # Check device availability (non-blocking check)
146
+ device_lock = self._get_device_lock(device_id)
147
+ if device_lock.locked():
148
+ raise DeviceBusyError(
149
+ f"Device {device_id} is currently processing a request"
150
+ )
151
+
152
+ # Set initializing state
153
+ self._states[device_id] = AgentState.INITIALIZING
154
+
155
+ try:
156
+ # Create agent
157
+ agent = PhoneAgent(
158
+ model_config=model_config,
159
+ agent_config=agent_config,
160
+ takeover_callback=takeover_callback or non_blocking_takeover,
161
+ )
162
+
163
+ # Store in state (transactional)
164
+ agents[device_id] = agent
165
+ agent_configs[device_id] = (model_config, agent_config)
166
+
167
+ # Update metadata
168
+ self._metadata[device_id] = AgentMetadata(
169
+ device_id=device_id,
170
+ state=AgentState.IDLE,
171
+ model_config=model_config,
172
+ agent_config=agent_config,
173
+ created_at=time.time(),
174
+ last_used=time.time(),
175
+ )
176
+ self._states[device_id] = AgentState.IDLE
177
+
178
+ logger.info(f"Agent initialized for device {device_id}")
179
+ return agent
180
+
181
+ except Exception as e:
182
+ # Rollback on error
183
+ agents.pop(device_id, None)
184
+ agent_configs.pop(device_id, None)
185
+ self._metadata.pop(device_id, None)
186
+ self._states[device_id] = AgentState.ERROR
187
+
188
+ logger.error(f"Failed to initialize agent for {device_id}: {e}")
189
+ raise AgentInitializationError(
190
+ f"Failed to initialize agent: {str(e)}"
191
+ ) from e
192
+
193
+ def get_agent(self, device_id: str) -> PhoneAgent:
194
+ """
195
+ Get initialized agent for a device.
196
+
197
+ Args:
198
+ device_id: Device identifier
199
+
200
+ Returns:
201
+ PhoneAgent: Agent instance
202
+
203
+ Raises:
204
+ AgentNotInitializedError: If agent not initialized
205
+ """
206
+ from AutoGLM_GUI.state import agents
207
+
208
+ with self._manager_lock:
209
+ if device_id not in agents:
210
+ raise AgentNotInitializedError(
211
+ f"Agent not initialized for device {device_id}. "
212
+ f"Call /api/init first."
213
+ )
214
+ return agents[device_id]
215
+
216
+ def get_agent_safe(self, device_id: str) -> Optional[PhoneAgent]:
217
+ """
218
+ Get initialized agent for a device (safe version, no exception).
219
+
220
+ Args:
221
+ device_id: Device identifier
222
+
223
+ Returns:
224
+ PhoneAgent or None: Agent instance or None if not initialized
225
+ """
226
+ from AutoGLM_GUI.state import agents
227
+
228
+ with self._manager_lock:
229
+ return agents.get(device_id)
230
+
231
+ def reset_agent(self, device_id: str) -> None:
232
+ """
233
+ Reset agent state and rebuild from cached config.
234
+
235
+ Args:
236
+ device_id: Device identifier
237
+
238
+ Raises:
239
+ AgentNotInitializedError: If agent not initialized
240
+ """
241
+ from phone_agent import PhoneAgent
242
+
243
+ from AutoGLM_GUI.state import agent_configs, agents, non_blocking_takeover
244
+
245
+ with self._manager_lock:
246
+ if device_id not in agents:
247
+ raise AgentNotInitializedError(
248
+ f"Agent not initialized for device {device_id}"
249
+ )
250
+
251
+ # Get cached config
252
+ if device_id not in agent_configs:
253
+ logger.warning(
254
+ f"No cached config for {device_id}, only resetting agent state"
255
+ )
256
+ agents[device_id].reset()
257
+ return
258
+
259
+ # Rebuild agent from cached config
260
+ model_config, agent_config = agent_configs[device_id]
261
+
262
+ agents[device_id] = PhoneAgent(
263
+ model_config=model_config,
264
+ agent_config=agent_config,
265
+ takeover_callback=non_blocking_takeover,
266
+ )
267
+
268
+ # Update metadata
269
+ if device_id in self._metadata:
270
+ self._metadata[device_id].last_used = time.time()
271
+ self._metadata[device_id].error_message = None
272
+
273
+ self._states[device_id] = AgentState.IDLE
274
+
275
+ logger.info(f"Agent reset for device {device_id}")
276
+
277
+ def destroy_agent(self, device_id: str) -> None:
278
+ """
279
+ Destroy agent and clean up resources.
280
+
281
+ Args:
282
+ device_id: Device identifier
283
+ """
284
+ from AutoGLM_GUI.state import agent_configs, agents
285
+
286
+ with self._manager_lock:
287
+ # Remove agent
288
+ agent = agents.pop(device_id, None)
289
+ if agent:
290
+ try:
291
+ agent.reset() # Clean up agent state
292
+ except Exception as e:
293
+ logger.warning(f"Error resetting agent during destroy: {e}")
294
+
295
+ # Remove config
296
+ agent_configs.pop(device_id, None)
297
+
298
+ # Remove metadata
299
+ self._metadata.pop(device_id, None)
300
+ self._states.pop(device_id, None)
301
+
302
+ logger.info(f"Agent destroyed for device {device_id}")
303
+
304
+ def is_initialized(self, device_id: str) -> bool:
305
+ """Check if agent is initialized for device."""
306
+ from AutoGLM_GUI.state import agents
307
+
308
+ with self._manager_lock:
309
+ return device_id in agents
310
+
311
+ # ==================== Concurrency Control ====================
312
+
313
+ def _get_device_lock(self, device_id: str) -> threading.Lock:
314
+ """
315
+ Get or create device lock (double-checked locking pattern).
316
+
317
+ Args:
318
+ device_id: Device identifier
319
+
320
+ Returns:
321
+ threading.Lock: Device-specific lock
322
+ """
323
+ # Fast path: lock already exists
324
+ if device_id in self._device_locks:
325
+ return self._device_locks[device_id]
326
+
327
+ # Slow path: create lock
328
+ with self._device_locks_lock:
329
+ # Double-check inside lock
330
+ if device_id not in self._device_locks:
331
+ self._device_locks[device_id] = threading.Lock()
332
+ return self._device_locks[device_id]
333
+
334
+ def acquire_device(
335
+ self,
336
+ device_id: str,
337
+ timeout: Optional[float] = None,
338
+ raise_on_timeout: bool = True,
339
+ ) -> bool:
340
+ """
341
+ Acquire device lock for exclusive access.
342
+
343
+ Args:
344
+ device_id: Device identifier
345
+ timeout: Lock acquisition timeout (None = blocking, 0 = non-blocking)
346
+ raise_on_timeout: Raise DeviceBusyError on timeout
347
+
348
+ Returns:
349
+ bool: True if acquired, False if timeout (when raise_on_timeout=False)
350
+
351
+ Raises:
352
+ DeviceBusyError: If timeout and raise_on_timeout=True
353
+ AgentNotInitializedError: If agent not initialized
354
+ """
355
+ # Verify agent exists
356
+ if not self.is_initialized(device_id):
357
+ raise AgentNotInitializedError(
358
+ f"Agent not initialized for device {device_id}"
359
+ )
360
+
361
+ lock = self._get_device_lock(device_id)
362
+
363
+ # Try to acquire with timeout
364
+ if timeout is None:
365
+ # Blocking mode
366
+ acquired = lock.acquire(blocking=True)
367
+ elif timeout == 0:
368
+ # Non-blocking mode
369
+ acquired = lock.acquire(blocking=False)
370
+ else:
371
+ # Timeout mode
372
+ acquired = lock.acquire(blocking=True, timeout=timeout)
373
+
374
+ if acquired:
375
+ # Update state
376
+ with self._manager_lock:
377
+ self._states[device_id] = AgentState.BUSY
378
+ if device_id in self._metadata:
379
+ self._metadata[device_id].last_used = time.time()
380
+
381
+ logger.debug(f"Device lock acquired for {device_id}")
382
+ return True
383
+ else:
384
+ if raise_on_timeout:
385
+ raise DeviceBusyError(
386
+ f"Device {device_id} is busy, could not acquire lock"
387
+ + (f" within {timeout}s" if timeout else "")
388
+ )
389
+ return False
390
+
391
+ def release_device(self, device_id: str) -> None:
392
+ """
393
+ Release device lock.
394
+
395
+ Args:
396
+ device_id: Device identifier
397
+ """
398
+ lock = self._get_device_lock(device_id)
399
+
400
+ if lock.locked():
401
+ lock.release()
402
+
403
+ # Update state
404
+ with self._manager_lock:
405
+ self._states[device_id] = AgentState.IDLE
406
+
407
+ logger.debug(f"Device lock released for {device_id}")
408
+
409
+ @contextmanager
410
+ def use_agent(self, device_id: str, timeout: Optional[float] = None):
411
+ """
412
+ Context manager for automatic lock acquisition/release.
413
+
414
+ Args:
415
+ device_id: Device identifier
416
+ timeout: Lock acquisition timeout
417
+
418
+ Yields:
419
+ PhoneAgent: Agent instance
420
+
421
+ Raises:
422
+ DeviceBusyError: If device is busy
423
+ AgentNotInitializedError: If agent not initialized
424
+
425
+ Example:
426
+ >>> manager = PhoneAgentManager.get_instance()
427
+ >>> with manager.use_agent("device_123") as agent:
428
+ >>> result = agent.run("Open WeChat")
429
+ """
430
+ acquired = False
431
+ try:
432
+ acquired = self.acquire_device(device_id, timeout, raise_on_timeout=True)
433
+ agent = self.get_agent(device_id)
434
+ yield agent
435
+ except Exception as exc:
436
+ # Handle errors
437
+ self.set_error_state(device_id, str(exc))
438
+ raise
439
+ finally:
440
+ if acquired:
441
+ self.release_device(device_id)
442
+
443
+ # ==================== State Management ====================
444
+
445
+ def get_state(self, device_id: str) -> AgentState:
446
+ """Get current agent state."""
447
+ with self._manager_lock:
448
+ return self._states.get(device_id, AgentState.ERROR)
449
+
450
+ def set_error_state(self, device_id: str, error_message: str) -> None:
451
+ """Mark agent as errored."""
452
+ with self._manager_lock:
453
+ self._states[device_id] = AgentState.ERROR
454
+ if device_id in self._metadata:
455
+ self._metadata[device_id].error_message = error_message
456
+
457
+ logger.error(f"Agent error for {device_id}: {error_message}")
458
+
459
+ # ==================== Configuration Management ====================
460
+
461
+ def get_config(self, device_id: str) -> tuple[ModelConfig, AgentConfig]:
462
+ """Get cached configuration for device."""
463
+ from AutoGLM_GUI.state import agent_configs
464
+
465
+ with self._manager_lock:
466
+ if device_id not in agent_configs:
467
+ raise AgentNotInitializedError(
468
+ f"No configuration found for device {device_id}"
469
+ )
470
+ return agent_configs[device_id]
471
+
472
+ def update_config(
473
+ self,
474
+ device_id: str,
475
+ model_config: Optional[ModelConfig] = None,
476
+ agent_config: Optional[AgentConfig] = None,
477
+ ) -> None:
478
+ """
479
+ Update agent configuration (requires reinitialization).
480
+
481
+ Args:
482
+ device_id: Device identifier
483
+ model_config: New model config (None = keep existing)
484
+ agent_config: New agent config (None = keep existing)
485
+ """
486
+ from AutoGLM_GUI.state import agent_configs
487
+
488
+ with self._manager_lock:
489
+ if device_id not in agent_configs:
490
+ raise AgentNotInitializedError(
491
+ f"No configuration found for device {device_id}"
492
+ )
493
+
494
+ old_model_config, old_agent_config = agent_configs[device_id]
495
+
496
+ new_model_config = model_config or old_model_config
497
+ new_agent_config = agent_config or old_agent_config
498
+
499
+ # Reinitialize with new config
500
+ self.initialize_agent(
501
+ device_id,
502
+ new_model_config,
503
+ new_agent_config,
504
+ force=True,
505
+ )
506
+
507
+ # ==================== DeviceManager Integration ====================
508
+
509
+ def find_agent_by_serial(self, serial: str) -> Optional[str]:
510
+ """
511
+ Find agent device_id by hardware serial (connection switching support).
512
+
513
+ Args:
514
+ serial: Hardware serial number
515
+
516
+ Returns:
517
+ Optional[str]: device_id of initialized agent, or None
518
+ """
519
+ from AutoGLM_GUI.device_manager import DeviceManager
520
+ from AutoGLM_GUI.state import agents
521
+
522
+ with self._manager_lock:
523
+ # Get device by serial from DeviceManager
524
+ device_manager = DeviceManager.get_instance()
525
+ device = device_manager._devices.get(serial)
526
+
527
+ if not device:
528
+ return None
529
+
530
+ # Check all connections for initialized agents
531
+ for conn in device.connections:
532
+ if conn.device_id in agents:
533
+ return conn.device_id
534
+
535
+ return None
536
+
537
+ # ==================== Introspection ====================
538
+
539
+ def list_agents(self) -> list[str]:
540
+ """Get list of all initialized device IDs."""
541
+ from AutoGLM_GUI.state import agents
542
+
543
+ with self._manager_lock:
544
+ return list(agents.keys())
545
+
546
+ def get_metadata(self, device_id: str) -> Optional[AgentMetadata]:
547
+ """Get agent metadata."""
548
+ with self._manager_lock:
549
+ return self._metadata.get(device_id)