autoglm-gui 1.2.0__py3-none-any.whl → 1.3.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.
- AutoGLM_GUI/adb_plus/__init__.py +6 -6
- AutoGLM_GUI/api/__init__.py +49 -15
- AutoGLM_GUI/api/agents.py +163 -209
- AutoGLM_GUI/api/dual_model.py +310 -0
- AutoGLM_GUI/api/mcp.py +134 -0
- AutoGLM_GUI/api/metrics.py +36 -0
- AutoGLM_GUI/config_manager.py +110 -6
- AutoGLM_GUI/dual_model/__init__.py +53 -0
- AutoGLM_GUI/dual_model/decision_model.py +664 -0
- AutoGLM_GUI/dual_model/dual_agent.py +917 -0
- AutoGLM_GUI/dual_model/protocols.py +354 -0
- AutoGLM_GUI/dual_model/vision_model.py +442 -0
- AutoGLM_GUI/exceptions.py +75 -3
- AutoGLM_GUI/metrics.py +283 -0
- AutoGLM_GUI/phone_agent_manager.py +264 -14
- AutoGLM_GUI/prompts.py +97 -0
- AutoGLM_GUI/schemas.py +40 -9
- AutoGLM_GUI/static/assets/{about-PcGX7dIG.js → about-CrBXGOgB.js} +1 -1
- AutoGLM_GUI/static/assets/chat-Di2fwu8V.js +124 -0
- AutoGLM_GUI/static/assets/dialog-CHJSPLHJ.js +45 -0
- AutoGLM_GUI/static/assets/{index-DOt5XNhh.js → index-9IaIXvyy.js} +1 -1
- AutoGLM_GUI/static/assets/index-Dt7cVkfR.js +12 -0
- AutoGLM_GUI/static/assets/index-Z0uYCPOO.css +1 -0
- AutoGLM_GUI/static/assets/{workflows-B1hgBC_O.js → workflows-DHadKApI.js} +1 -1
- AutoGLM_GUI/static/index.html +2 -2
- {autoglm_gui-1.2.0.dist-info → autoglm_gui-1.3.0.dist-info}/METADATA +11 -4
- {autoglm_gui-1.2.0.dist-info → autoglm_gui-1.3.0.dist-info}/RECORD +30 -20
- AutoGLM_GUI/static/assets/chat-B0FKL2ne.js +0 -124
- AutoGLM_GUI/static/assets/dialog-BSNX0L1i.js +0 -45
- AutoGLM_GUI/static/assets/index-BjYIY--m.css +0 -1
- AutoGLM_GUI/static/assets/index-CnEYDOXp.js +0 -11
- {autoglm_gui-1.2.0.dist-info → autoglm_gui-1.3.0.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.2.0.dist-info → autoglm_gui-1.3.0.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.2.0.dist-info → autoglm_gui-1.3.0.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/metrics.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Prometheus metrics collector for AutoGLM-GUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from prometheus_client.core import CollectorRegistry, GaugeMetricFamily
|
|
9
|
+
from prometheus_client.registry import Collector
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from prometheus_client.core import Metric
|
|
13
|
+
|
|
14
|
+
from AutoGLM_GUI.logger import logger
|
|
15
|
+
from AutoGLM_GUI.version import APP_VERSION
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AutoGLMMetricsCollector(Collector):
|
|
19
|
+
"""
|
|
20
|
+
Custom Prometheus collector for AutoGLM-GUI metrics.
|
|
21
|
+
|
|
22
|
+
Implements on-demand metric collection to avoid:
|
|
23
|
+
- Stale metric data
|
|
24
|
+
- Memory leaks from unbounded label cardinality
|
|
25
|
+
- Complexity of background metric updates
|
|
26
|
+
|
|
27
|
+
Thread Safety:
|
|
28
|
+
- Acquires manager locks during collect() only
|
|
29
|
+
- Read-only operations (no state modification)
|
|
30
|
+
- Uses shallow copies where needed
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def collect(self) -> list[Metric]:
|
|
34
|
+
"""
|
|
35
|
+
Called by Prometheus client on each scrape.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of MetricFamily objects
|
|
39
|
+
"""
|
|
40
|
+
metrics = []
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
# Agent metrics
|
|
44
|
+
metrics.extend(self._collect_agent_metrics())
|
|
45
|
+
|
|
46
|
+
# Device metrics
|
|
47
|
+
metrics.extend(self._collect_device_metrics())
|
|
48
|
+
|
|
49
|
+
# Build info
|
|
50
|
+
metrics.append(self._collect_build_info())
|
|
51
|
+
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.error(f"Error collecting Prometheus metrics: {e}")
|
|
54
|
+
|
|
55
|
+
return metrics
|
|
56
|
+
|
|
57
|
+
def _collect_agent_metrics(self) -> list[Metric]:
|
|
58
|
+
"""Collect agent-related metrics (high priority only)."""
|
|
59
|
+
from AutoGLM_GUI.device_manager import DeviceManager
|
|
60
|
+
from AutoGLM_GUI.phone_agent_manager import AgentState, PhoneAgentManager
|
|
61
|
+
|
|
62
|
+
metrics = []
|
|
63
|
+
manager = PhoneAgentManager.get_instance()
|
|
64
|
+
device_manager = DeviceManager.get_instance()
|
|
65
|
+
|
|
66
|
+
# Metric 1: autoglm_agents_total (per-agent state)
|
|
67
|
+
agents_gauge = GaugeMetricFamily(
|
|
68
|
+
"autoglm_agents_total",
|
|
69
|
+
"Agent state by device",
|
|
70
|
+
labels=["device_id", "serial", "state"],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Metric 4: autoglm_agent_last_used_timestamp_seconds
|
|
74
|
+
last_used_gauge = GaugeMetricFamily(
|
|
75
|
+
"autoglm_agent_last_used_timestamp_seconds",
|
|
76
|
+
"Agent last used timestamp",
|
|
77
|
+
labels=["device_id", "serial"],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Metric 5: autoglm_agent_created_timestamp_seconds
|
|
81
|
+
created_gauge = GaugeMetricFamily(
|
|
82
|
+
"autoglm_agent_created_timestamp_seconds",
|
|
83
|
+
"Agent creation timestamp",
|
|
84
|
+
labels=["device_id", "serial"],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
busy_count = 0
|
|
88
|
+
|
|
89
|
+
with manager._manager_lock:
|
|
90
|
+
# Get snapshots (shallow copy to minimize lock time)
|
|
91
|
+
metadata_snapshot = dict(manager._metadata)
|
|
92
|
+
states_snapshot = dict(manager._states)
|
|
93
|
+
|
|
94
|
+
# Iterate over _states (not _metadata) to capture failed agents
|
|
95
|
+
for device_id, state in states_snapshot.items():
|
|
96
|
+
# Get metadata if exists (will be None for failed initialization)
|
|
97
|
+
metadata = metadata_snapshot.get(device_id)
|
|
98
|
+
|
|
99
|
+
# Get serial from DeviceManager
|
|
100
|
+
with device_manager._devices_lock:
|
|
101
|
+
device = device_manager.get_device_by_device_id(device_id)
|
|
102
|
+
serial = device.serial if device else "unknown"
|
|
103
|
+
|
|
104
|
+
# Per-agent state (1 for actual state, 0 for others)
|
|
105
|
+
for agent_state in AgentState:
|
|
106
|
+
value = 1 if state == agent_state else 0
|
|
107
|
+
agents_gauge.add_metric(
|
|
108
|
+
[device_id, serial, agent_state.value],
|
|
109
|
+
value,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Count busy agents
|
|
113
|
+
if state == AgentState.BUSY:
|
|
114
|
+
busy_count += 1
|
|
115
|
+
|
|
116
|
+
# Timestamps (0 if metadata doesn't exist, e.g., failed init)
|
|
117
|
+
if metadata:
|
|
118
|
+
last_used_gauge.add_metric(
|
|
119
|
+
[device_id, serial],
|
|
120
|
+
metadata.last_used,
|
|
121
|
+
)
|
|
122
|
+
created_gauge.add_metric(
|
|
123
|
+
[device_id, serial],
|
|
124
|
+
metadata.created_at,
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
# Failed initialization: report 0 timestamps
|
|
128
|
+
last_used_gauge.add_metric([device_id, serial], 0)
|
|
129
|
+
created_gauge.add_metric([device_id, serial], 0)
|
|
130
|
+
|
|
131
|
+
metrics.extend([agents_gauge, last_used_gauge, created_gauge])
|
|
132
|
+
|
|
133
|
+
# Metric 2: autoglm_agents_busy_count
|
|
134
|
+
busy_gauge = GaugeMetricFamily(
|
|
135
|
+
"autoglm_agents_busy_count",
|
|
136
|
+
"Number of busy agents",
|
|
137
|
+
)
|
|
138
|
+
busy_gauge.add_metric([], busy_count)
|
|
139
|
+
metrics.append(busy_gauge)
|
|
140
|
+
|
|
141
|
+
# Metric 3: autoglm_streaming_sessions_active
|
|
142
|
+
with manager._streaming_contexts_lock:
|
|
143
|
+
streaming_count = len(manager._streaming_contexts)
|
|
144
|
+
|
|
145
|
+
streaming_gauge = GaugeMetricFamily(
|
|
146
|
+
"autoglm_streaming_sessions_active",
|
|
147
|
+
"Active streaming agent sessions",
|
|
148
|
+
)
|
|
149
|
+
streaming_gauge.add_metric([], streaming_count)
|
|
150
|
+
metrics.append(streaming_gauge)
|
|
151
|
+
|
|
152
|
+
return metrics
|
|
153
|
+
|
|
154
|
+
def _collect_device_metrics(self) -> list[Metric]:
|
|
155
|
+
"""Collect device-related metrics (high priority only)."""
|
|
156
|
+
from AutoGLM_GUI.device_manager import DeviceManager, DeviceState
|
|
157
|
+
|
|
158
|
+
metrics = []
|
|
159
|
+
manager = DeviceManager.get_instance()
|
|
160
|
+
|
|
161
|
+
# Metric 6: autoglm_devices_total
|
|
162
|
+
devices_gauge = GaugeMetricFamily(
|
|
163
|
+
"autoglm_devices_total",
|
|
164
|
+
"Device state by serial",
|
|
165
|
+
labels=["serial", "model", "state", "connection_type", "status"],
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Metric 8: autoglm_device_connections_total
|
|
169
|
+
connections_gauge = GaugeMetricFamily(
|
|
170
|
+
"autoglm_device_connections_total",
|
|
171
|
+
"Connection count by type",
|
|
172
|
+
labels=["serial", "connection_type", "status"],
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Metric 10: autoglm_device_last_seen_timestamp_seconds
|
|
176
|
+
last_seen_gauge = GaugeMetricFamily(
|
|
177
|
+
"autoglm_device_last_seen_timestamp_seconds",
|
|
178
|
+
"Device last seen timestamp",
|
|
179
|
+
labels=["serial", "model"],
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
online_count = 0
|
|
183
|
+
unauthorized_count = 0
|
|
184
|
+
|
|
185
|
+
with manager._devices_lock:
|
|
186
|
+
devices_snapshot = list(manager._devices.values())
|
|
187
|
+
|
|
188
|
+
# Process connected devices
|
|
189
|
+
for device in devices_snapshot:
|
|
190
|
+
model = device.model or "unknown"
|
|
191
|
+
|
|
192
|
+
# Per-device state
|
|
193
|
+
for dev_state in DeviceState:
|
|
194
|
+
value = 1 if device.state == dev_state else 0
|
|
195
|
+
devices_gauge.add_metric(
|
|
196
|
+
[
|
|
197
|
+
device.serial,
|
|
198
|
+
model,
|
|
199
|
+
dev_state.value,
|
|
200
|
+
device.connection_type.value,
|
|
201
|
+
device.status,
|
|
202
|
+
],
|
|
203
|
+
value,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Count online devices
|
|
207
|
+
if device.state == DeviceState.ONLINE:
|
|
208
|
+
online_count += 1
|
|
209
|
+
|
|
210
|
+
# Connection breakdown
|
|
211
|
+
for conn in device.connections:
|
|
212
|
+
connections_gauge.add_metric(
|
|
213
|
+
[device.serial, conn.connection_type.value, conn.status],
|
|
214
|
+
1, # Each connection counts as 1
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if conn.status == "unauthorized":
|
|
218
|
+
unauthorized_count += 1
|
|
219
|
+
|
|
220
|
+
# Last seen timestamp
|
|
221
|
+
last_seen_gauge.add_metric([device.serial, model], device.last_seen)
|
|
222
|
+
|
|
223
|
+
metrics.extend(
|
|
224
|
+
[
|
|
225
|
+
devices_gauge,
|
|
226
|
+
connections_gauge,
|
|
227
|
+
last_seen_gauge,
|
|
228
|
+
]
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Metric 7: autoglm_devices_online_count
|
|
232
|
+
online_gauge = GaugeMetricFamily(
|
|
233
|
+
"autoglm_devices_online_count",
|
|
234
|
+
"Number of online devices",
|
|
235
|
+
)
|
|
236
|
+
online_gauge.add_metric([], online_count)
|
|
237
|
+
metrics.append(online_gauge)
|
|
238
|
+
|
|
239
|
+
# Metric 9: autoglm_device_unauthorized_connections_total
|
|
240
|
+
unauth_gauge = GaugeMetricFamily(
|
|
241
|
+
"autoglm_device_unauthorized_connections_total",
|
|
242
|
+
"Total unauthorized connections",
|
|
243
|
+
)
|
|
244
|
+
unauth_gauge.add_metric([], unauthorized_count)
|
|
245
|
+
metrics.append(unauth_gauge)
|
|
246
|
+
|
|
247
|
+
return metrics
|
|
248
|
+
|
|
249
|
+
def _collect_build_info(self) -> Metric:
|
|
250
|
+
"""Collect build information."""
|
|
251
|
+
build_info = GaugeMetricFamily(
|
|
252
|
+
"autoglm_build_info",
|
|
253
|
+
"Build information",
|
|
254
|
+
labels=["version", "python_version"],
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
258
|
+
build_info.add_metric([APP_VERSION, python_version], 1)
|
|
259
|
+
|
|
260
|
+
return build_info
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# Global collector instance (registered once)
|
|
264
|
+
_collector_registry: CollectorRegistry | None = None
|
|
265
|
+
_collector_instance: AutoGLMMetricsCollector | None = None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def get_metrics_registry() -> CollectorRegistry:
|
|
269
|
+
"""
|
|
270
|
+
Get or create the Prometheus registry with AutoGLM collector.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
CollectorRegistry: Registry instance for prometheus_client
|
|
274
|
+
"""
|
|
275
|
+
global _collector_registry, _collector_instance
|
|
276
|
+
|
|
277
|
+
if _collector_registry is None:
|
|
278
|
+
_collector_registry = CollectorRegistry()
|
|
279
|
+
_collector_instance = AutoGLMMetricsCollector()
|
|
280
|
+
_collector_registry.register(_collector_instance)
|
|
281
|
+
logger.info("Prometheus metrics collector registered")
|
|
282
|
+
|
|
283
|
+
return _collector_registry
|
|
@@ -44,6 +44,15 @@ class AgentMetadata:
|
|
|
44
44
|
error_message: Optional[str] = None
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
@dataclass
|
|
48
|
+
class StreamingAgentContext:
|
|
49
|
+
"""Streaming agent 会话上下文."""
|
|
50
|
+
|
|
51
|
+
streaming_agent: "PhoneAgent"
|
|
52
|
+
original_agent: "PhoneAgent"
|
|
53
|
+
stop_event: threading.Event
|
|
54
|
+
|
|
55
|
+
|
|
47
56
|
class PhoneAgentManager:
|
|
48
57
|
"""
|
|
49
58
|
Singleton manager for PhoneAgent lifecycle and concurrency control.
|
|
@@ -91,6 +100,13 @@ class PhoneAgentManager:
|
|
|
91
100
|
# State tracking
|
|
92
101
|
self._states: dict[str, AgentState] = {}
|
|
93
102
|
|
|
103
|
+
# Streaming agent state (device_id -> StreamingAgentContext)
|
|
104
|
+
self._streaming_contexts: dict[str, StreamingAgentContext] = {}
|
|
105
|
+
self._streaming_contexts_lock = threading.Lock()
|
|
106
|
+
|
|
107
|
+
# Abort events (device_id -> threading.Event)
|
|
108
|
+
self._abort_events: dict[str, threading.Event] = {}
|
|
109
|
+
|
|
94
110
|
@classmethod
|
|
95
111
|
def get_instance(cls) -> PhoneAgentManager:
|
|
96
112
|
"""Get singleton instance (thread-safe, double-checked locking)."""
|
|
@@ -190,10 +206,194 @@ class PhoneAgentManager:
|
|
|
190
206
|
f"Failed to initialize agent: {str(e)}"
|
|
191
207
|
) from e
|
|
192
208
|
|
|
209
|
+
def _create_streaming_agent(
|
|
210
|
+
self,
|
|
211
|
+
model_config: "ModelConfig",
|
|
212
|
+
agent_config: "AgentConfig",
|
|
213
|
+
on_thinking_chunk: Callable[[str], None],
|
|
214
|
+
) -> "PhoneAgent":
|
|
215
|
+
"""
|
|
216
|
+
创建支持流式输出的 PhoneAgent(monkey-patched model_client).
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
model_config: 模型配置
|
|
220
|
+
agent_config: Agent 配置
|
|
221
|
+
on_thinking_chunk: 思考块回调函数
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
已 patch 的 PhoneAgent 实例
|
|
225
|
+
"""
|
|
226
|
+
from phone_agent import PhoneAgent
|
|
227
|
+
|
|
228
|
+
from AutoGLM_GUI.state import non_blocking_takeover
|
|
229
|
+
|
|
230
|
+
# 创建 agent
|
|
231
|
+
agent = PhoneAgent(
|
|
232
|
+
model_config=model_config,
|
|
233
|
+
agent_config=agent_config,
|
|
234
|
+
takeover_callback=non_blocking_takeover,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Monkey-patch model_client.request 以支持流式回调
|
|
238
|
+
original_request = agent.model_client.request
|
|
239
|
+
|
|
240
|
+
def patched_request(messages, **kwargs):
|
|
241
|
+
return original_request(messages, on_thinking_chunk=on_thinking_chunk)
|
|
242
|
+
|
|
243
|
+
agent.model_client.request = patched_request
|
|
244
|
+
|
|
245
|
+
return agent
|
|
246
|
+
|
|
247
|
+
@contextmanager
|
|
248
|
+
def use_streaming_agent(
|
|
249
|
+
self,
|
|
250
|
+
device_id: str,
|
|
251
|
+
on_thinking_chunk: Callable[[str], None],
|
|
252
|
+
timeout: Optional[float] = None,
|
|
253
|
+
auto_initialize: bool = True,
|
|
254
|
+
):
|
|
255
|
+
"""
|
|
256
|
+
Context manager for streaming-enabled agent with automatic:
|
|
257
|
+
- 设备锁获取/释放
|
|
258
|
+
- Streaming agent 创建(带 monkey-patch)
|
|
259
|
+
- 上下文隔离和同步
|
|
260
|
+
- Abort 事件注册/清理
|
|
261
|
+
|
|
262
|
+
By default, automatically initializes the agent using global configuration
|
|
263
|
+
if not already initialized. Set auto_initialize=False to require explicit
|
|
264
|
+
initialization via initialize_agent().
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
device_id: 设备标识符
|
|
268
|
+
on_thinking_chunk: 流式思考块回调函数
|
|
269
|
+
timeout: 锁获取超时时间(None=阻塞,0=非阻塞)
|
|
270
|
+
auto_initialize: 自动初始化(默认: True)
|
|
271
|
+
|
|
272
|
+
Yields:
|
|
273
|
+
tuple[PhoneAgent, threading.Event]: (streaming_agent, stop_event)
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
DeviceBusyError: 设备忙
|
|
277
|
+
AgentNotInitializedError: Agent 未初始化且 auto_initialize=False
|
|
278
|
+
AgentInitializationError: auto_initialize=True 且初始化失败
|
|
279
|
+
|
|
280
|
+
Example:
|
|
281
|
+
>>> def on_chunk(chunk: str):
|
|
282
|
+
>>> print(chunk, end='', flush=True)
|
|
283
|
+
>>>
|
|
284
|
+
>>> with manager.use_streaming_agent("device_123", on_chunk) as (agent, stop_event):
|
|
285
|
+
>>> result = agent.step("Open WeChat")
|
|
286
|
+
"""
|
|
287
|
+
acquired = False
|
|
288
|
+
streaming_agent = None
|
|
289
|
+
stop_event = threading.Event()
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
# 获取设备锁(默认非阻塞)
|
|
293
|
+
acquired = self.acquire_device(
|
|
294
|
+
device_id,
|
|
295
|
+
timeout=timeout if timeout is not None else 0,
|
|
296
|
+
raise_on_timeout=True,
|
|
297
|
+
auto_initialize=auto_initialize,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# 获取原始 agent 和配置
|
|
301
|
+
original_agent = self.get_agent(device_id)
|
|
302
|
+
model_config, agent_config = self.get_config(device_id)
|
|
303
|
+
|
|
304
|
+
# 创建 streaming agent
|
|
305
|
+
streaming_agent = self._create_streaming_agent(
|
|
306
|
+
model_config=model_config,
|
|
307
|
+
agent_config=agent_config,
|
|
308
|
+
on_thinking_chunk=on_thinking_chunk,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# 复制上下文(由于持有设备锁,线程安全)
|
|
312
|
+
streaming_agent._context = original_agent._context.copy()
|
|
313
|
+
streaming_agent._step_count = original_agent._step_count
|
|
314
|
+
|
|
315
|
+
# 注册 abort 事件
|
|
316
|
+
with self._streaming_contexts_lock:
|
|
317
|
+
self._abort_events[device_id] = stop_event
|
|
318
|
+
self._streaming_contexts[device_id] = StreamingAgentContext(
|
|
319
|
+
streaming_agent=streaming_agent,
|
|
320
|
+
original_agent=original_agent,
|
|
321
|
+
stop_event=stop_event,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
logger.debug(f"Streaming agent created for {device_id}")
|
|
325
|
+
|
|
326
|
+
yield streaming_agent, stop_event
|
|
327
|
+
|
|
328
|
+
finally:
|
|
329
|
+
# 同步状态回原始 agent
|
|
330
|
+
if streaming_agent and not stop_event.is_set():
|
|
331
|
+
original_agent = self.get_agent_safe(device_id)
|
|
332
|
+
if original_agent:
|
|
333
|
+
original_agent._context = streaming_agent._context
|
|
334
|
+
original_agent._step_count = streaming_agent._step_count
|
|
335
|
+
logger.debug(
|
|
336
|
+
f"Synchronized context back to original agent for {device_id}"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# 清理 abort 事件注册
|
|
340
|
+
with self._streaming_contexts_lock:
|
|
341
|
+
self._abort_events.pop(device_id, None)
|
|
342
|
+
self._streaming_contexts.pop(device_id, None)
|
|
343
|
+
|
|
344
|
+
# 释放设备锁
|
|
345
|
+
if acquired:
|
|
346
|
+
self.release_device(device_id)
|
|
347
|
+
|
|
348
|
+
def _auto_initialize_agent(self, device_id: str) -> None:
|
|
349
|
+
"""
|
|
350
|
+
使用全局配置自动初始化 agent(内部方法,需在 manager_lock 内调用).
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
device_id: 设备标识符
|
|
354
|
+
|
|
355
|
+
Raises:
|
|
356
|
+
AgentInitializationError: 如果配置不完整或初始化失败
|
|
357
|
+
"""
|
|
358
|
+
from phone_agent.agent import AgentConfig
|
|
359
|
+
from phone_agent.model import ModelConfig
|
|
360
|
+
|
|
361
|
+
from AutoGLM_GUI.config import config
|
|
362
|
+
from AutoGLM_GUI.config_manager import config_manager
|
|
363
|
+
|
|
364
|
+
logger.info(f"Auto-initializing agent for device {device_id}...")
|
|
365
|
+
|
|
366
|
+
# 热重载配置
|
|
367
|
+
config_manager.load_file_config()
|
|
368
|
+
config_manager.sync_to_env()
|
|
369
|
+
config.refresh_from_env()
|
|
370
|
+
|
|
371
|
+
effective_config = config_manager.get_effective_config()
|
|
372
|
+
|
|
373
|
+
if not effective_config.base_url:
|
|
374
|
+
raise AgentInitializationError(
|
|
375
|
+
f"Cannot auto-initialize agent for {device_id}: base_url not configured. "
|
|
376
|
+
f"Please configure base_url via /api/config or call /api/init explicitly."
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
model_config = ModelConfig(
|
|
380
|
+
base_url=effective_config.base_url,
|
|
381
|
+
api_key=effective_config.api_key,
|
|
382
|
+
model_name=effective_config.model_name,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
agent_config = AgentConfig(device_id=device_id)
|
|
386
|
+
|
|
387
|
+
# 调用 initialize_agent(RLock 支持重入,不会死锁)
|
|
388
|
+
self.initialize_agent(device_id, model_config, agent_config)
|
|
389
|
+
logger.info(f"Agent auto-initialized for device {device_id}")
|
|
390
|
+
|
|
193
391
|
def get_agent(self, device_id: str) -> PhoneAgent:
|
|
194
392
|
"""
|
|
195
393
|
Get initialized agent for a device.
|
|
196
394
|
|
|
395
|
+
Auto-initializes the agent using global config if not already initialized.
|
|
396
|
+
|
|
197
397
|
Args:
|
|
198
398
|
device_id: Device identifier
|
|
199
399
|
|
|
@@ -201,16 +401,14 @@ class PhoneAgentManager:
|
|
|
201
401
|
PhoneAgent: Agent instance
|
|
202
402
|
|
|
203
403
|
Raises:
|
|
204
|
-
|
|
404
|
+
AgentInitializationError: If agent not initialized and auto-init fails
|
|
205
405
|
"""
|
|
206
406
|
from AutoGLM_GUI.state import agents
|
|
207
407
|
|
|
208
408
|
with self._manager_lock:
|
|
209
409
|
if device_id not in agents:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
f"Call /api/init first."
|
|
213
|
-
)
|
|
410
|
+
# 自动初始化:使用全局配置
|
|
411
|
+
self._auto_initialize_agent(device_id)
|
|
214
412
|
return agents[device_id]
|
|
215
413
|
|
|
216
414
|
def get_agent_safe(self, device_id: str) -> Optional[PhoneAgent]:
|
|
@@ -336,6 +534,7 @@ class PhoneAgentManager:
|
|
|
336
534
|
device_id: str,
|
|
337
535
|
timeout: Optional[float] = None,
|
|
338
536
|
raise_on_timeout: bool = True,
|
|
537
|
+
auto_initialize: bool = False,
|
|
339
538
|
) -> bool:
|
|
340
539
|
"""
|
|
341
540
|
Acquire device lock for exclusive access.
|
|
@@ -344,19 +543,28 @@ class PhoneAgentManager:
|
|
|
344
543
|
device_id: Device identifier
|
|
345
544
|
timeout: Lock acquisition timeout (None = blocking, 0 = non-blocking)
|
|
346
545
|
raise_on_timeout: Raise DeviceBusyError on timeout
|
|
546
|
+
auto_initialize: Auto-initialize agent if not already initialized (default: False)
|
|
347
547
|
|
|
348
548
|
Returns:
|
|
349
549
|
bool: True if acquired, False if timeout (when raise_on_timeout=False)
|
|
350
550
|
|
|
351
551
|
Raises:
|
|
352
552
|
DeviceBusyError: If timeout and raise_on_timeout=True
|
|
353
|
-
AgentNotInitializedError: If agent not initialized
|
|
553
|
+
AgentNotInitializedError: If agent not initialized AND auto_initialize=False
|
|
554
|
+
AgentInitializationError: If auto_initialize=True and initialization fails
|
|
354
555
|
"""
|
|
355
|
-
# Verify agent exists
|
|
556
|
+
# Verify agent exists (with optional auto-initialization)
|
|
356
557
|
if not self.is_initialized(device_id):
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
558
|
+
if auto_initialize:
|
|
559
|
+
# Double-check locking pattern for thread safety
|
|
560
|
+
with self._manager_lock:
|
|
561
|
+
if not self.is_initialized(device_id):
|
|
562
|
+
self._auto_initialize_agent(device_id)
|
|
563
|
+
else:
|
|
564
|
+
raise AgentNotInitializedError(
|
|
565
|
+
f"Agent not initialized for device {device_id}. "
|
|
566
|
+
f"Use auto_initialize=True or call initialize_agent() first."
|
|
567
|
+
)
|
|
360
568
|
|
|
361
569
|
lock = self._get_device_lock(device_id)
|
|
362
570
|
|
|
@@ -407,29 +615,47 @@ class PhoneAgentManager:
|
|
|
407
615
|
logger.debug(f"Device lock released for {device_id}")
|
|
408
616
|
|
|
409
617
|
@contextmanager
|
|
410
|
-
def use_agent(
|
|
618
|
+
def use_agent(
|
|
619
|
+
self,
|
|
620
|
+
device_id: str,
|
|
621
|
+
timeout: Optional[float] = None,
|
|
622
|
+
auto_initialize: bool = True,
|
|
623
|
+
):
|
|
411
624
|
"""
|
|
412
625
|
Context manager for automatic lock acquisition/release.
|
|
413
626
|
|
|
627
|
+
By default, automatically initializes the agent using global configuration
|
|
628
|
+
if not already initialized. Set auto_initialize=False to require explicit
|
|
629
|
+
initialization via initialize_agent().
|
|
630
|
+
|
|
414
631
|
Args:
|
|
415
632
|
device_id: Device identifier
|
|
416
633
|
timeout: Lock acquisition timeout
|
|
634
|
+
auto_initialize: Auto-initialize if not already initialized (default: True)
|
|
417
635
|
|
|
418
636
|
Yields:
|
|
419
637
|
PhoneAgent: Agent instance
|
|
420
638
|
|
|
421
639
|
Raises:
|
|
422
640
|
DeviceBusyError: If device is busy
|
|
423
|
-
AgentNotInitializedError: If agent not initialized
|
|
641
|
+
AgentNotInitializedError: If agent not initialized AND auto_initialize=False
|
|
642
|
+
AgentInitializationError: If auto_initialize=True and initialization fails
|
|
424
643
|
|
|
425
644
|
Example:
|
|
426
645
|
>>> manager = PhoneAgentManager.get_instance()
|
|
427
|
-
>>> with manager.use_agent("device_123") as agent:
|
|
646
|
+
>>> with manager.use_agent("device_123") as agent: # Auto-initializes
|
|
428
647
|
>>> result = agent.run("Open WeChat")
|
|
648
|
+
>>> with manager.use_agent("device_123", auto_initialize=False) as agent:
|
|
649
|
+
>>> result = agent.run("Open WeChat") # Requires prior init
|
|
429
650
|
"""
|
|
430
651
|
acquired = False
|
|
431
652
|
try:
|
|
432
|
-
acquired = self.acquire_device(
|
|
653
|
+
acquired = self.acquire_device(
|
|
654
|
+
device_id,
|
|
655
|
+
timeout,
|
|
656
|
+
raise_on_timeout=True,
|
|
657
|
+
auto_initialize=auto_initialize,
|
|
658
|
+
)
|
|
433
659
|
agent = self.get_agent(device_id)
|
|
434
660
|
yield agent
|
|
435
661
|
except Exception as exc:
|
|
@@ -547,3 +773,27 @@ class PhoneAgentManager:
|
|
|
547
773
|
"""Get agent metadata."""
|
|
548
774
|
with self._manager_lock:
|
|
549
775
|
return self._metadata.get(device_id)
|
|
776
|
+
|
|
777
|
+
def abort_streaming_chat(self, device_id: str) -> bool:
|
|
778
|
+
"""
|
|
779
|
+
中止正在进行的流式对话.
|
|
780
|
+
|
|
781
|
+
Args:
|
|
782
|
+
device_id: 设备标识符
|
|
783
|
+
|
|
784
|
+
Returns:
|
|
785
|
+
bool: True 表示发送了中止信号,False 表示没有活跃会话
|
|
786
|
+
"""
|
|
787
|
+
with self._streaming_contexts_lock:
|
|
788
|
+
if device_id in self._abort_events:
|
|
789
|
+
logger.info(f"Aborting streaming chat for device {device_id}")
|
|
790
|
+
self._abort_events[device_id].set()
|
|
791
|
+
return True
|
|
792
|
+
else:
|
|
793
|
+
logger.warning(f"No active streaming chat for device {device_id}")
|
|
794
|
+
return False
|
|
795
|
+
|
|
796
|
+
def is_streaming_active(self, device_id: str) -> bool:
|
|
797
|
+
"""检查设备是否有活跃的流式会话."""
|
|
798
|
+
with self._streaming_contexts_lock:
|
|
799
|
+
return device_id in self._abort_events
|